fix(tmux): treat pane-already-closed as success in closeTmuxPane
After send-keys C-c the subprocess running inside the pane (for example "opencode attach") exits on SIGINT, which causes tmux to destroy the pane automatically. The subsequent kill-pane then returns exit 1 with stderr "can't find pane: %NN" even though the end state is exactly what we wanted. Before this fix closeTmuxPane reported failure for that branch, which kept TmuxSessionManager's retryPendingCloses loop marking the (now deleted) pane as still-pending forever and left stale entries behind in the tracked sessions map. This is the behavior the user observed as "screen opens, streaming runs, but cleanup doesn't finish" when running with tmux.isolation="session". Now we detect the "can't find pane" stderr and return true, treating the auto-destroy path the same as an explicit successful kill.
This commit is contained in:
@@ -170,7 +170,7 @@ describe("closeTmuxPane", () => {
|
||||
expect(spawnCalls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("#given kill-pane fails #when closeTmuxPane called #then returns false", async () => {
|
||||
it("#given kill-pane fails with unknown error #when closeTmuxPane called #then returns false", async () => {
|
||||
// given
|
||||
const closeTmuxPane = await loadCloseTmuxPane()
|
||||
queuedProcesses.push(createProcess(0), createProcess(1))
|
||||
@@ -182,6 +182,30 @@ describe("closeTmuxPane", () => {
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it("#given pane already closed by Ctrl+C (kill-pane reports 'can't find pane') #when closeTmuxPane called #then returns true", async () => {
|
||||
// given
|
||||
const closeTmuxPane = await loadCloseTmuxPane()
|
||||
queuedProcesses.push(
|
||||
createProcess(0),
|
||||
{
|
||||
exited: Promise.resolve(1),
|
||||
stdout: createClosedStream(),
|
||||
stderr: new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("can't find pane: %42\n"))
|
||||
controller.close()
|
||||
},
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
// when
|
||||
const result = await closeTmuxPane("%42")
|
||||
|
||||
// then
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it("#given kill-pane stdout stream waits for drain #when closeTmuxPane called #then returns true once drainer consumes stdout", async () => {
|
||||
// given
|
||||
const closeTmuxPane = await loadCloseTmuxPane()
|
||||
|
||||
@@ -46,11 +46,19 @@ export async function closeTmuxPane(paneId: string): Promise<boolean> {
|
||||
killPaneProc.exited,
|
||||
])
|
||||
|
||||
if (exitCode !== 0) {
|
||||
log("[closeTmuxPane] FAILED", { paneId, exitCode, stderr: stderr.trim() })
|
||||
} else {
|
||||
log("[closeTmuxPane] SUCCESS", { paneId })
|
||||
const trimmedStderr = stderr.trim()
|
||||
const paneAlreadyGone = exitCode !== 0 && /can't find pane/i.test(trimmedStderr)
|
||||
|
||||
if (paneAlreadyGone) {
|
||||
log("[closeTmuxPane] SUCCESS (pane already closed by Ctrl+C)", { paneId })
|
||||
return true
|
||||
}
|
||||
|
||||
return exitCode === 0
|
||||
if (exitCode !== 0) {
|
||||
log("[closeTmuxPane] FAILED", { paneId, exitCode, stderr: trimmedStderr })
|
||||
return false
|
||||
}
|
||||
|
||||
log("[closeTmuxPane] SUCCESS", { paneId })
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user