Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2652d30
Notepad: shared model, archive logic, and adapter port
nedtwigg Sep 4, 2026
34126a8
Notepad: live-note store, archive service, and the three host ports
nedtwigg Sep 4, 2026
fc4f42f
Notepad: fix the capture and pin entry-point signatures
nedtwigg Sep 4, 2026
2d0945a
Notepad: header icon, panel, Door popover, capture, and the Archive view
nedtwigg Sep 4, 2026
0beb455
Notepad: close coordinator, quit gate, live-resume hydration, and the…
nedtwigg Sep 5, 2026
61dbd29
Notepad: fold the parallel implementations onto shared pieces
nedtwigg Sep 5, 2026
9acc9ac
Notepad: fix the review findings
nedtwigg Sep 5, 2026
f1b6553
Notepad: drop the planning document from the tree
nedtwigg Sep 5, 2026
e921570
Notepad: reject unknown fields in the stored archive
nedtwigg Sep 5, 2026
b7ef741
Notepad: freeze a Surface's notes while its closure is writing
nedtwigg Sep 5, 2026
33dee6c
Notepad: replace the batch when a closure is retried
nedtwigg Sep 5, 2026
0f8dcba
Wall: reference-count the dialog keyboard lease
nedtwigg Sep 5, 2026
2f23eb0
Notepad: lock the standalone archive across processes and hash its re…
nedtwigg Sep 5, 2026
07672f1
Notepad: delete a landed batch when its retried closure is empty
nedtwigg Sep 5, 2026
c61585f
Notepad: refresh the process CWD before a Surface's batch is built
nedtwigg Sep 5, 2026
ce98bb3
Notepad: refresh the process CWD from the VS Code mirror before its t…
nedtwigg Sep 5, 2026
c136e54
Notepad: keep a pin through the alternate buffer
nedtwigg Sep 5, 2026
06c6994
Notepad: give a revealed selection its baseline
nedtwigg Sep 5, 2026
21a3f46
Notepad: simplify the fix series
nedtwigg Sep 5, 2026
dd81a9b
Refresh the notepad archive whenever its view reopens
nedtwigg Sep 5, 2026
dc4f3fa
Revalidate the VS Code archive before recovering unreadable data
nedtwigg Sep 5, 2026
78cb480
Serialize archive transactions across VS Code extension hosts
nedtwigg Sep 5, 2026
7263482
Carry pending archive batches through volatile mirrors and teardown
nedtwigg Sep 5, 2026
b827dbc
Reserve closing PTYs until deferred teardown kills them
nedtwigg Sep 5, 2026
cb53e7a
Restore mirrored notes when browser-only views resume
nedtwigg Sep 5, 2026
5e51461
Merge main and adapt notepads to current terminal identity and Burrow…
nedtwigg Sep 5, 2026
1f08b21
Share extension test aliases with the webview smoke test
nedtwigg Sep 5, 2026
8aa477c
Include pending empty notepads in standalone quit and handle CWD look…
nedtwigg Sep 5, 2026
b02f967
Clarify that the quit archive gate includes pending batches
nedtwigg Sep 5, 2026
ea668ec
Merge new-right-click-a and share parent notepads with helpers
nedtwigg Sep 5, 2026
5f093c4
Merge origin/main into notepad-2
nedtwigg Sep 6, 2026
1d0987e
Restore post-merge refinements and apply review fixes
nedtwigg Sep 6, 2026
398ff0f
Preserve notes during ensure cleanup and separate closure from archiv…
nedtwigg Sep 6, 2026
41204af
Correct ensure cleanup reference in layout spec
nedtwigg Sep 6, 2026
06366f1
Merge origin/main into notepad-2
nedtwigg Sep 6, 2026
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
12 changes: 8 additions & 4 deletions .github/audit/application-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 4 additions & 2 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down Expand Up @@ -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. |
Expand Down
17 changes: 12 additions & 5 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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.

Expand Down Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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 `<iframe>` — survives every op.
4. **Asymmetric back-navigation**: the breadcrumb ([Spatial navigation](#spatial-navigation)) makes every arrow move reversible even where no spatial query would return you.
5. **Door keeps selection through the auto-spawn refill** ([Auto-spawn refill](#auto-spawn-refill)). Explicit user selection of a pane — a click, a drag, or an embed focusing itself — still moves selection off a door.
6. **Focus-neutral surface creation (`dor ensure` / `dor iframe` / `dor ab`)**: unlike `dor split`, these open in the background without moving focus off the caller (`docs/specs/dor-cli.md`, `docs/specs/dor-browser.md`). An add never re-parents the caller's subtree or steals activation, and the create does not call `selectPane` (`settleAddSelection` returns false for a focus-neutral, non-selection-replacing add). **The one exception**: `dor iframe` / `dor ab` replacing the pane the user is *currently selected on* moves selection to the replacement, else it would dangle on the removed leaf; any other pane, or a door selection, is left untouched. A throwaway that never reports OSC 633 integration is torn down with `killPaneImmediately`, whose live selection check leaves the caller's selection intact (a `--minimize` throwaway is already a door, disposed directly).
6. **Focus-neutral surface creation (`dor ensure` / `dor iframe` / `dor ab`)**: unlike `dor split`, these open in the background without moving focus off the caller (`docs/specs/dor-cli.md`, `docs/specs/dor-browser.md`). An add never re-parents the caller's subtree or steals activation, and the create does not call `selectPane` (`settleAddSelection` returns false for a focus-neutral, non-selection-replacing add). **The one exception**: `dor iframe` / `dor ab` replacing the pane the user is *currently selected on* moves selection to the replacement, else it would dangle on the removed leaf; any other pane, or a door selection, is left untouched. Cleanup of a `dor ensure` temporary Surface follows `docs/specs/notepad.md` → "Closure"; any completed teardown preserves the caller's live selection.

## Future

Expand Down
4 changes: 4 additions & 0 deletions docs/specs/mouse-and-clipboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ A finalized selection gets a popup of action buttons adjacent to it, on the side

Source of truth: `lib/src/components/SelectionPopup.tsx` (Copy Raw, Copy Rewrapped, platform-dependent shortcut labels).

A third button, **Add to notepad**, follows the two copies on every host that has a notepad; it captures the selection, flashes in place, and dismisses the popup without opening the notepad (`docs/specs/notepad.md` → "Capture").

#### 4.1.1 Copy Raw

**Must preserve displayed row breaks and decorative characters**, trimming trailing whitespace on each selected row; soft-wrapped rows also get `\n`. Source of truth: `extractSelectionText` in `lib/src/lib/selection-text.ts`.
Expand All @@ -132,6 +134,8 @@ Copies with two transformations applied (`lib/src/lib/rewrap.ts`):

With an active, finalized terminal selection, popup focused or not: **Cmd+C** (Ctrl+C on non-macOS) triggers Copy Raw, **Cmd+Shift+C** (Ctrl+Shift+C) triggers Copy Rewrapped.

**Cmd+N** (Ctrl+N on non-macOS) adds the selection to the notepad, **gated exactly like Ctrl+C** — intercepted only with a finalized selection, otherwise reaching the program as readline's next-history — and shown only where the host binds it (`docs/specs/notepad.md` → "Notepad UI").

**Intercept Ctrl+C as Copy Raw only while a terminal selection is active.** With none it is forwarded to the inside program as usual (SIGINT for shells, app-defined for TUIs). An in-program selection a TUI maintains itself (vim visual mode, less search highlight) is **not** a terminal selection and does not change that routing.

### 4.3 Dismissing the Popup
Expand Down
Loading