diff --git a/src/features/tmux-subagent/pane-state-parser.ts b/src/features/tmux-subagent/pane-state-parser.ts
new file mode 100644
index 0000000000..c9aa1ecc9e
--- /dev/null
+++ b/src/features/tmux-subagent/pane-state-parser.ts
@@ -0,0 +1,125 @@
+import type { TmuxPaneInfo } from "./types"
+
+const MANDATORY_PANE_FIELD_COUNT = 8
+
+type ParsedPaneState = {
+ windowWidth: number
+ windowHeight: number
+ panes: TmuxPaneInfo[]
+}
+
+type ParsedPaneLine = {
+ pane: TmuxPaneInfo
+ windowWidth: number
+ windowHeight: number
+}
+
+type MandatoryPaneFields = [
+ paneId: string,
+ widthString: string,
+ heightString: string,
+ leftString: string,
+ topString: string,
+ activeString: string,
+ windowWidthString: string,
+ windowHeightString: string,
+]
+
+export function parsePaneStateOutput(stdout: string): ParsedPaneState | null {
+ const lines = stdout
+ .split("\n")
+ .map((line) => line.replace(/\r$/, ""))
+ .filter((line) => line.length > 0)
+
+ if (lines.length === 0) return null
+
+ const parsedPaneLines = lines
+ .map(parsePaneLine)
+ .filter((parsedPaneLine): parsedPaneLine is ParsedPaneLine => parsedPaneLine !== null)
+
+ if (parsedPaneLines.length === 0) return null
+
+ const latestPaneLine = parsedPaneLines[parsedPaneLines.length - 1]
+ if (!latestPaneLine) return null
+
+ return {
+ windowWidth: latestPaneLine.windowWidth,
+ windowHeight: latestPaneLine.windowHeight,
+ panes: parsedPaneLines.map(({ pane }) => pane),
+ }
+}
+
+function parsePaneLine(line: string): ParsedPaneLine | null {
+ const fields = line.split("\t")
+ const mandatoryFields = getMandatoryPaneFields(fields)
+ if (!mandatoryFields) return null
+
+ const [paneId, widthString, heightString, leftString, topString, activeString, windowWidthString, windowHeightString] = mandatoryFields
+
+ const width = parseInteger(widthString)
+ const height = parseInteger(heightString)
+ const left = parseInteger(leftString)
+ const top = parseInteger(topString)
+ const windowWidth = parseInteger(windowWidthString)
+ const windowHeight = parseInteger(windowHeightString)
+
+ if (
+ width === null ||
+ height === null ||
+ left === null ||
+ top === null ||
+ windowWidth === null ||
+ windowHeight === null
+ ) {
+ return null
+ }
+
+ return {
+ pane: {
+ paneId,
+ width,
+ height,
+ left,
+ top,
+ title: fields.slice(MANDATORY_PANE_FIELD_COUNT).join("\t"),
+ isActive: activeString === "1",
+ },
+ windowWidth,
+ windowHeight,
+ }
+}
+
+function getMandatoryPaneFields(fields: string[]): MandatoryPaneFields | null {
+ if (fields.length < MANDATORY_PANE_FIELD_COUNT) return null
+
+ const [paneId, widthString, heightString, leftString, topString, activeString, windowWidthString, windowHeightString] = fields
+
+ if (
+ paneId === undefined ||
+ widthString === undefined ||
+ heightString === undefined ||
+ leftString === undefined ||
+ topString === undefined ||
+ activeString === undefined ||
+ windowWidthString === undefined ||
+ windowHeightString === undefined
+ ) {
+ return null
+ }
+
+ return [
+ paneId,
+ widthString,
+ heightString,
+ leftString,
+ topString,
+ activeString,
+ windowWidthString,
+ windowHeightString,
+ ]
+}
+
+function parseInteger(value: string): number | null {
+ const parsedValue = Number.parseInt(value, 10)
+ return Number.isNaN(parsedValue) ? null : parsedValue
+}
diff --git a/src/features/tmux-subagent/pane-state-querier.test.ts b/src/features/tmux-subagent/pane-state-querier.test.ts
new file mode 100644
index 0000000000..7088892461
--- /dev/null
+++ b/src/features/tmux-subagent/pane-state-querier.test.ts
@@ -0,0 +1,75 @@
+///
+
+import { describe, expect, it } from "bun:test"
+import { parsePaneStateOutput } from "./pane-state-parser"
+
+describe("parsePaneStateOutput", () => {
+ it("accepts a single pane when tmux omits the empty trailing title field", () => {
+ // given
+ const stdout = "%0\t120\t40\t0\t0\t1\t120\t40\n"
+
+ // when
+ const result = parsePaneStateOutput(stdout)
+
+ // then
+ expect(result).not.toBe(null)
+ expect(result).toEqual({
+ windowWidth: 120,
+ windowHeight: 40,
+ panes: [
+ {
+ paneId: "%0",
+ width: 120,
+ height: 40,
+ left: 0,
+ top: 0,
+ title: "",
+ isActive: true,
+ },
+ ],
+ })
+ })
+
+ it("handles CRLF line endings without dropping panes", () => {
+ // given
+ const stdout = "%0\t120\t40\t0\t0\t1\t120\t40\r\n%1\t60\t40\t60\t0\t0\t120\t40\tagent\r\n"
+
+ // when
+ const result = parsePaneStateOutput(stdout)
+
+ // then
+ expect(result).not.toBe(null)
+ expect(result?.panes).toEqual([
+ {
+ paneId: "%0",
+ width: 120,
+ height: 40,
+ left: 0,
+ top: 0,
+ title: "",
+ isActive: true,
+ },
+ {
+ paneId: "%1",
+ width: 60,
+ height: 40,
+ left: 60,
+ top: 0,
+ title: "agent",
+ isActive: false,
+ },
+ ])
+ })
+
+ it("preserves tabs inside pane titles", () => {
+ // given
+ const stdout = "%0\t120\t40\t0\t0\t1\t120\t40\ttitle\twith\ttabs\n"
+
+ // when
+ const result = parsePaneStateOutput(stdout)
+
+ // then
+ expect(result).not.toBe(null)
+ expect(result?.panes[0]?.title).toBe("title\twith\ttabs")
+ })
+})
diff --git a/src/features/tmux-subagent/pane-state-querier.ts b/src/features/tmux-subagent/pane-state-querier.ts
index 2c581163dd..005e316723 100644
--- a/src/features/tmux-subagent/pane-state-querier.ts
+++ b/src/features/tmux-subagent/pane-state-querier.ts
@@ -1,5 +1,6 @@
import { spawn } from "bun"
import type { WindowState, TmuxPaneInfo } from "./types"
+import { parsePaneStateOutput } from "./pane-state-parser"
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
import { log } from "../../shared"
@@ -27,31 +28,12 @@ export async function queryWindowState(sourcePaneId: string): Promise 8 ? fields.slice(8).join("\t") : ""
- const width = parseInt(widthStr, 10)
- const height = parseInt(heightStr, 10)
- const left = parseInt(leftStr, 10)
- const top = parseInt(topStr, 10)
- const isActive = activeStr === "1"
- windowWidth = parseInt(windowWidthStr, 10)
- windowHeight = parseInt(windowHeightStr, 10)
-
- if (!isNaN(width) && !isNaN(left) && !isNaN(height) && !isNaN(top)) {
- panes.push({ paneId, width, height, left, top, title, isActive })
- }
- }
+ const { panes } = parsedPaneState
+ const windowWidth = parsedPaneState.windowWidth
+ const windowHeight = parsedPaneState.windowHeight
panes.sort((a, b) => a.left - b.left || a.top - b.top)