Skip to content

feat(terminal): shell fallback retry + persisted sanitized history - #21

Merged
sambitcreate merged 8 commits into
fix/terminal-posix-spawnp-macfrom
fix/terminal-shell-fallback-history
Aug 11, 2026
Merged

feat(terminal): shell fallback retry + persisted sanitized history#21
sambitcreate merged 8 commits into
fix/terminal-posix-spawnp-macfrom
fix/terminal-shell-fallback-history

Conversation

@sambitcreate

Copy link
Copy Markdown
Owner

Summary

Two improvements adapted from studying t3code's terminal, building directly on the posix_spawnp fix in #20.

Stacks on #20. Target this branch's base (fix/terminal-posix-spawnp-mac) until #20 merges, then rebase onto main.

1. Shell-candidate fallback with retry-on-spawn-failure

A broken $SHELL (or a shell that vanishes after a Homebrew uninstall) no longer breaks terminal creation. The spawn path now walks an executable candidate list — $SHELL/bin/zsh/bin/bash/bin/sh — and retries the next candidate on a retryable failure (posix_spawnp failed, ENOENT, not found). Genuine errors (EINVAL, out of fds) surface immediately and are never masked.

When the preferred shell is skipped and a fallback launches the terminal, the session reports preferredShellSkipped: true + resolvedShell, and the renderer shows a one-time toast: "Used /bin/zsh because $SHELL was unavailable."

Ported from t3code's trySpawn / isRetryableShellSpawnError (Manager.ts:567,1830).

2. Per-workspace persisted + sanitized history

Terminal output now survives close/reopen and app restart:

  • New terminal-history.ts store — one debounced (40ms), line-capped (5000) log per workspace under <userData>/terminal-history/. File names are sha256(workspaceId) so worktree paths with separators are safe.
  • Control-sequence sanitization — device-query/reply traffic is stripped before persisting and before the live snapshot is replayed, so a reopened terminal doesn't make the shell answer stale cursor-position/device-attribute queries and echo junk at the prompt. Stripped: CSI CPR (…R), DSR (…n), DA (…c), DECRQM/PM ($p/$y), XTVERSION (>q), Kitty keyboard (?u), DCS DECRQSS/XTGETTCAP, OSC color queries. Benign SGR/cursor sequences survive. This also fixes a latent bug in the existing snapshot() reconnect flow, which replayed raw (un-sanitized) output.
  • Seed-on-opencreate() reads the prior history and seeds the session buffer; the renderer's existing xterm-hydrate-from-snapshot shows it with no renderer change needed.

Ported from t3code's sanitizeTerminalHistoryChunk (Manager.ts:953).

Files

  • main/services/terminal-history.tsnew store + sanitizer
  • main/services/terminal-history.test.tsnew (12 tests)
  • main/services/terminal.ts — retry loop, history wiring, resolvedShell/preferredShellSkipped
  • main/services/terminal.test.ts — 5 new tests (retry, non-retryable, all-fail, happy path, history seeding)
  • renderer/lib/ipc.tsTerminalSession gains two fields
  • renderer/components/terminal-drawer.tsx — fallback toast
  • package.json — register both test files in test + test:coverage (they weren't in CI before)

Verification

  • tsc --noEmit clean
  • npm run lint clean
  • 15/15 terminal tests + 12/12 history tests (27 total, all green)

Scope (deliberately out of scope)

No architecture change — Aiden's PTY lives in the main process so t3code's server/contract model doesn't map; we adapt the logic, not the architecture. In-place restart, subprocess-aware labels, clickable links, and a reconnect stream are the medium-effort items from the comparison and are natural follow-ups.

…ation

Adds TerminalHistoryStore: a debounced, line-capped, per-workspace log of
terminal output rooted at <userData>/terminal-history. Output is sanitized
before persisting so a replayed snapshot cannot trigger fresh shell replies
— CSI cursor-position reports, device-attributes/status queries, DECRQM/PM,
XTVERSION, Kitty keyboard, DCS DECRQSS/XTGETTCAP, and OSC color queries are
stripped while benign SGR/cursor sequences survive. Partial sequences split
across chunks are carried via a pending prefix.

Ported from t3code's sanitizeTerminalHistoryChunk (Manager.ts:953).
Shell fallback: the spawn path now walks an executable candidate list
($SHELL -> /bin/zsh -> /bin/bash -> /bin/sh) and retries the next on a
retryable failure (posix_spawnp failed, ENOENT, not found). A broken $SHELL
self-heals instead of throwing. Non-retryable errors (EINVAL, out of fds)
surface immediately. The session result gains resolvedShell and
preferredShellSkipped so the renderer can tell the user which shell launched.

History wiring: TerminalService now accepts an optional historyStore. On open
the prior sanitized output seeds the buffer (the renderer re-hydrates xterm
from snapshot, so no renderer change is needed for the seed); each PTY data
event appends to the store; terminate/exit flush the final chunk.
…hell refactor

The phase3 contract test asserts the terminal.ts source orders revalidate →
abort-check → spawn → abort-check. The spawn call changed shape (single
spawn → trySpawnShell destructure) in the shell-fallback PR; update the
assertion to match while preserving the ordering invariant it protects.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — 7 commits across the shell-fallback retry loop, persisted sanitized history store, IPC contract additions, renderer toast, and test registration.

  • Shell-candidate retry looptrySpawnShell walks $SHELL/bin/zsh/bin/bash/bin/sh, retries on retryable spawn errors, and surfaces non-retryable errors without masking. isRetryableShellSpawnError walks the full error + cause chain with cycle detection. The pre-filter via isExecutable avoids wasting spawn attempts on obviously-missing binaries.
  • Persisted sanitized historyTerminalHistoryStore provides debounced per-workspace file persistence under <userData>/terminal-history/ with SHA-256 hashed filenames. sanitizeTerminalHistoryChunk strips device-query/reply sequences (CSI CPR/DSR/DA/DECRQM/XTVERSION/Kitty keyboard, DCS DECRQSS/XTGETTCAP, OSC color queries) while preserving benign SGR/cursor sequences, with chunk-boundary carry for incomplete sequences.
  • Buffer seeding on opencreate() seeds the session buffer from the history store so xterm rehydrates with prior output via the existing snapshot path. The sequence starts at 1 for restored sessions so the renderer's lastSequence guard correctly treats the seed as already-consumed.
  • Renderer toast — a one-time toast.info when preferredShellSkipped is true, telling the user which fallback shell was used.
  • Test coverage — 17 new tests (12 for the history store + 5 for the retry loop), all with tight exact-value assertions. Both test files registered in npm test and npm run test:coverage.

The design decisions are sound: the TerminalHistoryStoreLike interface decouples the service from the filesystem for testability, persist is best-effort (a terminal must never block on disk trouble), and pending half-sequences are intentionally not persisted (incomplete control sequences have no visual value).

Pullfrog  | View workflow run | Using DeepSeek Pro𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — 1 new commit since the prior review at 15ad2d2.

  • Source-reading test assertion updatesubagent-phase3-contract.test.ts:563 now searches for "const { pty," instead of "const pty = (this.options.spawnPty ?? spawn)(", reflecting the create() refactor that destructures trySpawnShell's return value. The ordering assertion (abort check → spawn → post-spawn check) is preserved.

Pullfrog  | View workflow run | Using DeepSeek Pro𝕏

@sambitcreate
sambitcreate merged commit 5718840 into fix/terminal-posix-spawnp-mac Aug 11, 2026
2 checks passed
@sambitcreate
sambitcreate deleted the fix/terminal-shell-fallback-history branch August 12, 2026 04:49
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.

1 participant