feat(tmux-subagent): add replace action to prevent mass eviction

- Add column-based splittable calculation (getColumnCount, getColumnWidth)
- New decision tree: splittable → split, k=1 eviction → close+spawn, else → replace
- Add 'replace' action type using tmux respawn-pane (preserves layout)
- Replace oldest pane in-place instead of closing all panes when unsplittable
- Prevents scenario where all agent panes get closed leaving only 1
This commit is contained in:
justsisyphus
2026-01-26 15:25:05 +09:00
parent 8ebc933118
commit 04f2b513c6
7 changed files with 188 additions and 66 deletions
+47
View File
@@ -179,6 +179,53 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
return exitCode === 0
}
export async function replaceTmuxPane(
paneId: string,
sessionId: string,
description: string,
config: TmuxConfig,
serverUrl: string
): Promise<SpawnPaneResult> {
const { log } = await import("../logger")
log("[replaceTmuxPane] called", { paneId, sessionId, description })
if (!config.enabled) {
return { success: false }
}
if (!isInsideTmux()) {
return { success: false }
}
const tmux = await getTmuxPath()
if (!tmux) {
return { success: false }
}
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`
const proc = spawn([tmux, "respawn-pane", "-k", "-t", paneId, opencodeCmd], {
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
if (exitCode !== 0) {
const stderr = await new Response(proc.stderr).text()
log("[replaceTmuxPane] FAILED", { paneId, exitCode, stderr: stderr.trim() })
return { success: false }
}
const title = `omo-subagent-${description.slice(0, 20)}`
spawn([tmux, "select-pane", "-t", paneId, "-T", title], {
stdout: "ignore",
stderr: "ignore",
})
log("[replaceTmuxPane] SUCCESS", { paneId, sessionId })
return { success: true, paneId }
}
export async function applyLayout(
tmux: string,
layout: TmuxLayout,