diff --git a/.github/audit/application-security.md b/.github/audit/application-security.md index bc137e89..71a69abc 100644 --- a/.github/audit/application-security.md +++ b/.github/audit/application-security.md @@ -46,10 +46,14 @@ today's tree, not the scope. For the rest of `docs/specs/security-local.md`, read each section's owner first — `docs/specs/terminal-escapes.md`, `docs/specs/dor-browser.md`, `docs/specs/dor-cli.md`, `docs/specs/vscode.md` -> "Webview message -authentication", `docs/specs/standalone.md` -> "Persistence" — then the parser, -the iframe shim, the control-socket code, and the persistence path they point -at. The attacker there is a program printing to the terminal, a page in a -browser pane, or another local account, never the network. +authentication", `docs/specs/standalone.md` -> "Persistence", +`docs/specs/notepad.md` -> "Archive" — then the parser, the iframe shim, the +control-socket code, and the persistence paths they point at. `## Persisted +state` now covers two stores that hold user text on purpose: the session +snapshot and the notepad archive, both written through +`write_file_atomically` on standalone, the archive in `globalState` on VS Code. +The attacker there is a program printing to the terminal, a page in a browser +pane, or another local account, never the network. ## Qualitative pass diff --git a/AGENTS.md b/AGENTS.md index 6ef9066e..51f339e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,7 @@ A spec is the accurate reference for the current code: it states the invariants - **`docs/specs/terminal-escapes.md`** — Registry of every escape sequence parsed, answered, or ignored, each row pointing at its owning spec. Read before touching OSC/CSI parsing. - **`docs/specs/transport.md`** — Adapter-agnostic webview ↔ host protocol: PTY lifecycle and buffering, reconnection, message contracts, persisted-session types, the invariants every adapter honors. - **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned selection, copy (Raw / Rewrapped), paste tiers, smart URL/path extension, the mouse-ownership state matrix. +- **`docs/specs/notepad.md`** — The per-Surface notepad: the note model and host archive port, capture from a terminal selection, source pins back to scrollback, the panel/Door/Archive UI, and every closure path that archives notes. - **`docs/specs/theme.md`** — The two-layer CSS variable strategy, consumed-token resolver, terminal color contract, theme debugger. - **`docs/specs/dor-cli.md`** — The `dor` CLI on every Dormouse terminal's `PATH`: bundling and env contract, `spawnAndCapture` rules, control-socket plumbing, the Surface handle model, the command set. - **`docs/specs/dor-browser.md`** — The browser surface: `BrowserPanel` with swappable `renderMode`, browser chrome, the agent-browser stack, the iframe proxy and CSP boundaries. diff --git a/docs/specs/dor-cli.md b/docs/specs/dor-cli.md index 42b2cd8e..8590e70a 100644 --- a/docs/specs/dor-cli.md +++ b/docs/specs/dor-cli.md @@ -229,9 +229,11 @@ late response for a reaped id is a silent no-op on the server. **Must cancel `ensure`'s polling when the client disconnects.** Cancellation before an interrupted command returns to its prompt prevents relaunch; -cancellation during initial integration detection removes the throwaway split. +cancellation during initial integration detection closes the temporary Surface per `docs/specs/notepad.md` → "Closure". `lib/src/components/Wall.test.tsx` pins both paths. +**Must exclude Surfaces with a Wall closure in progress from reuse.** An unrelated notes freeze permits reuse. + Source of truth: `standalone/sidecar/dor-control-server.js`, `dor/src/control-client.ts`, `dor/src/protocol.ts`, `peerDirIsSafe` in `vscode-ext/src/peer-link.ts`, `lib/src/lib/platform/dor-control-dispatch.ts`, @@ -353,7 +355,7 @@ The spec keeps the behavior help cannot express: | Command | Behavioral contract | |---|---| | `split` | **Only a bare split focuses the new Surface.** A `--` marker or command tail leaves the caller focused; pre-parse preserves the marker stricli discards. | -| `ensure` | **Must have a `--` command tail.** Matching uses the exact OSC 633 command plus resolved CWD; `cmd.exe` without integration fails immediately, other unintegrated shells time out after 8s and lose their throwaway split. `--restart` drives the live PTY in place, preserving layout and minimized/visible state, so it works on Doors too. | +| `ensure` | **Must have a `--` command tail.** Matching uses the exact OSC 633 command plus resolved CWD; `cmd.exe` without integration fails immediately, other unintegrated shells time out after 8s and close through the notepad coordinator. `--restart` drives the live PTY in place, preserving layout and minimized/visible state, so it works on Doors too. | | `send` | **Must select exactly one input mode.** Text then key is the only mixed order; duplicate flags require the explicit sequence form. | | `read` | Clean, ANSI-free rendered lines; line limits count rendered lines. | | `await` | **Must name `--until quiet\|exit`; never infer it.** Timeout 1–86400 whole seconds, default 600; `alert.md` owns wake semantics. | diff --git a/docs/specs/layout.md b/docs/specs/layout.md index c2685202..2f658996 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -48,7 +48,7 @@ Panes are separated by a 7px gap (`PANE_GUTTER_PX`), odd so the 1px selection ri A 30px header doubling as a drag handle: **a `pointerdown` past a 5px threshold begins a Lath pane drag**; below the threshold the header's own click behavior stands. It uses `cursor-grab` / `active:cursor-grabbing`, `select-none`, the shared terminal top radius from `lib/src/components/design.tsx`, and the `--color-header-active-*` / `--color-header-inactive-*` token pairs (VSCode file-tree list colors). -Elements left to right: derived label; alert bell; TODO pill (compact+); flexible gap; mouse-reporting override icon (compact+, only while the inside program requests mouse reporting); split left/right, split top/bottom, zoom/unzoom (full only); minimize; kill (hover turns error-red). +Elements left to right: derived label; alert bell; TODO pill (compact+); flexible gap; mouse-reporting override icon (compact+, only while the inside program requests mouse reporting); notepad icon (`docs/specs/notepad.md` → "Notepad UI"); split left/right, split top/bottom, zoom/unzoom (full only); minimize; kill (hover turns error-red). The label is the `DerivedHeader` from `deriveHeader(...)`; `docs/specs/terminal-state.md` owns the priority chain and disambiguator. Layout renders it: primary truncates with ellipsis, secondary muted beside it, a failed last command appends an error-colored glyph. Click renames/pins; right-click — or `>` in command mode — opens the header context menu. @@ -72,6 +72,8 @@ The label is the `DerivedHeader` from `deriveHeader(...)`; `docs/specs/terminal- **Must focus context controls on opening.** Explicit entry into helper xterm gives it terminal keys; Escape there belongs to its program. Escape from controls closes the innermost disclosure, then context. Terminal clipboard routing uses the focused helper rather than the selected source. Actions use subdued link color and shared compact `OnOffSwitch` controls. +**Must place the shared notepad button beside Promote in the Helper status row**, opening the parent's panel over the context; `docs/specs/notepad.md` → "Helper terminals" owns its behavior. + **Must tint the copyable Surface ref as an action and confirm each successful context copy in its button** with a checkmark and “Copied” for 1.4 seconds, preserving button width and keeping the context open. Failed copies show the action error without success feedback. **Must suppress context action hover and focus highlights while the window is unfocused**, including after opening a native explorer or system browser. **Must also withhold hover from an in-flight action, which stays focusable and `aria-disabled` rather than `disabled`** so the innermost disclosure keeps a focused descendant for Escape and Tab. @@ -103,7 +105,7 @@ A ResizeObserver picks one of three tiers by header width: - **Full** (>280px): everything. - **Compact** (>160px): split, zoom, and unzoom hidden. -- **Minimal** (≤160px): also hides the TODO pill and the mouse-override icon, leaving alert, minimize, and kill. The label truncates with ellipsis. +- **Minimal** (≤160px): also hides the TODO pill and the mouse-override icon, leaving alert, minimize, and kill. **The notepad icon survives this tier only while the Surface has notes** (`docs/specs/notepad.md` → "Notepad UI"). The label truncates with ellipsis. ## Baseboard @@ -121,6 +123,7 @@ A minimized session becomes a **door**, showing its label plus the alert/TODO/sp - **m** / **d** (command mode): restore into a pane but stay in command mode — the inverse of `m`/`d` on a pane, making them toggles. - **x** / **k** (command mode): restore into a pane, then show the kill confirmation (an untouched Surface is killed outright — [Kill confirmation](#kill-confirmation)). - **Arrow keys** navigate to and between doors ([Spatial navigation](#spatial-navigation)). +- **A Door holding notes carries a second button**, the notepad, which neither reattaches nor drags (`docs/specs/notepad.md` → "Notepad UI"). **A reattach that stays in command mode defers its follow-up** (focus, kill, replace) to `requestAnimationFrame` and skips it if the pane vanished in between. @@ -179,6 +182,8 @@ All keys are handled in one capture-phase `keydown` listener on `window` (`use-w That order is load-bearing twice: a rename input suppresses the pane shortcuts but **not** the mode-exit gesture or the field's own clipboard chords; and a staged kill confirmation hijacks each key reaching it before the dialog gate, so the confirm letter works even though the modal is open. +**Every open dialog holds its own reference-counted lease on that gate**, and command-mode dispatch resumes only once the last lease is released — so a dialog closing over another cannot lift the survivor's suppression (`createDialogKeyboardCoordinator` in `lib/src/components/wall/wall-context.tsx`). + ### Split cwd inheritance A split from an existing pane (`|`/`%`/`-`/`"` or the header split buttons) spawns the new pane with its source pane's last-known cwd, then selects it and enters passthrough; host New Terminal actions share that focus tail (rationale). Focus-neutral control-plane creation (`dor split -- …`, `dor ensure`, `dor iframe`, `dor ab`) keeps its documented background behavior. @@ -191,9 +196,11 @@ The source cwd is read from `getTerminalPaneState(sourceId).cwd`. **Never inheri **Confirmation must be staged in a ref synchronously, not only in React state** — a second confirm keydown arriving before React flushes would otherwise pass the guard and kill twice (`lath.isDying` is the second line of defense). +**Every kill routes through the notepad close coordinator**, confirmed and untouched-fast-path alike, which archives the Surface's notes before teardown and can refuse the close (`docs/specs/notepad.md` → "Closure"; that spec also names who may still tear a Surface down immediately). + **Untouched sessions skip this confirmation.** A newly spawned shell starts `untouched: true`; the first user-originated PTY input flips it to false. Counted: printable keys, Enter, control keys, keyboard CSI such as arrows/history, paste, file-drop path insertion. Not counted: replay-shaped terminal reports and stripped mouse-report-only input — **the gate checks `inputIsReplayTerminalReport`**, the broader synthetic-report check gating input recording and alert attention, not this flag. Killing an untouched pane runs the normal kill animation/dispose path immediately; killing an untouched door first reattaches it only far enough to reuse that removal path, then kills it with no overlay. -Source of truth: `acceptKill` in `lib/src/components/Wall.tsx`, `lib/src/components/KillConfirm.tsx`. +Source of truth: `requestKill` (every kill gesture: Door reattach, untouched fast path, or staging the overlay) and `acceptKill` in `lib/src/components/Wall.tsx`, `lib/src/components/KillConfirm.tsx`. ## Selection overlay @@ -303,7 +310,7 @@ Source of truth: `lib/src/components/wall/IllegalRenameWarning.tsx`, `lib/src/co | **Swap** | `Cmd/Ctrl+Arrow` trades two leaf identities via a Lath `swap`; registry entries follow the ids ([Spatial navigation](#spatial-navigation)). | - **Untouched**: new `getOrCreateTerminal` sessions start untouched; `isUntouched(id)` exposes the flag, user-originated PTY input clears it, and resume/restore seed the persisted one. **Missing legacy snapshot data defaults to touched (`false`)**, keeping close confirmation conservative. -- **Shell selection replacement**: the standalone Settings dialog's Shell row and the VS Code shell picker send `dormouse:new-terminal` with `replaceUntouched` when the selected shell type changes. **A shell is identified by executable path plus ordered arguments**, so WSL distributions and Windows Developer shells sharing an executable stay distinct. **`Wall` always mints a new session id and a fresh `surface:N` ref.** An untouched selected pane or door has the new terminal take over its leaf via a Lath `replace` op (an atomic identity swap; doors reattach through the normal restore path first), the old session disposed and its ref retired; a touched selection, or none, spawns a new pane beside it. Announced spawns show a transient pane-anchored notice (`Switched to zsh`, `Opened bash`). +- **Shell selection replacement**: the standalone Settings dialog's Shell row and the VS Code shell picker send `dormouse:new-terminal` with `replaceUntouched` when the selected shell type changes. **A shell is identified by executable path plus ordered arguments**, so WSL distributions and Windows Developer shells sharing an executable stay distinct. **`Wall` always mints a new session id and a fresh `surface:N` ref.** An untouched selected pane or door has the new terminal take over its leaf via a Lath `replace` op (an atomic identity swap; doors reattach through the normal restore path first), the old session disposed and its ref retired; a touched selection, or none, spawns a new pane beside it. Announced spawns show a transient pane-anchored notice (`Switched to zsh`, `Opened bash`). **A replacement migrates the Surface's notepad to the new id rather than archiving it** (`docs/specs/notepad.md` → "Closure"). - **Replay-time terminal reports must be dropped; user input must not be** — during **resume** replay the registry drops the replies xterm.js emits to queries embedded in buffered output, before they reach the retained PTY (`docs/specs/terminal-escapes.md` → "Report filtering on the input side"). Source of truth: `lib/src/lib/terminal-store.ts` (registry maps and pending shell opts, imported directly, including by `lib/src/remote/burrow/`), `lib/src/lib/terminal-lifecycle.ts` (the ops), `lib/src/lib/terminal-registry.ts` (the facade). @@ -398,7 +405,7 @@ A store commit that empties the tree (last pane killed or minimized) triggers th 3. **Stable hitboxes across moves**: a leaf measured after a move reports its new rect ([Position tracking](#position-tracking)), and Lath never re-parents a leaf div, so its node identity — and any embedded `