Skip to content
Merged
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
91 changes: 66 additions & 25 deletions TESTING_AND_MODIFICATION_GUIDE.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@ Invariants:
**Must address unnumbered registry Workspaces as `workspace:<id>`, resolving
exact ids before names**, so legacy snapshots, duplicate names, and numeric
names cannot redirect a ref. `workspace:<name>` **resolves only when exactly one
Workspace carries that name**, else the error lists the candidates. Both are
accepted bare (`2`, `build`), and **a ref that reads as a number is a ref**,
Workspace carries that name**, else the error lists the candidates. All three are
accepted bare (`2`, `ws-a`, `build`), and **a ref that reads as a number is a ref**,
never a name. **A Window is `window:<label>` — its host's own name for it**
(`window:main`, `window:ws-2`), and a host with one Window answers
`window:1`; each accepts its own ref bare.
Expand Down Expand Up @@ -364,7 +364,7 @@ order.
**Port enumeration is opt-in.** With `includePorts` set (`dor list --ports` /
`--port`) the host scans each terminal Surface's process tree
(`docs/specs/dor-browser.md` → Dev-Server Chip), shelling out (`lsof` /
`Get-NetTCPConnection`) under `OPEN_PORT_TIMEOUT_MS`. **One listing costs one
`Get-NetTCPConnection`). **One listing costs one
scan where the adapter can batch it** (`PlatformAdapter.getOpenPortsMany`), and
`getOpenPorts(id)` per Surface in parallel where it cannot — so a listing
spanning Workspaces does not multiply a synchronous host scan by its row count.
Expand Down
21 changes: 18 additions & 3 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,27 @@ not a create: a Workspace that arrives mounts from the record it brought.
**Must confirm before a move that would destroy an iframe's page state**: a
plain iframe Surface's document cannot leave its webview, so it reopens at its
saved URL, and a Workspace holding one — Doored ones included — asks with the
kill's typed letter before it leaves; agent-browser Surfaces reconnect and ask
Close's typed confirmation before it leaves; agent-browser Surfaces reconnect and ask
nothing (`iframeSurfaceIds` on the Wall handle; `workspace-drag.test.ts`).

**Create** adds a Workspace named `Workspace N`, makes it active, and gives its Wall no restored record, so Lath's fresh branch spawns one default-shell pane. **Close** confirms first when the Workspace holds touched Surfaces or running work, reusing the kill-confirm letter and key rule over the Window's content area (**a bare `Shift` or `Meta` is not an answer**, as for a pane kill), then routes every member Surface through the closure coordinator; **the last remaining Workspace cannot be closed** — there is always one active Workspace, as there is always one visible pane (corner case #5). **One close runs at a time for the whole Window**, with the count re-checked after the confirmation, so two of them cannot empty two Walls between them; **a close the store then refuses hands the Wall back its auto-spawn** rather than leaving it mounted and empty. **A Workspace whose Wall has not registered is refused** (`workspace '<ref>' is still mounting`, one wording for every caller), never closed past — the Wall walks the member Surfaces, so dropping it would leave its Sessions running unheld (`docs/specs/glossary.md` → "Invariants" I4); **a gesture waits out the registration gap first**, as `dor workspace close` does, so `×` or `&` right after a create closes rather than silently doing nothing. **Rename** edits the Workspace `name` only — no Surface title, and not the per-pane inline rename. **Reorder** moves a tab in the strip and renumbers `workspace:<n>` refs with it only where they are positional (`docs/specs/dor-cli.md` → "Handle Model"); **a press inside the open rename editor never starts a reorder**. **Must drop only the closing Workspace’s rename editor and pending confirmation**, or a stale `renamingId` holds the chrome keyboard lease for the session (`WorkspaceStrip.test.tsx`). **Every Workspace verb runs outside the strip**, which renders the rename editor and confirmation from a store, so a tab gesture and a command-mode key take one path.
**Create** adds a Workspace named `Workspace N`, makes it active, and gives its Wall no restored record, so Lath's fresh branch spawns one default-shell pane. **Close** confirms first when the Workspace holds touched Surfaces or running work, with a kill-confirm letter over the Window's content area, then routes every member Surface through the closure coordinator; **the last remaining Workspace cannot be closed** — there is always one active Workspace, as there is always one visible pane (corner case #5). **One close runs at a time for the whole Window**, with the count re-checked after the confirmation, so two of them cannot empty two Walls between them; **a close the store then refuses hands the Wall back its auto-spawn** rather than leaving it mounted and empty. **A Workspace whose Wall has not registered is refused** (`workspace '<ref>' is still mounting`, one wording for every caller), never closed past — the Wall walks the member Surfaces, so dropping it would leave its Sessions running unheld (`docs/specs/glossary.md` → "Invariants" I4); **a gesture waits out the registration gap first**, as `dor workspace close` does, so `×` or `&` right after a create closes rather than silently doing nothing. **Rename** edits the Workspace `name` only — no Surface title, and not the per-pane inline rename. **Reorder** moves a tab in the strip and renumbers `workspace:<n>` refs with it only where they are positional (`docs/specs/dor-cli.md` → "Handle Model"); **a press inside the open rename editor never starts a reorder**. **Must drop the closing Workspace’s rename editor and pending confirmation, and no other’s** (`releases the rename lease when the tab being renamed is middle-clicked closed` in `lib/src/components/WorkspaceStrip.test.tsx`; `preserves another Workspace’s rename and close confirmation when closing a sibling` in `lib/src/components/wall/workspace-lifecycle.test.ts`). **Every Workspace verb runs outside the strip**, which renders the rename editor and confirmation from a store, so a tab gesture and a command-mode key take one path.

**Must use `WorkspaceKillConfirm` for Workspace close, the iframe move gate,
and host termination confirmations**, titled “Confirm kill workspace” except the
move gate: **a bare matching letter confirms, another bare key cancels, and a
modifier or chord never answers**, so `Cmd+Q` still quits. **Must ignore
its confirmation key while that Workspace transfers.** A successful transfer
dismisses only the departing Workspace's pending close, move, and rename UI;
a failed transfer retains them. No pending kill follows a Workspace to its
destination. Pinned by `does not accept a pending kill during transfer and releases its keyboard lease on departure`
in `lib/src/components/WorkspaceStrip.test.tsx` and `keeps the pending kill until commit, then dismisses only the departing Workspace`
in `lib/src/components/wall/workspace-transfer.test.ts`.
Source of truth: `dismissWorkspaceUi` in `lib/src/lib/workspace-ui-store.ts`;
`prepareWorkspaceTransfer` in `lib/src/components/wall/workspace-transfer.ts`.

The union projection and its indicators are owned by `docs/specs/alert.md` → Workspace union; the strip that renders them by `docs/specs/standalone.md` → AppBar. Persisted containers are owned by `docs/specs/transport.md`: standalone stores one `PersistedWindow` per window, so a relaunch restores every Workspace ([Session persistence](#session-persistence)).

Source of truth: `WorkspaceWindow` in `lib/src/components/WorkspaceWindow.tsx`; `registerWallHandle` in `lib/src/components/wall/wall-handles.ts`; `closeAll` in `lib/src/components/Wall.tsx`; `requestWorkspaceClose` in `lib/src/components/wall/workspace-lifecycle.ts`; `createWorkspace` / `closeWorkspace` / `renameWorkspace` / `moveWorkspace` / `setActiveWorkspace` in `lib/src/lib/workspace-store.ts`; `getWorkspaceUiSnapshot` in `lib/src/lib/workspace-ui-store.ts`; `setWorkspaceSurfaces` in `lib/src/lib/workspace-surfaces.ts`.
Source of truth: `WorkspaceWindow` in `lib/src/components/WorkspaceWindow.tsx`; `registerWallHandle` in `lib/src/components/wall/wall-handles.ts`; `closeAll` in `lib/src/components/Wall.tsx`; `requestWorkspaceClose` in `lib/src/components/wall/workspace-lifecycle.ts`; `WorkspaceKillConfirm` in `lib/src/components/WorkspaceKillConfirm.tsx`; `createWorkspace` / `closeWorkspace` / `renameWorkspace` / `moveWorkspace` / `setActiveWorkspace` in `lib/src/lib/workspace-store.ts`; `getWorkspaceUiSnapshot` in `lib/src/lib/workspace-ui-store.ts`; `setWorkspaceSurfaces` in `lib/src/lib/workspace-surfaces.ts`.

**Every Workspace verb has a `dor` counterpart** (`docs/specs/dor-cli.md` → "dor workspace"), taking the same route as the strip and the command-mode keys: a command close raises no confirmation, refusing instead, and closes its member Surfaces silently.

Expand Down Expand Up @@ -207,6 +220,8 @@ That order is load-bearing twice: a rename input suppresses the pane shortcuts b

**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`).

**Must defer Workspace close and move confirmations while an inline Workspace rename editor is open**, leaving its keys to the input; the pending gate appears after rename ends. Pinned by `defers the %s gate while another Workspace is being renamed` in `lib/src/components/WorkspaceStrip.test.tsx`.

**Chrome outside every Wall takes the chrome keyboard lease instead**: the Workspace strip's rename editor and close confirmation live in the app bar, where `stopPropagation` cannot reach a capture-phase window listener. **The Workspace branch is inert on a Wall with no Workspace id**, which is what leaves those keys unbound on a bare Wall. Source of truth: `acquireChromeKeyboardLease` in `lib/src/components/wall/chrome-keyboard-lease.ts`; `handleWorkspaceShortcuts` in `lib/src/components/wall/keyboard/handle-workspace-shortcuts.ts`.

### Split cwd inheritance
Expand Down
4 changes: 3 additions & 1 deletion docs/specs/notepad.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ A pin is the runtime link from a captured note back to the scrollback it came fr
- **While the alternate buffer is active a pin is temporarily unavailable and kept** — the markers belong to the normal buffer and resolve again once the program exits; the notepad says to exit it.
- **Every other pin failure removes the pin and keeps the note.** Disposed markers, rows out of range, and a text mismatch all report that the source is no longer available, the notepad kept or reopened to say so.
- **Disposing or replacing a terminal instance drops its pins immediately**, notes untouched — a marker belongs to one xterm instance.
- **Must drop source pins when a Workspace moves between windows**, keeping the
notes (rationale).
- **Pins never affect ordering and are not user-controlled favorites.**

Source of truth: `registerTerminalSource`, `resolveTerminalSource` and `revealResolvedSource` in `lib/src/lib/notepad/source-link.ts`; `revealNoteSource` in `lib/src/lib/notepad/pin.ts`; `setTerminalSelectionBaseline` in `lib/src/lib/terminal-store.ts`; `dropSourcesForTerminal` in `lib/src/lib/notepad/notepad-store.ts`, called from `disposeSession` in `lib/src/lib/terminal-lifecycle.ts`.
Expand Down Expand Up @@ -140,7 +142,7 @@ Source of truth: `archiveSurfaceNotes` in `lib/src/lib/notepad/close-coordinator

**Archiving is a gate step before teardown**: after the running-work confirmation, or immediately on an all-idle quit, and **before the first `quit_progress`** (`docs/specs/standalone.md` → "Quit flow"; rationale). **It is bounded at 3 s.**

**Both deliberate endings run the same gate**, over their own window's Surfaces: a quit, and closing one window of several (`docs/specs/standalone.md` → "Per-window close"). **Moving a Workspace to another window runs neither** — nothing ended, so the notes ride the move and the target hydrates them, minus their pins, which are markers in the xterm instances the source disposed. **Several windows archiving at once contend through the archive's own file lock and compare-and-swap retry** ([The archive port](#the-archive-port)), so a window whose write lost the race retries against fresh bytes rather than dropping the other window's batches.
**Both deliberate endings run the same gate**, over their own window's Surfaces: a quit, and closing one window of several (`docs/specs/standalone.md` → "Per-window close"). **Moving a Workspace to another window runs neither** — nothing ended, so the target hydrates the notes; pin behavior follows [Source links](#source-links). **Several windows archiving at once contend through the archive's own file lock and compare-and-swap retry** ([The archive port](#the-archive-port)), so a window whose write lost the race retries against fresh bytes rather than dropping the other window's batches.

**Standalone still archives at quit even though it now restores its windows** (`docs/specs/transport.md` → "The governing rule"): VS Code's live notes survive a Reload only through the extension host's in-memory mirror ([Live resume](#live-resume)), and quitting standalone leaves no such survivor.

Expand Down
4 changes: 4 additions & 0 deletions docs/specs/notepad.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ first sixteen are the user's and the rest are a fixed formula xterm itself appli

## Source links

In Tauri manual testing (2026-09-11), a transferred pin remained visible but failed
its text proof on use. Rebuilt buffers do not guarantee identical absolute row
positions. Transfers therefore retain notes without presenting unusable pins.

The pin could have stored a scrollback line number. It stores two xterm markers
because a marker is the only handle xterm keeps correct as the buffer scrolls, and
scrolling is the normal case — a capture is usually of something that has already
Expand Down
4 changes: 2 additions & 2 deletions docs/specs/security-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,8 @@ buffer, unlinked as it is read (`docs/specs/standalone.md` -> "Agent recovery").
excerpts the user explicitly captured, their colors, the Surface title and kind,
and the CWD at closure, appended only by a Surface closing
(`docs/specs/notepad.md` -> "Archive"). Standalone keeps it as
`<app_data_dir>/notepad-archive-v1.json`, owner-only and shared by every build
so notes are not stranded in a dev subtree; VS Code keeps it in
`<app_data_dir>/notepad-archive-v1.json`, owner-only and shared by builds with the
same Tauri identifier; the dev wrapper uses a per-worktree identifier. VS Code keeps it in
`<globalStorageUri>/notepad-archive.json`, mode `0600` on Unix and inheriting VS Code's directory ACL on Windows. Migration and Settings Sync follow `docs/specs/notepad.md` -> "VS Code lifecycle". Its live half never reaches disk.

**VS Code persists pane structure in VS Code's own storage** — `workspaceState`
Expand Down
Loading