Skip to content

Fix terminal lifecycle, agent status, dock layout, and remote input ordering - #607

Closed
iamjakeirl wants to merge 25 commits into
greenfield-inc:mainfrom
iamjakeirl:fix/terminal-refresh-after-unmount
Closed

iamjakeirl wants to merge 25 commits into
greenfield-inc:mainfrom
iamjakeirl:fix/terminal-refresh-after-unmount

Conversation

@iamjakeirl

@iamjakeirl iamjakeirl commented Sep 13, 2026 •

Copy link
Copy Markdown

Summary

Switching or closing Panes could leave asynchronous work touching a disposed terminal, report an agent as working or finished at the wrong time, or restore an agent into the shell dock. Remote typing also issued concurrent requests that could reach the host out of order. This PR fixes those paths and gives fresh repository workspaces the same tool launcher and shell dock as worktree workspaces.

The changes span the terminal's lifecycle: launch → input/output and status detection → renderer attach/reconnect → close/archive. The sections below explain the problem, the resulting behavior, and where to review each part.

Branch and review scope

This revision is based on upstream v2.4.129, f977e553, merged at 12e94a0e. Against that upstream commit, the contribution is 54 files, +2,536 / −394 lines, including tests, documentation, and one existing review screenshot. There are 20 non-merge commits and five upstream merge commits in the PR history. Upstream release, dependency, skill-bundle, and other changes brought in by those merges are already on main; they are not additional features introduced by this PR.

Since the previously published head, b51e7fab, this update adds 12e94a0e:

  • Merge upstream v2.4.129 without conflicts. This retains upstream's cached agent-status scans, cached restore serialization, bundled Geist Mono font, local journey timings, deferred dependency loading, and background login-shell PATH probing alongside the existing lifecycle/status/input fixes.
  • Adapt the font-settings fixture to explicitly start with a collapsed dock, as its single-terminal assertion requires. The personal branch intentionally defaults to an expanded dock.
  • Make the keyboard-copy regression wait for xterm to mount and finish its loading/activation overlay before selecting text. A reproduced race let the test select while terminal restoration was still able to clear the selection. The corrected case passed eight consecutive runs.
  • No additional application-code changes were required to integrate this upstream release. These two fixture adjustments are the only changes beyond the automatic merge.

Earlier updates remain in the history: 09da5133 integrated v2.4.115; b68ff505 preserved bare-Escape input boundaries and handled discarded keyboard input; b51e7fab integrated v2.4.125 and adapted lifecycle guards to worker-backed terminal snapshots.

Suggested review order: 1–2 for terminal ownership and teardown, 3–4 for status detection and consumers, 5 for dock/launcher behavior, then 6 for remote input. Each section lists its implementation and regression coverage so these can be reviewed in smaller groups.

1. Keep asynchronous work attached to the correct terminal

Problem: A refresh or refocus operation can await a resize/state reply while the user switches or archives a session. Its captured xterm may have been disposed when the reply arrives, producing the original Windows terminal dimensions error. Main-process callbacks can similarly outlive the terminal they were created for, even when a replacement reuses the same panel ID.

Changes and reasons:

  • After awaited resize and state requests, renderer refresh/refocus code verifies that its captured xterm is still current before reading dimensions or refreshing it. This preserves normal refresh behavior while discarding obsolete work.
  • Main-process output, exit, initial-command, CLI-ready, initial-prompt, delayed submit, and forced-resize paths verify terminal identity and teardown state. An old process or timer cannot write into, resize, unregister, or report status for its replacement.
  • Terminal handlers are installed before asynchronous launch-state persistence. If another initialization wins while PTY spawning is awaited, the redundant PTY is killed instead of replacing the registered terminal.
  • State reads and saves recheck terminal identity after restoreSnapshot(); saves also recheck panel identity. A deleted/replaced panel cannot receive a late snapshot. The save reads the current panel.state after the await, preserving changes such as input-delivery timestamps, selection state, and dimensions made while the worker was responding.
  • A PTY write exception is logged without removing the terminal from tracking. A failed write alone does not establish that the process exited; the exit callback remains responsible for retirement.

Implementation: frontend/src/components/panels/TerminalPanel.tsx; main/src/services/terminalPanelManager.ts.

Coverage: tests/terminal-blur-recovery.spec.ts holds resize replies across session switches and verifies no disposed-terminal error or change to the replacement terminal. terminalPanelManager.status.test.ts covers replacement callbacks, stale worker snapshots, panel deletion/replacement, concurrent panel-state updates, delayed initial input, and write failure followed by a real exit.

2. Drain and persist before teardown; reclaim resources on failure

Problem: Fire-and-forget persistence raced emulator disposal and panel deletion. Repeated close requests, natural exit during a save, and errors while delivering output could also leave inconsistent terminal ownership or cleanup.

Changes and reasons:

  • destroyTerminal() returns a shared promise for a terminal's teardown. Repeated callers join the same operation; detection and new input/output processing stop as teardown begins.
  • For terminals being retained, teardown awaits the worker's restore snapshot and persistence before disposing the emulator. This preserves output already accepted before teardown and the rendered scrollback used for restoration.
  • Panel deletion awaits teardown with { saveState: false }, avoiding a snapshot/database write for a panel about to be deleted. Session archive and Pane Chat agent switching await normal teardown before proceeding.
  • If the PTY exits while a save is pending, retain its exit code/signal until the save finishes. Retirement happens once, and a process that already exited is not killed again.
  • Retirement clears output timers, flow-control state, terminal/viewer/buffer maps, and status tracking. Cleanup still disposes the emulator and attempts to reclaim the PTY if persistence, output/status delivery, or emulator disposal fails. WSL's delayed kill remains scheduled even if writing exit fails.
  • Lifecycle exit events retain panel/session identity even if the panel was already removed, allowing journal/transport consumers to observe the exit.

Implementation: terminalPanelManager.ts; its callers in main/src/ipc/panels.ts, main/src/ipc/session.ts, and main/src/services/paneChatManager.ts. The terminalStateEmulator.ts comment now describes the awaited teardown behavior.

Coverage: terminalPanelManager.status.test.ts covers repeated destruction, drain-before-dispose, successful/signaled exit during save, deletion without persistence, event-delivery failure, disposal failure, and WSL exit-write failure. terminalPanelManager.persistence.test.ts awaits teardown in its cleanup/restore scenarios.

3. Distinguish startup, actual work, reliable idle, and live blockers

Problem: Boot banners, inherited shell titles, cursor redraws, and typing could create false working/completed transitions. Conversely, a prompt that remains visible during a turn could mark real work idle. Broad prompt matching could keep an agent blocked by answered questions in scrollback.

Changes and reasons:

  • A new terminal publishes unknown. Detection waits for a pending initial command to be injected, clears the launch shell's title in the headless model, and restarts the monitor's grace window at injection. Legacy idle waits remain pending during startup instead of succeeding before the tool launches.
  • During startup grace, unclassified output does not invent a task. Explicit working or blocked evidence can still take effect immediately.
  • The monitor prioritizes live blockers, explicit working chrome, then reliable idle evidence, using PTY byte activity as fallback. Reliable idle clears accumulated activity so redraws do not restart a finished task. Real work can continue through a transient weak prompt using recent activity.
  • Claude detection recognizes circle and braille title spinners and live status lines. Trust/permission prompts outrank stale working titles and are scoped to the live prompt region. Its composer is no longer considered reliable completion evidence because it remains visible during work.
  • Codex detection recognizes wrapped working text and both transcript-viewer close labels. Weak yes/no blockers must resemble a live question after the current prompt marker, reducing matches against completed prose and scrollback.
  • Emitted reasons distinguish matched detector rules, activity fallback, and idle settling. Legacy activity events derive from the status being emitted so the two status views agree. Status comments clarify that plain shells can participate through generic activity detection and unknown means no detected status yet.

Implementation: main/src/services/agentStatus/agentStatusMonitor.ts, manifests.ts, and terminalPanelManager.ts; the visibleIdle contract in shared/types/agentStatus.ts; related frontend status comments. docs/ADDING_NEW_CLI_TOOLS.md documents reliable idle evidence, precedence, and startup handling for future integrations.

Coverage: agentStatusMonitor.test.ts, manifests.test.ts, terminalPanelManager.status.test.ts, and terminalPanelManager.persistence.test.ts cover boot/grace timing, delayed command injection, inherited titles, real startup work, idle redraws, typing, live blockers, answered prompts, and wrapped CLI output.

4. Reconcile status after reconnect without announcing false completions

Problem: Live events alone cannot reconstruct status when a renderer attaches to an already-running daemon or misses transitions while disconnected. Applying an older snapshot over a newer event can regress state; interpreting a snapshot or terminal shutdown as a completed turn can trigger false notifications and unseen-completion badges.

Changes and reasons:

  • Add the daemon-owned panels:agent-statuses read, returning authoritative monitor state for terminal panels in non-archived sessions, including hidden Pane Chat. A panel without a detected state is returned as unknown.
  • App installs a shared status subscription that listens before requesting its initial snapshot and refreshes on remote resync. Live events received during a read win over that snapshot; superseded requests and replies after unsubscribe are ignored.
  • Successful snapshots remove stale status entries. Deletion suppresses late events/snapshots for the removed panel, while recreation or a later reconnect snapshot can restore a reused ID. Reloading a session's panel list also prunes status for panels that disappeared. Invalid snapshot replies are logged without clearing known state.
  • A snapshot-version counter lets notification subscribers replace their baseline atomically. Hydration, reconnect reconciliation, and exit/destroyed events update display state without inventing a new completed turn. A subsequent real working → idle transition still marks a background session as completed when the session has settled.
  • Mobile push skips completion/needs-input notifications for terminal endings. The workspace journal records lifecycle exits separately from agent-turn completion and resets its per-panel lifecycle state at terminal_start, so successive processes using the same panel ID can each record an exit.

Implementation: frontend/src/services/panelStatusSync.ts, App.tsx, stores/panelStore.ts, types/panelStore.ts, and hooks/useNotifications.ts; main/src/ipc/panels.ts, services/workspaceJournal.ts, and daemon/mobilePushSender.ts.

Coverage: panelStatusSync.test.ts, panelStore.test.ts, panels.status.test.ts, daemonRegistryBindings.test.ts, workspaceJournal.test.ts, and mobilePushSender.test.ts. tests/agent-status.spec.ts checks sidebar/tab state and notification behavior through attach, reconnect, real completion, terminal ending, and deletion.

5. Keep shells in the dock and agents in working tabs

Problem: Treating the first terminal as the dock could move an agent there after the original shell was deleted or the workspace reopened. Main repository workspaces also differed from worktree workspaces: their empty state only offered “Open a terminal,” and their shell occupied the main stage. Dock promotion and asynchronous tab restoration could leave the selected tab out of sync with persisted state.

Changes and reasons:

  • getDockTerminalPanel() selects the first plain shell. It excludes terminals with an initial command or CLI/agent metadata, including tools whose runtime metadata has not arrived yet. Additional shells remain working tabs until one is promoted into the dock.
  • Both workspace views use the shared TerminalDock and EmptyPanelStage. The launcher offers Terminal, environment-appropriate agent presets, configured custom commands, icons, and shortcuts; it remains available above an open dock when there are no working tabs.
  • The dock defaults to expanded for a profile without a saved preference, preserves the existing collapse preference, and expands when a newly created shell becomes the dock. Its shared implementation retains resizing, hidden/inert behavior, and non-autofocusing terminal content; non-permanent shells have a close action.
  • Worktree layout reconciliation excludes the dock shell. Closing it locally or through a backend deletion event removes any promoted shell from the working layout before repairing/persisting selection, focus, and zoom.
  • Main repository selection repair waits for saved panels/active-tab restoration before choosing a fallback. This preserves a saved non-first tab and repairs selection when the old active panel belongs to the dock/inspector or was deleted.
  • Automatic shell creation checks whether any terminal already exists, so an agent-only workspace does not gain an unwanted replacement shell merely because the dock is absent.

Implementation: frontend/src/components/ProjectView.tsx, SessionView.tsx, panels/TerminalDock.tsx, panels/EmptyPanelStage.tsx, and utils/terminalDock.ts. Existing CHANGELOG.md entries describe the dock default and tab-retention behavior.

Coverage: terminalDock.test.ts and tests/terminal-dock.spec.ts cover launcher availability, agent launching, runtime-metadata restoration, shell removal, single/split layout promotion, saved collapse state, and active-tab persistence. The Electron browser fixture now persists layout/selection and emits panel creation/deletion events. Existing adaptive-layout, font-settings, and selection-popover tests explicitly request a collapsed starting dock where their scenario requires one, preserving their original assertions under the new default. The keyboard-copy test waits for a mounted, restored terminal before selecting text, so startup cannot clear the test selection.

6. Preserve remote typing order and Escape boundaries

Problem: Concurrent HTTP input requests could arrive in a different order from the user's keystrokes. Serializing every buffered key into a separate request would add avoidable latency. Retrying an interrupted write could duplicate text or replay part of a command that already reached the host; combining a bare Escape with the next key could turn it into an Alt shortcut.

Changes and reasons:

  • Desktop and browser remote clients share RemoteInputQueue. It allows one input request in flight per panel across terminal:input and panels:send-terminal-input; different panels and non-input commands remain independent.
  • Input arriving during an active request is combined into pending batches when the channel matches. Combined batches are capped at 64 Ki characters without splitting an individual input event/paste. Callers retain the response or failure associated with their batch.
  • A pending batch ending in bare Escape cannot absorb the next event. Escape therefore ends its write, while complete escape sequences and pasted data remain intact.
  • Failure, disconnect/reconnect cancellation, or a ten-second input timeout rejects outstanding callers and discards buffered input. Requests are never replayed: delivery of an interrupted in-flight write is uncertain. Late completion of a canceled queue cannot disturb a new queue for the same panel.
  • The renderer's sendTerminalInput() helper handles delivery rejection for fire-and-forget typing, special-key, paste, and interceptor paths. It logs the failure instead of allowing one global unhandled-rejection alert per discarded keystroke.

Implementation: shared/remoteInputQueue.ts; main/src/daemon/client/remotePaneClient.ts; frontend/src/remote/runtime/remoteDaemonBrowserClient.ts; frontend/src/utils/terminalInput.ts and its TerminalPanel.tsx call sites. docs/remote-daemon-lifecycle.md documents ordering, batching, Escape, timeout, and no-replay semantics.

Coverage: remoteTerminalInput.test.ts exercises both client implementations against a delayed local HTTP server, covering fast typing, Unicode/control sequences, batching, independent panels, both input channels, HTTP errors, and disconnects. remoteInputQueue.test.ts covers Escape boundaries, batch sizing, timeout, and late completion after cancellation. terminalInput.test.ts verifies delivery rejection is handled. remotePwaBrowserRuntime.test.ts now supplies valid input arguments when checking an invalid response from the host.

Compatibility and areas needing care

  • Remote input uses the existing HTTP channels and payloads; the input-ordering change requires no host protocol upgrade.
  • Status hydration adds panels:agent-statuses. An older daemon without that handler cannot supply the new baseline; the renderer logs the failure and retains live-event handling. Mixed-version status reconciliation was not exercised in this refresh.
  • destroyTerminal() is now asynchronous. Ordering-sensitive deletion, archive, and agent-switch callers await it; the persistence format and database schema are unchanged.
  • The default dock expansion is an intentional UI change; saved collapse preferences continue to apply.
  • The v2.4.125 integration keeps upstream's worker emulator and pushed screen-state polling. The lifecycle checks wrap its asynchronous snapshot reads rather than replacing that architecture.

Validation of this revision

Fresh local checks for 12e94a0e, using Node 22.18.0, pnpm 10.19.0, and Electron 41.10.3 in WSL/Linux:

Check Result
pnpm lint Passed after the fixture adjustments, including Oxlint, residual ESLint, boundary-decoder conformance, and Knip; zero advisory anti-slop findings.
pnpm typecheck Passed across the workspace.
pnpm build:main Passed, including sandboxed preload verification.
pnpm build:frontend Passed, including xterm request-mode and production React Scan checks; Vite reported a non-blocking large-chunk warning.
pnpm --filter frontend test 377 passed, 40 files.
Focused backend selection below, under Electron's Node runtime 274 passed, 2 failed, 1 skipped, 19 files. Both failures are the launcher-test environment issue explained below.
pnpm --filter main exec vitest run src/services/skillCacheManager.test.ts under Node 16 passed, 1 skipped; this includes both launcher cases that failed under Electron.
Six selected Playwright specs below Final combined run: 60 passed, 1 clock-timing failure, 11 not run. Rerunning the complete blur/recovery spec alone: 14 passed, covering the failed case and all 11 that its serial group had skipped. All 72 selected scenarios passed across those final runs.
Keyboard-copy regression repeated with two workers 8 passed after the fixture correction.
pnpm exec electron-builder --linux AppImage --x64 --publish never --config.npmRebuild=false Passed. The package carries version 2.4.129 and commit 12e94a0e.
Isolated packaged headless startup Reached daemon-ready state; then stopped by the test's intentional 15-second timeout.
git diff --check upstream/main...HEAD Passed. The automatic upstream merge includes existing whitespace in shellPath.ts; no whitespace errors are introduced by the contribution against upstream.

Launcher-test diagnosis: The unchanged upstream test helper replaces its child-process PATH with the directory containing process.execPath. Under Electron that directory has neither node nor npx, which the generated launcher requires. A direct probe confirmed both are absent from the restricted PATH; the complete skill-cache suite passes under regular Node. The Windows-only test is skipped on Linux. No production or test behavior was weakened to hide these failures.

Browser timing limitation: The remaining combined-run failure was clock.pauseAt: Cannot fast-forward to the past in the 10,001 ms blur case. The complete 14-test blur/recovery file passed when rerun alone. The original font/clipboard failures were addressed by the two fixture changes described above.

Exact focused backend and browser commands
ELECTRON_RUN_AS_NODE=1 pnpm --filter main exec electron node_modules/vitest/vitest.mjs run \
  src/services/terminalPanelManager.persistence.test.ts \
  src/services/terminalPanelManager.status.test.ts \
  src/services/terminalPanelManager.test.ts \
  src/services/terminalStateEmulator.test.ts \
  src/services/agentStatus \
  src/ipc/panels.status.test.ts \
  src/ipc/daemonRegistryBindings.test.ts \
  src/daemon/mobilePushSender.test.ts \
  src/services/workspaceJournal.test.ts \
  src/daemon/client/remoteInputQueue.test.ts \
  src/daemon/client/remoteTerminalInput.test.ts \
  src/daemon/remotePwaBrowserRuntime.test.ts \
  src/utils/shellPath.test.ts \
  src/database/database.journey-timings.test.ts \
  src/services/analyticsIdentity.test.ts \
  src/services/skillCacheManager.test.ts

pnpm --filter main exec vitest run src/services/skillCacheManager.test.ts

# Browser fixtures with mocked Electron IPC, using a separate Vite server:
pnpm --filter frontend exec vite --host 127.0.0.1 --port 4527 --strictPort
# In another terminal:
PLAYWRIGHT_PORT=4527 pnpm exec playwright test \
  tests/terminal-dock.spec.ts \
  tests/agent-status.spec.ts \
  tests/terminal-blur-recovery.spec.ts \
  tests/adaptive-panel-layout.spec.ts \
  tests/terminal-selection-popover.spec.ts \
  tests/settings.spec.ts --workers=2

PLAYWRIGHT_PORT=4527 pnpm exec playwright test tests/terminal-blur-recovery.spec.ts --workers=1
PLAYWRIGHT_PORT=4527 pnpm exec playwright test tests/terminal-selection-popover.spec.ts \
  --grep 'keeps keyboard copy' --workers=2 --repeat-each=8

Installed personal-build verification: Installed the merged 12e94a0e build as both the Windows desktop application and the primary WSL AppImage daemon. Verified version 2.4.129, commit identity, and personal lifecycle-fix markers directly in both installed packages, including the archive mounted by the running daemon. The Windows application reported that version/commit at runtime, opened a responding Pane window, and established a connection to the primary WSL daemon. Both primary and development daemon health endpoints returned ready. The Windows package reuses the existing, matching Electron 41.10.3 runtime and unchanged Windows native dependencies; 1,201 application files were compared with the new build and 22 native files with the previous Windows installation. Previous application resources and local session databases were backed up before replacement. This is startup/connection verification, not exhaustive interactive testing.

Validation limits: The complete backend suite, full Playwright suite, packaged macOS DMG, and exhaustive interactive Windows flows were not run for this revision. Browser fixtures validate renderer behavior with mocked IPC. The direct remote-input tests exercise both client implementations against a local HTTP server; they do not cover every real-network condition.

Earlier evidence, retained for context only: The original PR description reported manual dock/startup/status testing with a Windows development client connected to the WSL daemon. It also reported a full backend run with 996 passed, 3 failed, and 2 skipped, with failures reproduced on then-unchanged upstream: two skill-cache launcher tests and one macOS Tailscale DNS timeout in WSL. Those are historical results, not a full-suite result for this revision.

Existing UI reference

Fresh repository launcher above the terminal dock, captured for the earlier revision using the browser fixture. The shell is shown loading; this is a layout reference, not a new screenshot of the v2.4.129 build.

Fresh repository launcher above the terminal dock

Related work

The earlier PR description links #572 for neighboring activation/launcher-flashing work. The disposed-terminal guards here specifically address asynchronous refresh work completing after its terminal has been replaced or disposed.

@iamjakeirl iamjakeirl changed the title Fix terminal dimensions error after switching sessions Fix agent status reporting, terminal dock, and fresh pane startup Sep 13, 2026
@iamjakeirl iamjakeirl changed the title Fix agent status reporting, terminal dock, and fresh pane startup Fix terminal lifecycle, agent status, dock layout, and remote input ordering Sep 24, 2026
@iamjakeirl

Copy link
Copy Markdown
Author

also sorry this PR is massive i've only ever solo'd projects before this and am getting used to github. future prs will be branched properly

@parsakhaz

Copy link
Copy Markdown
Member

Thanks for this, @iamjakeirl. The fixes are solid, and you tracked down some nasty bugs. To make review and landing easier, we've split this PR into five smaller ones. Your commits are kept as authored by you:

Each change was checked against current main, and all of them are still needed. Closing this one in favor of those. Review comments go on the new PRs.

@parsakhaz parsakhaz closed this Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants