refactor(tmux-subagent): state-first architecture with decision engine (#1125)

* refactor(tmux-subagent): add state-first architecture with decision engine

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* feat(tmux): add pane spawn callbacks for background and sync sessions

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
justsisyphus
2026-01-26 12:02:37 +09:00
committed by GitHub
parent 3a79b8761b
commit 68aa913499
15 changed files with 1390 additions and 255 deletions
+3 -2
View File
@@ -7,5 +7,6 @@ export const SESSION_TIMEOUT_MS = 10 * 60 * 1000 // 10 minutes
// Grace period for missing session before cleanup
export const SESSION_MISSING_GRACE_MS = 6000 // 6 seconds
// Delay after pane spawn before sending prompt
export const PANE_SPAWN_DELAY_MS = 500
// Session readiness polling config
export const SESSION_READY_POLL_INTERVAL_MS = 500
export const SESSION_READY_TIMEOUT_MS = 10_000 // 10 seconds max wait
+81 -12
View File
@@ -51,28 +51,82 @@ export function resetServerCheck(): void {
serverCheckUrl = null
}
export type SplitDirection = "-h" | "-v"
export function getCurrentPaneId(): string | undefined {
return process.env.TMUX_PANE
}
export interface PaneDimensions {
paneWidth: number
windowWidth: number
}
export async function getPaneDimensions(paneId: string): Promise<PaneDimensions | null> {
const tmux = await getTmuxPath()
if (!tmux) return null
const proc = spawn([tmux, "display", "-p", "-t", paneId, "#{pane_width},#{window_width}"], {
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stdout = await new Response(proc.stdout).text()
if (exitCode !== 0) return null
const [paneWidth, windowWidth] = stdout.trim().split(",").map(Number)
if (isNaN(paneWidth) || isNaN(windowWidth)) return null
return { paneWidth, windowWidth }
}
export async function spawnTmuxPane(
sessionId: string,
description: string,
config: TmuxConfig,
serverUrl: string
serverUrl: string,
targetPaneId?: string,
splitDirection: SplitDirection = "-h"
): Promise<SpawnPaneResult> {
if (!config.enabled) return { success: false }
if (!isInsideTmux()) return { success: false }
if (!(await isServerRunning(serverUrl))) return { success: false }
const { log } = await import("../logger")
log("[spawnTmuxPane] called", { sessionId, description, serverUrl, configEnabled: config.enabled, targetPaneId, splitDirection })
if (!config.enabled) {
log("[spawnTmuxPane] SKIP: config.enabled is false")
return { success: false }
}
if (!isInsideTmux()) {
log("[spawnTmuxPane] SKIP: not inside tmux", { TMUX: process.env.TMUX })
return { success: false }
}
const serverRunning = await isServerRunning(serverUrl)
if (!serverRunning) {
log("[spawnTmuxPane] SKIP: server not running", { serverUrl })
return { success: false }
}
const tmux = await getTmuxPath()
if (!tmux) return { success: false }
if (!tmux) {
log("[spawnTmuxPane] SKIP: tmux not found")
return { success: false }
}
log("[spawnTmuxPane] all checks passed, spawning...")
const opencodeCmd = `opencode attach ${serverUrl} --session ${sessionId}`
const args = [
"split-window",
"-h",
splitDirection,
"-d",
"-P",
"-F",
"#{pane_id}",
"-l", String(config.agent_pane_min_width),
...(targetPaneId ? ["-t", targetPaneId] : []),
opencodeCmd,
]
@@ -91,22 +145,37 @@ export async function spawnTmuxPane(
stderr: "ignore",
})
await applyLayout(tmux, config.layout, config.main_pane_size)
return { success: true, paneId }
}
export async function closeTmuxPane(paneId: string): Promise<boolean> {
if (!isInsideTmux()) return false
const { log } = await import("../logger")
if (!isInsideTmux()) {
log("[closeTmuxPane] SKIP: not inside tmux")
return false
}
const tmux = await getTmuxPath()
if (!tmux) return false
if (!tmux) {
log("[closeTmuxPane] SKIP: tmux not found")
return false
}
log("[closeTmuxPane] killing pane", { paneId })
const proc = spawn([tmux, "kill-pane", "-t", paneId], {
stdout: "ignore",
stderr: "ignore",
stdout: "pipe",
stderr: "pipe",
})
const exitCode = await proc.exited
const stderr = await new Response(proc.stderr).text()
if (exitCode !== 0) {
log("[closeTmuxPane] FAILED", { paneId, exitCode, stderr: stderr.trim() })
} else {
log("[closeTmuxPane] SUCCESS", { paneId })
}
return exitCode === 0
}