Compare commits
2 Commits
refactor/b
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b71559113 | ||
|
|
26f7738397 |
125
src/features/tmux-subagent/pane-state-parser.ts
Normal file
125
src/features/tmux-subagent/pane-state-parser.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
73
src/features/tmux-subagent/pane-state-querier.test.ts
Normal file
73
src/features/tmux-subagent/pane-state-querier.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, test } from "bun:test"
|
||||||
|
import { parsePaneStateOutput } from "./pane-state-parser"
|
||||||
|
|
||||||
|
describe("parsePaneStateOutput", () => {
|
||||||
|
test("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.toBeNull()
|
||||||
|
expect(result).toEqual({
|
||||||
|
windowWidth: 120,
|
||||||
|
windowHeight: 40,
|
||||||
|
panes: [
|
||||||
|
{
|
||||||
|
paneId: "%0",
|
||||||
|
width: 120,
|
||||||
|
height: 40,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
title: "",
|
||||||
|
isActive: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test("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.toBeNull()
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("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.toBeNull()
|
||||||
|
expect(result?.panes[0]?.title).toBe("title\twith\ttabs")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { spawn } from "bun"
|
import { spawn } from "bun"
|
||||||
import type { WindowState, TmuxPaneInfo } from "./types"
|
import type { WindowState, TmuxPaneInfo } from "./types"
|
||||||
|
import { parsePaneStateOutput } from "./pane-state-parser"
|
||||||
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
|
import { getTmuxPath } from "../../tools/interactive-bash/tmux-path-resolver"
|
||||||
import { log } from "../../shared"
|
import { log } from "../../shared"
|
||||||
|
|
||||||
@@ -27,31 +28,12 @@ export async function queryWindowState(sourcePaneId: string): Promise<WindowStat
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = stdout.trim().split("\n").filter(Boolean)
|
const parsedPaneState = parsePaneStateOutput(stdout)
|
||||||
if (lines.length === 0) return null
|
if (!parsedPaneState) return null
|
||||||
|
|
||||||
let windowWidth = 0
|
const { panes } = parsedPaneState
|
||||||
let windowHeight = 0
|
const windowWidth = parsedPaneState.windowWidth
|
||||||
const panes: TmuxPaneInfo[] = []
|
const windowHeight = parsedPaneState.windowHeight
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const fields = line.split("\t")
|
|
||||||
if (fields.length < 9) continue
|
|
||||||
|
|
||||||
const [paneId, widthStr, heightStr, leftStr, topStr, activeStr, windowWidthStr, windowHeightStr] = fields
|
|
||||||
const title = 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 })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panes.sort((a, b) => a.left - b.left || a.top - b.top)
|
panes.sort((a, b) => a.left - b.left || a.top - b.top)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user