Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ The icon-and-tooltip button used inside pane headers (kill, alert toggle, todo,
- **Hover:** `hover:bg-current/10` — a 10%-opacity wash of the current text color. Theme-agnostic, works light or dark.
- **Tooltip:** rendered through a portal as a `PopupButtonRow` 8px below the button, with `text-sm` primary line and an optional muted detail line. Keybindings inside the tooltip auto-render as `[bracketed]` shortcuts.

#### Popup Button (`popupButton`)
The flat segments inside a `PopupButtonRow` — the row owns the border, background, shadow, and `text-sm`, so a segment contributes only padding and tone.
- **Tones:** `foreground` (default), `muted` (`text-muted`, hover to full foreground), and `primary` — `bg-header-active-bg` + `text-header-active-fg`, for the one emphasized action in rows that ask rather than offer (the restored pane's resume offer).
- **Hover:** the default wash is `hover:bg-foreground/10`. A filled tone holds its fill instead — the wash sets *background*, so on `primary` it would replace the accent rather than sit over it. This matches the modal primary button, which likewise has no hover treatment.
- **Flash:** `flashed` swaps in `animate-copy-flash` with `bg-header-active-bg/25` for copy-confirm moments.

#### Chrome Button (window controls)
The Windows/Linux native-style window control row in the standalone app bar.
- **Variants:** `icon` (h-5 min-w-5, hover bg-current/10), `labeled` (h-5 min-w-5 px-1.5 text-xs), `window` (w-11, hover bg-current/10), `windowClose` (w-11, hover bg `#b92a1b` text-white).
Expand Down
19 changes: 18 additions & 1 deletion docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,28 @@ For a terminal Surface the pane ID is its session ID. `TerminalPane` calls `getO
- **Create**: `getOrCreateTerminal` spawns xterm.js + UnicodeGraphemesAddon + FitAddon + PTY, returns existing if already created. The xterm instance sets `allowProposedApi: true` because UnicodeGraphemesAddon activates through xterm's proposed Unicode API. The WebGL addon is *not* loaded at create — it is claimed lazily on the session's first mount (see "Renderer" below).
- **Resume**: `resumeTerminal` creates xterm entry and writes replay data without spawning a new PTY. Used when the webview is recreated while the host retains Live PTYs (Link: Severed → Resuming → Live).
- **Restore**: `restoreTerminal` creates xterm entry and spawns a new PTY with saved cwd and scrollback. Used on cold start from a saved Snapshot (Link: Cold → Live).
- **Resume offer**: a restored pane whose snapshot carried a `resumeCommand` (`docs/specs/transport.md`) offers to run it — the replayed scrollback ends in an agent's resume hint, but the process that wrote it is gone and the pane now holds a fresh shell. See "Resume offer" below.
- **Untouched**: new `getOrCreateTerminal` sessions start untouched. `isUntouched(id)` exposes the flag, and user-originated PTY input clears it via the registry input paths. Resume/restore seed the persisted flag; missing legacy snapshot data defaults to touched (`false`) so close confirmation remains conservative.
- **Shell selection replacement**: the standalone shell dropdown and VS Code shell picker send `dormouse:new-terminal` with `replaceUntouched` when the selected shell type changes. `Wall` always creates a new session id and a fresh `surface:N` ref for that request. If the currently selected pane or door is untouched, the new terminal takes over the same leaf via a Lath `replace` op (an atomic identity swap; doors first reattach through the normal restore path), the old untouched session is disposed, and the replaced Surface's ref is retired. If the selected terminal is touched or no terminal is selected, the request spawns a new pane beside the selected one. Announced shell-selection spawns show a transient pane-anchored notice such as `Switched to zsh` or `Opened bash`.
- During resume/restore replay, xterm.js may emit terminal-generated replies for OSC/CSI/DCS queries that were embedded in saved output. The registry drops those replay-time replies before they reach the new shell. This filter is limited to query/focus reports, and must not swallow user keyboard escape sequences such as arrows, function keys, or bracketed paste.
- **mount / unmount (DOM)**: `mountElement` reparents the persistent DOM element into a container; `unmountElement` removes it. The Registry entry survives.
- **Dispose**: `disposeSession` kills the PTY, disposes xterm, removes the registry entry. Only called on explicit kill (`x`).
- **Swap**: the Cmd/Ctrl+Arrow swap trades two leaf identities via a Lath `swap` op — per-leaf metadata and registry entries are keyed by id, so they follow the swap with no DOM reattach or title swap (see "Cmd/Ctrl+Arrow swap" above).

### Resume offer

A cold **restore** replays a Session's saved scrollback into a *fresh* shell. When that scrollback ended in an agent's resume hint, the snapshot carries the command as `PersistedPane.resumeCommand` (`docs/specs/transport.md`), and the restored pane offers to run it: two buttons at the pane's bottom-right, `Run <invocation>` and `Dismiss`.

- **Seeded by restore only.** `restoreSession` seeds the offer per terminal pane; **resume** never does, because there the process is still Live and has nothing to resume. Browser surfaces are skipped with the rest of the terminal restore path.
- **Retired** by taking it, dismissing it, the user's first input into the pane, the pane's process exiting, or session dispose. Input counts from every path that reaches the PTY: keyboard input through xterm, direct clipboard paste, file-drop path insertion, a Pocket client's remote keystrokes (`remote-api.ts` → `#write`), and `dor send`. Only the xterm path runs through `onData`; each direct platform write retires the offer itself. Replay-shaped terminal reports do not count. It does not survive into the next save — the offer lives only in the runtime store, and the next restore re-seeds from the snapshot.
- **Untouched is not the gate.** A Session that ran an agent is touched by definition, so `isUntouched` would suppress the offer in exactly the case it exists for. Retirement keys off *post-restore* input instead.
- **Hidden, not retired, while a command is running** (`activity.kind === 'running'`): the offer types into the shell, and a shell with a foreground process is not listening. Shells without OSC integration report `unknown` and keep the offer.
- **Taking it** selects the pane, enters passthrough, and uses the normal deferred pane-click focus transition. It revalidates the command, drops out if the Session has no registry entry or its process already exited (a dead shell can run nothing, and the seed below would be a command start nothing ever finishes — the pane would count as running forever in `countRunningSessions`), marks the Session touched, seeds `commandLine` + `commandStart(user_input)` so semantic state is already `running`, then writes `<command>\r` straight to the PTY. The explicit seed is required because the platform write bypasses xterm's keystroke fallback; an OSC-integrated shell may replace it with authoritative boundaries afterward. Not a bracketed paste — bracketing exists to stop an embedded newline from executing, which is the opposite of the intent. A click landing before the fresh shell has drawn its first prompt can still be swallowed by shell startup, the same hazard `typeCommandWhenPromptReady` guards for launched commands; the offer accepts it rather than delaying the button, since restore-then-click is far slower than spawn-then-type.
- **The button carries the invocation, not the command.** `Run claude --resume`, never `Run claude --resume <uuid>` — the session id is already on screen in the replayed scrollback directly above. The full command is the button's tooltip.
- **Chrome is the pane-overlay vocabulary**, not the modal one: a `PopupButtonRow` owning the border/background/shadow, with `popupButton` segments inside — the same recipe as `MouseOverrideBanner` in the opposite corner. Run takes the row's `primary` tone (`DESIGN.md` → Popup Button); Dismiss is `muted`.

Source of truth: `lib/src/lib/resume-offers.ts` (store), `runResumeCommand` in `lib/src/lib/terminal-lifecycle.ts`, `lib/src/components/wall/ResumeBanner.tsx` (the pane-mounted offer + its presentational `ResumeBannerView`), seeded in `lib/src/lib/session-restore.ts`.

### Renderer

Every terminal renders through stock `@xterm/addon-webgl`, claimed lazily by
Expand Down Expand Up @@ -505,7 +520,9 @@ The refill adopts the replacement (`selectPane`) only when the current selection
| `lib/src/lib/session-save.ts` | Serialization: collects layout, scrollback, cwd, alert state for persistence |
| `lib/src/lib/session-restore.ts` | Deserialization: loads saved session, calls `restoreTerminal()` for each pane |
| `lib/src/lib/reconnect.ts` | Priority-based recovery: live PTYs first, then saved session, then empty |
| `lib/src/lib/resume-patterns.ts` | Detects resumable commands (`claude --resume`, etc.) in scrollback |
| `lib/src/lib/resume-patterns.ts` | Detects resumable commands (`claude --resume`, etc.) in scrollback, newest line first, and labels them with the invocation alone |
| `lib/src/lib/resume-offers.ts` | Pending resume offers per Session — seeded by cold restore, retired on take/dismiss/input/dispose |
| `lib/src/components/wall/ResumeBanner.tsx` | The restored pane's `Run <invocation>` / `Dismiss` offer |
| `lib/src/index.css` | Lath host styling — `.lath-host` / `.lath-leaf` / `.lath-sash` / drop-preview layout and background flattening |
| `lib/src/theme.css` | Two-layer VSCode theme token system (`@theme --color-*` → `--vscode-*`) and Tailwind v4 `@theme` integration |

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/mouse-and-clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ Only the single extension step described above is offered. There is no multi-lev

### 8.1 Overview

Paste reads the system clipboard and writes the content to the PTY. Paste keystrokes are **intercepted by the terminal**, not forwarded to the inside program. The inside program only receives the pasted bytes (optionally wrapped in bracketed-paste markers; see §8.5).
Paste reads the system clipboard and writes the content to the PTY. Paste keystrokes are **intercepted by the terminal**, not forwarded to the inside program. The inside program only receives the pasted bytes (optionally wrapped in bracketed-paste markers; see §8.5). A non-empty clipboard or file-path paste counts as user input: before the direct PTY write it marks the Session touched and retires any cold-restore resume offer (`docs/specs/layout.md`).

Paste behavior differs by platform to match each OS's native convention.

Expand Down
4 changes: 4 additions & 0 deletions docs/specs/terminal-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ Non-OSC title source:

The `user_input` command fallback is best effort and renderer-only: it is sufficient for headers and grouping, but the `AlertManager` never sees it. Command-exit alerting and command-keyed WATCHING both need real shell integration, and `docs/specs/alert.md` records that as a deliberate limitation rather than plumbing the heuristic into a second command-tracking path.

Programmatic interactive launches that write directly to the platform PTY bypass xterm's keystroke fallback. They therefore emit `commandLine` + `commandStart(source: "user_input")` synchronously before the write: both `dor split/ensure -- <command>` and a cold-restore resume offer use `seedLaunchedCommand`. This keeps headers, grouping, and `countRunningSessions` correct on shells without OSC integration; an integrated shell's later boundaries remain authoritative. Source of truth: `seedLaunchedCommand` and its callers in `lib/src/lib/terminal-state-store.ts` and `lib/src/lib/terminal-lifecycle.ts`.

The parser accepts both BEL and ST terminators and handles split chunks. Supported-but-malformed semantic OSCs are consumed without changing state. Unsupported OSC pass-through vs. consume/ignore behavior is defined centrally in `docs/specs/terminal-escapes.md`.

## Reducer
Expand Down Expand Up @@ -202,6 +204,7 @@ first authentic OSC boundary ──▶ pane promoted to OSC-driven; fallback ret
- **Shape survival and reconnect seeding.** The prompt shape survives across commands (it does not reset on `promptStart`/`promptEnd`/`commandStart`) and is pre-seeded from restored scrollback on session restore / VS Code panel reopen, so the first command after a reconnect — when the live shell will not re-emit its prompt — is still titled. Seeding is learn-only and fires no prompt transition.
- **Swap safety.** The fallback resolves the current Session id from the PTY id before recording submit input or prompt-looking output, so drag-to-swap moves the fallback state — including the learned prompt shape — with the visible pane.
- **Synthesized idle transitions.** Visible output that looks like a returned shell prompt always refreshes the learned prompt shape, but only synthesizes the idle prompt transition when `currentCommand.source === "user_input"`. This keeps shape learning available for all shells while scoping the finish/start synthesis to shells that do not emit command finish/start OSCs (OSC-tracked shells drive their own boundaries).
- **Control stripping.** Prompt-looking output is judged over the last 1024 chars of a pane's output, with alt-screen spans dropped and terminal presentation controls removed by the shared `stripTerminalControls` (`lib/src/lib/terminal-controls.ts`, also used by resume-hint detection — `docs/specs/transport.md`). That tail cuts mid-sequence routinely, so an *unterminated* string control swallows the rest of the buffer instead of surrendering its payload: a half-arrived title OSC carrying the prompt string would otherwise land as the last visible line and flip a running command back to idle.
- **Per-pane retirement.** The keystroke fallback and real OSC 633/133 integration are mutually exclusive per pane. The first authentic OSC boundary a pane emits (`promptStart`/`promptEnd`/`commandFinish` always, or a `commandStart` whose source is an OSC boundary — not `user_input`) promotes the pane to **OSC-driven**, after which the keystroke path stops recording: `recordTerminalUserInput` early-returns and no further `user_input` `commandStart`/`commandLine` is synthesized, so injected shells never double-count. The synthesized prompt markers the fallback itself emits are passed with a `keystrokeHeuristic` flag so they do **not** trigger promotion — otherwise the fallback would retire the very path that emits them. The flag is per-pane runtime state, seeded fresh and cleared on pane reset/removal; it is not persisted.

### CWD precedence
Expand Down Expand Up @@ -265,4 +268,5 @@ Windows UNC display labels keep `\\server\share\` as the path root and do not re
|------|------|
| `lib/src/lib/terminal-state.ts` | Pure semantic model: types, reducer, CWD precedence, header derivation, grouping |
| `lib/src/lib/terminal-state-store.ts` | React-facing store; PTY-id → pane-id resolution; keystroke fallback recording (`recordTerminalUserInput`) |
| `lib/src/lib/terminal-controls.ts` | `stripTerminalControls` — shared presentation-control stripping for consumers that read raw output as content |
| `lib/src/lib/terminal-protocol.ts` | Semantic OSC parsing that emits `TerminalSemanticEvent` (parsing location rules in `docs/specs/terminal-escapes.md`) |
Loading