diff --git a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts index 8ffe7ba2965..aca5c6f1cd8 100644 --- a/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts +++ b/src/renderer/src/components/terminal-pane/use-terminal-pane-close-actions.ts @@ -190,7 +190,7 @@ export function useTerminalPaneCloseActions(controller: TerminalPaneBindingContr clearTimeout(probeTimeout) decide(() => { if ( - (pending ? probes[0]?.verdict === 'exited' : probes[0]?.verdict !== 'live') || + probes[0]?.verdict === 'exited' || settings?.skipCloseTerminalWithRunningProcessConfirm ) { close() diff --git a/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts b/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts index 38d63b2a293..d90da60a63a 100644 --- a/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts +++ b/src/renderer/src/components/terminal/running-terminal-close-guard.test.ts @@ -183,31 +183,20 @@ describe('guardRunningTerminalClose', () => { expect(onClose).not.toHaveBeenCalled() }) - it('fails open and closes when the probe rejects (wedged relay / legacy provider)', async () => { - inspectRuntimeTerminalProcessMock.mockRejectedValue(new Error('rpc_timeout')) - const onClose = vi.fn() - - guard(onClose) - await settleProbe() - - expect(onClose).toHaveBeenCalledTimes(1) - expect(visibleRequest()).toBeNull() - }) - - it('fails open when a remote handle reports client-only unverifiable inspection', async () => { + // #33265: a host that cannot read its child processes must not let a close kill a running agent. + it('prompts when the host cannot observe child processes', async () => { inspectRuntimeTerminalProcessMock.mockResolvedValue({ foregroundProcess: null, hasChildProcesses: false, - verdict: 'unverifiable', - reason: 'transport_loss' + childProcessEvidence: 'unverifiable' }) const onClose = vi.fn() guard(onClose) await settleProbe() - expect(onClose).toHaveBeenCalledTimes(1) - expect(visibleRequest()).toBeNull() + expect(onClose).not.toHaveBeenCalled() + expect(visibleRequest()).toMatchObject({ terminalTabId: 'tab-1' }) }) it('prompts once for a split tab where only the second pane is busy', async () => { diff --git a/src/renderer/src/components/terminal/running-terminal-close-guard.ts b/src/renderer/src/components/terminal/running-terminal-close-guard.ts index 0e63c9cfb94..006a19e7f58 100644 --- a/src/renderer/src/components/terminal/running-terminal-close-guard.ts +++ b/src/renderer/src/components/terminal/running-terminal-close-guard.ts @@ -106,9 +106,14 @@ export function guardRunningTerminalClose(params: { confirmClose(ptyIds) return } - // Answered probe failures/stale handles fail open like Cmd+W; they provide no live-child evidence and must not disable closing. + // "Could not tell" must not kill a bound pty's work (#33265); layout-only ids are often stale leaves that never answer. + const boundPtyIds = new Set(useAppStore.getState().ptyIdsByTabId?.[terminalTabId] ?? []) const busyPtyIds = probes - .filter((probe) => probe.verdict === 'live') + .filter( + (probe) => + probe.verdict === 'live' || + (probe.verdict === 'unverifiable' && boundPtyIds.has(probe.ptyId)) + ) .map((probe) => probe.ptyId) if (busyPtyIds.length === 0) { closeNow()