Skip to content

[Bug]: Dismissing 'Remote terminal was closed' toast clears alert banner without tearing down dead pane #21342

Closed

Description

@LesleyMurfin

[Bug]: Dismissing 'Remote terminal was closed' toast clears alert banner without tearing down dead pane

Metadata


Executive Summary

When a remote terminal session terminates or unexpectedly disconnects on a headless server, Orca surfaces a notification banner at the base of the terminal surface:
"Remote terminal was closed." (or localized in bilingual builds: "Remote terminal was closed. / 远程终端已关闭", refs #9194).

When the user clicks the dismiss button (×) on TerminalErrorToast, the handler dismissTerminalError() in use-terminal-pane-title-state.ts clears the active error string from component state (setTerminalError(null)). This unmounts the notification overlay.

However, no teardown or close event is dispatched to the underlying terminal container or parent split layout. The underlying dead terminal pane—now lacking an active PTY, ignoring keyboard input, and displaying an inert cursor or frozen buffer—remains mounted indefinitely inside the split tree and tab strip. The user is left with an unresponsive "zombie" pane that cannot be recovered or closed from the terminal surface itself.


Technical Problem Description & Root Cause

1. The Disconnect Between Alert Dismissal and Container Teardown

In src/renderer/src/components/terminal-pane/TerminalErrorToast.tsx, the dismiss action is rendered as a simple click handler:

<button
  onClick={onDismiss}
  style={{
    background: 'none',
    border: 'none',
    color: 'inherit',
    cursor: 'pointer',
    fontSize: 14,
    padding: '0 0 0 8px',
    lineHeight: 1,
    flexShrink: 0
  }}
>
  ×
</button>

The onDismiss prop is wired directly from TerminalPaneSurface.tsx:

{visibleTerminalError && isActive && !showSshReconnectOverlay && activePane
  ? createPortal(
      <TerminalErrorToast
        error={visibleTerminalError}
        onDismiss={dismissTerminalError}
        onRestartDaemon={() => daemonActions.setPending('restart')}
        onRetry={ ... }
      />,
      activePane.container,
      `terminal-error-${activePane.id}`
    )
  : null}

2. State Mutation in use-terminal-pane-title-state.ts

dismissTerminalError is implemented as:

const dismissTerminalError = useCallback(() => {
  const paneId = managerRef.current?.getActivePane()?.id ?? null
  setTerminalError(null)
  if (paneId !== null) {
    setTerminalErrorsByPaneId((current) => clearPaneTerminalError(current, paneId))
    paneTransportsRef.current.get(paneId)?.notifyErrorSurfaceDismissed?.()
  }
}, [])

This function performs two mutations:

  1. setTerminalError(null): Sets local component state to null, causing visibleTerminalError to evaluate falsy.
  2. notifyErrorSurfaceDismissed(): Informs the transport layer that the error visual was dismissed.

Crucially, neither dismissTerminalError nor TerminalErrorToast ever calls:

  • executeClosePane(paneId) to remove the dead pane from managerRef or collapse the split layout.
  • session.tabs.close(tabId) to inform the host runtime and client store that the dead tab should be closed and tombstoned.

3. The Lifecycle Breakdown

TerminalErrorToast conflates two fundamentally different classes of errors:

  1. Transient Recoverable Errors: Errors such as SSH reconnect attempts or unverifiable pane owners where the user may dismiss the banner to inspect scrollback while recovery occurs.
  2. Terminal Termination / Death: Wire markers like REMOTE_TERMINAL_CLOSED_MARKER = 'Remote terminal was closed.'. In this case, the underlying PTY process on the remote host is permanently dead. Keeping the pane container mounted without a running process produces an inert, unresponsive black box.

Operational & User Impact

  1. Zombie Split Panes: In multi-pane grid layouts (e.g. 2×2 split layouts created during agent workflows), an exited remote terminal leaves a dead quarter. After dismissing the error banner, the split node remains fixed in the layout with no clear way to close it from the terminal surface.
  2. Input Trapping & Perceived Freeze: Clicking into the dead pane shifts focus to an inert DOM element. Keystrokes are swallowed or dropped, leading users to believe the desktop application has frozen.
  3. Workspace State Pollution: Because client layout states are serialized and persisted to disk across application restarts, the dead pane handle persists in orca-data.json. On subsequent launches or reconciler passes, the dead pane is remounted, re-triggering attachment errors and contributing to ghost tab accumulation (Paired viewer retains unclosable null-PTY terminal ghosts after host topology advances #11800, [Bug] Flickering/Broken file tabs in workspace, unable to close/delete #21189).
  4. Bilingual Confusion ([Bug]: Phantom remote-closed terminal pane cannot be dismissed (banner mixes EN + "如果这种情况持续存在,请 提交议题.") #9194): On localized builds, the error banner displayed bilingual strings ("Remote terminal was closed. / 远程终端已关闭"). While [Bug]: Phantom remote-closed terminal pane cannot be dismissed (banner mixes EN + "如果这种情况持续存在,请 提交议题.") #9194 addressed string localization, the underlying failure mode—leaving the pane unclosable after toast dismissal—persisted unaddressed.

Architectural State Matrix (Uniform 6-Column Format)

Lifecycle Stage Event / Trigger Current UI Action Defect / Failure Mode Required Fix Target Component / Symbol
Terminal Exit Remote shell exits or PTY process terminated on 10.x.x.x TerminalErrorToast displays "Remote terminal was closed." Error banner obscures bottom terminal buffer Surface explicit teardown actions in banner TerminalErrorToast.tsx
User Dismiss User clicks "×" dismiss button Invokes dismissTerminalError() Clears banner only; dead pane container remains mounted Trigger pane/tab teardown if terminal is terminated use-terminal-pane-title-state.ts
Layout Tree Split layout has multiple active leaves Banner disappears; split leaf remains in DOM Inert black box occupies layout grid permanently Collapse split leaf via executeClosePane(paneId) use-terminal-pane-close-actions.ts
Single Tab Single pane occupies terminal tab Banner disappears; empty dead terminal tab lingers User cannot recover shell; must manually find tab "×" Dispatch session.tabs.close(tabId) to close tab web-runtime-session-tab-lifecycle.ts
Transport Layer Remote PTY unreachable or closed notifyErrorSurfaceDismissed() called State machine remains latched; no tombstone registered Enforce terminal death transition and tombstone write remote-runtime-pty-transport.ts
State Sync Reconciler hydration on app reload Stale pane pointer restored from orca-data.json Zombie pane resurrects across window reloads (#21041) Record durable tombstone preventing resurrection use-terminal-store-reconciler.ts

Reproduction Steps

  1. Setup Remote Environment: Start an orca serve daemon on a headless Linux host (10.x.x.x).
  2. Connect Client: Open the Orca desktop application on macOS or Windows and connect to the remote workspace.
  3. Open Split Terminal: Open a terminal tab and split it horizontally or vertically into two panes.
  4. Terminate Remote Shell: In one of the split panes, execute exit or terminate the backing PTY process on the host (kill -9 <pid>).
  5. Observe Error Toast: The bottom banner displays: Remote terminal was closed.
  6. Click Dismiss: Click the "×" button on the toast banner.
  7. Observe Failure:
    • The toast banner disappears.
    • The dead split pane remains rendered in the layout.
    • The terminal accepts no keystrokes and cannot be revived.
    • No close action is offered on the dead surface.

Proposed Solution & Implementation

1. Decouple "Dismiss Banner" from "Close Dead Pane / Tab"

In TerminalErrorToast.tsx, when the error corresponds to a permanent terminal termination (REMOTE_TERMINAL_CLOSED_MARKER), provide two distinct user actions:

  • "Keep Output" (or "Dismiss"): Retains the frozen scrollback buffer for copying while hiding the banner.
  • "Close Terminal" (or "Close Pane"): Actively closes the dead pane and tears down the container.

Alternatively, if the generic "×" button is clicked on a terminated session, trigger the full teardown flow.

2. Connect Teardown to session.tabs.close(tabId) and executeClosePane(paneId)

In TerminalPaneSurface.tsx, pass a dedicated onClose callback to TerminalErrorToast:

<TerminalErrorToast
  error={visibleTerminalError}
  onDismiss={dismissTerminalError}
  onClose={() => {
    dismissTerminalError()
    if (paneCount <= 1) {
      void callWebRuntimeSessionTabMethod('session.tabs.close', {
        worktreeId,
        tabId,
        reason: 'user'
      })
    } else if (activePane) {
      handleRequestClosePane(activePane.id)
    }
  }}
  onRestartDaemon={() => daemonActions.setPending('restart')}
/>

3. Update TerminalErrorToast Component UI

Render an explicit action button when the error matches REMOTE_TERMINAL_CLOSED_MARKER:

{error.includes(REMOTE_TERMINAL_CLOSED_MARKER) && onClose ? (
  <Button
    variant="destructive"
    size="xs"
    onClick={onClose}
    className="ml-3 shrink-0"
  >
    {translate('auto.components.terminal.pane.TerminalErrorToast.closePane', 'Close Pane')}
  </Button>
) : null}

Test Verification Plan

  1. Unit Test (TerminalErrorToast.test.ts):
    • Verify that when error contains REMOTE_TERMINAL_CLOSED_MARKER, the "Close Pane" action button is rendered.
    • Verify clicking "Close Pane" triggers the onClose callback.
  2. Integration Test (TerminalPaneSurface.test.tsx):
    • Mock a dead remote terminal error.
    • Simulate clicking "Close Pane" on the toast.
    • Assert that handleRequestClosePane is called for split panes, or session.tabs.close is dispatched for single-pane tabs.
    • Verify that the dead pane node is removed from the DOM and split tree.
  3. Regression Test against [Bug]: Phantom remote-closed terminal pane cannot be dismissed (banner mixes EN + "如果这种情况持续存在,请 提交议题.") #9194:
    • Verify localized strings in non-English environments render cleanly alongside the close action without triggering view transition collisions or reconciler remount loops.

Activity

  1. AmethystLiang commented on Sep 18, 2026

    @AmethystLiang
    Contributor

    A fix is being actively worked on. Target roll out date is early next week

  2. added
    bugSomething isn't working
    orca-remote-serverRemote Orca Server / orca serve / paired remote runtime
    on Sep 19, 2026
  3. added theissue type on Sep 19, 2026
  4. nwparker commented on Oct 26, 2026

    @nwparker
    Contributor

    Thanks for the report, @LesleyMurfin. Fixed in #33949: when the server reports a terminal closed, the desktop now removes its copy of that tab instead of keeping a dead pane. A split with one pane still running is kept. The "Remote terminal was closed." message also has a Close Pane button now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingorca-remote-serverRemote Orca Server / orca serve / paired remote runtimeterminal

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions