From 20f7bb6a69d9a5386585030592ea85641ec72f4a Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 26 Oct 2026 07:16:04 -0700 Subject: [PATCH] fix(session-tabs): retire terminal rows the host closed, and offer Close Pane on a closed remote terminal (#33949) * fix(session-tabs): retire kept terminal rows the host closed, and offer Close Pane on a closed remote terminal * fix(session-tabs): a live handle outranks an older close record --- .../terminal-pane/TerminalErrorToast.test.ts | 25 +++++ .../terminal-pane/TerminalErrorToast.tsx | 18 +++- .../terminal-pane/TerminalPaneSurface.tsx | 1 + ...sion-restored-placeholder-identity.test.ts | 92 +++++++++++++++++++ .../apply-preparation-base.ts | 10 ++ .../host-retired-terminal-rows.ts | 63 +++++++++++++ 6 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/runtime/web-session-tabs-sync/host-retired-terminal-rows.ts diff --git a/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts b/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts index 77dd73abbf6..8c501cc5fc8 100644 --- a/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts +++ b/src/renderer/src/components/terminal-pane/TerminalErrorToast.test.ts @@ -660,3 +660,28 @@ it('uses existing Korean recovery copy and preserves the path', async () => { await i18n.changeLanguage('en') } }) + +// #21342: dismissing hid the toast but left the dead pane with no way out. +it('offers Close Pane only for a pane whose remote terminal was closed', () => { + const onClosePane = vi.fn() + const view = render( + React.createElement(TerminalErrorToast, { + error: 'Remote terminal was closed.', + paneOnClient: false, + onDismiss: vi.fn(), + onClosePane + }) + ) + fireEvent.click(view.getByText('Close Pane')) + expect(onClosePane).toHaveBeenCalledTimes(1) + + view.rerender( + React.createElement(TerminalErrorToast, { + error: 'Paste failed.', + paneOnClient: false, + onDismiss: vi.fn(), + onClosePane + }) + ) + expect(view.queryByText('Close Pane')).toBeNull() +}) diff --git a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx index 6202f00eb9f..83ee945a6a5 100644 --- a/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx @@ -99,6 +99,11 @@ export function isExplainedTerminalError(error: string): boolean { ) } +/** The host no longer has this pane's terminal, so nothing can come back into it. */ +export function isRemoteTerminalClosedError(error: string): boolean { + return error.includes(REMOTE_TERMINAL_CLOSED_MARKER) +} + export function isPaneOwnerUnverifiedError(error: string): boolean { const lines = error.split('\n').filter((line) => line.length > 0) return lines.length > 0 && lines.every((line) => line.includes(PANE_OWNER_UNVERIFIED_MARKER)) @@ -221,7 +226,8 @@ export function TerminalErrorToast({ onDismiss, onRestartDaemon, onRetry, - onOpenNewTerminal + onOpenNewTerminal, + onClosePane }: { error: string agentId?: string | null @@ -232,6 +238,8 @@ export function TerminalErrorToast({ onRetry?: () => Promise /** Leaves this pane and its saved session untouched. */ onOpenNewTerminal?: () => void + /** Dismissing only hides the toast; the dead pane needs a way out from where the user is looking (#21342). */ + onClosePane?: () => void }): React.JSX.Element { const ssh = isSshHostStateTerminalError(error) // Why: the client's OS and shell describe neither the host nor its shell, and the renderer knows neither. @@ -404,6 +412,14 @@ export function TerminalErrorToast({ )} ) : null} + {onClosePane && isRemoteTerminalClosedError(error) ? ( + + ) : null}