Skip to content

Live sessions: user-controlled startup restore - #198

Merged
raiseCatError merged 3 commits into
devfrom
feature/197-startup-restore-policy
Sep 29, 2026
Merged

raiseCatError merged 3 commits into
devfrom
feature/197-startup-restore-policy

Conversation

@raiseCatError

@raiseCatError raiseCatError commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #197

Behavior

  • Settings (Config → Sessions, persisted in the existing config): liveSessionStartup = Ask (default) / Always / Never; liveSessionMultiple = Ask which (default) / Open all. Unknown values fall back to the defaults.
  • One detached session, Ask: a prompt shows the cwd, the running command (or idle) and its age.
    • R / Enter: resume.
    • N / Esc: start fresh and leave the session running.
    • A: save Always, then resume.
    • D: save Never, then start fresh, without killing anything.
  • Always: one detached session resumes without a prompt. With several, the multiple-session setting applies.
  • Never: no prompt or picker at launch. Sessions keep running and stay in /resume.
  • Several, Ask which: a picker with ↑↓, Space to toggle, A for all (again to clear), Enter to resume the selected sessions, and Esc (or Enter with none selected) to start fresh. Unselected sessions keep running.
  • Open all: every detached session is restored.
  • Neither screen has a destructive key. --new and --attach <id> are unchanged. Attached sessions are never candidates, since attach is exclusive at the service.

Opening more than one

This window attaches the first chosen session. Each other one runs nmsh --attach <id> in a new host window, via src/host/terminalHost.ts:

  • detectTerminalHost returns an optional newWindow(argv) launcher: Ghostty on macOS (AppleScript: new surface configuration + new window with configuration, a normal window in the running app, with the command words passed as osascript argv), Ghostty on Linux (ghostty -e), Terminal.app (AppleScript do script), and kitty (kitten @ launch --type=os-window).
  • VS Code, Zed and unknown hosts have none.
  • openWindows takes an injectable spawner and reports every command it could not open. The current window then names each remaining session's nmsh --attach command. Nothing selected is dropped or ended.

Also fixed

  • The launch notice was added before a reattached journal was restored, which erased it. That affected Live sessions: stale-session cleanup, crash recovery and hardening #131's recovery notice too. It is now added after the restore.
  • The startup screens treat a read ending in a lone ESC as Esc (Space then Esc can arrive as one read and used to hang).
  • A lifecycle test waited for any running command, so it could kill the frontend while the previous echo was still marked running. It now waits for the gated command.

Tests

  • tests/startupRestore.test.ts: config defaults and validation, the launch plan for every policy combination, prompt and picker keys and rendering, host detection and command construction (including quoting), openWindows with a fake spawner, and orchestration (persisting, not-now, Never, selection, Open all, unsupported and failing hosts).
  • tests/startupDiscovery.test.ts (real PTY, nmshd and zsh):
    • Ask prompt, with Not now, Esc and Resume.
    • Never and Always persist, and Never ends nothing. /resume still lists the skipped session.
    • Picker selection and Esc, with the unselected sessions still detached.
    • Open all with the fallback notice.
    • No hijack of attached sessions.
  • The test sandbox pins a neutral host, so tests can never open real terminal windows.
  • Build, typecheck and git diff --check are clean. The combined live PTY files passed 10 consecutive runs, and a full-suite run passed 568/568.
  • Other full-suite runs had 1–2 timing failures, each in a different test. The machine's load average was ~29 from unrelated processes, and unmodified dev failed 2–4 tests per run under the same load.

Limitations

  • Ghostty on macOS needs its AppleScript support enabled (the default) and asks once for macOS Automation permission. If either is unavailable, the fallback message names the nmsh --attach command.
  • Terminal.app asks once for macOS Automation permission.
  • kitty needs allow_remote_control.
  • If an extra window's explicit attach fails, that window reports the error and exits. The session stays detached and listed in /resume.
  • Multi-window behavior is not verified automatically and needs physical QA.

Launch no longer reattaches silently. Two settings (Config → Sessions):
Startup restore (Ask by default, Always, Never) and Multiple detached
sessions (Ask which by default, Open all).

With one detached session and Ask, a prompt shows its directory, command
and age, and offers Resume (R/Enter), Not now (N/Esc), Always resume (A)
or Don't resume at startup (D); A and D save the setting. With several, a
picker selects which to resume (Space, A for all, Enter, Esc for none).
Neither has a destructive key, and Never only skips restoring at launch.

This window attaches the first chosen session; each other one opens in a
new host window through a small TerminalHost abstraction (Ghostty,
Terminal.app, kitty). Hosts that cannot open windows, or launcher
failures, leave those sessions running and name their nmsh --attach
command.

Also: show the launch notice after a reattached transcript is restored
(it was being erased), treat a read ending in a lone ESC as Esc in the
startup screens, and wait for the specific gated command in a lifecycle
test instead of any running command.
The next command was typed right after Ctrl+C, before the interrupted
job's prompt returned; the tty can flush input queued around an
interrupt, so the command was sometimes lost (seen in CI). Wait until the
sleep has exited and NMSh has recorded the interruption.
Replace open -na Ghostty.app, which started a separate Ghostty instance
per window, with Ghostty's AppleScript API: a new surface configuration
whose command is the nmsh --attach line, then new window with that
configuration. The script is fixed text; the command words are passed as
osascript argv and shell-quoted with quoted form of, so no path or
session id is interpolated into AppleScript. A disabled scripting
interface, denied Automation permission or a Ghostty error exits
non-zero, which keeps the existing fallback: the session stays detached
and its attach command is named.
@raiseCatError
raiseCatError merged commit 10fc2da into dev Sep 29, 2026
2 checks passed
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