Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9552098
Route every sidecar event to the window that owns its PTY
nedtwigg Sep 10, 2026
3e93ed3
Scope the sidecar's list and kill, and collect one ask answer per window
nedtwigg Sep 10, 2026
9290ddd
Give each window its own close, and make quit vote before it walks
nedtwigg Sep 10, 2026
c1615e7
Let a Workspace leave a Window without killing anything
nedtwigg Sep 10, 2026
fcb7d6e
Move a Workspace between windows without losing or duplicating output
nedtwigg Sep 10, 2026
6b53e5b
Drag a Workspace out of its window, and into another one
nedtwigg Sep 10, 2026
d3704db
Host the two app-global alert stores in the sidecar
nedtwigg Sep 10, 2026
12c72e8
Scope every window listener, and re-plan a Workspace that comes back
nedtwigg Sep 10, 2026
044d428
Promote multi-window into the specs
nedtwigg Sep 10, 2026
d4a01e3
Scan for unscoped window listeners where Node types exist
nedtwigg Sep 10, 2026
c469137
Stop tracking the generated alert-store bundle
nedtwigg Sep 10, 2026
c97eedc
Simplify the multi-window layer after review: the Rust hot path
nedtwigg Sep 10, 2026
ad3111d
Simplify the multi-window layer after review: the webview side
nedtwigg Sep 10, 2026
12e7748
Simplify the multi-window layer after review: the specs
nedtwigg Sep 10, 2026
4bbd00a
Route by ownership, queue arrivals, and settle a window at Destroyed
nedtwigg Sep 10, 2026
f311deb
Catch the macOS quit triggers Tauri never surfaces
nedtwigg Sep 10, 2026
a2d9539
Two-phase the transfer, token the init, and arbitrate the two teardowns
nedtwigg Sep 10, 2026
3ab1045
Promote the review fixes into the specs
nedtwigg Sep 10, 2026
ec25766
Pin the close confirmation's update warning
nedtwigg Sep 10, 2026
80415b0
Make a Workspace arrival one keyed transaction
nedtwigg Sep 10, 2026
3ee364f
Put the updater's install back behind main alone
nedtwigg Sep 10, 2026
dd6d722
Commit a transfer only once the target has adopted it
nedtwigg Sep 10, 2026
32d2a1b
Vote for a quit that meets a flow already tearing this window down
nedtwigg Sep 10, 2026
65f791e
Ask the host twice before believing it holds no PTYs
nedtwigg Sep 10, 2026
2a64fb4
Throttle the cross-window drag probe with the shared helper
nedtwigg Sep 10, 2026
e2ec5a5
Promote the transfer protocol into the specs
nedtwigg Sep 10, 2026
92ab9c4
Merge workspaces-persist into workspaces-multiwindow
nedtwigg Sep 10, 2026
797cc99
Keep votes already cast through a repeat quit trigger
nedtwigg Sep 10, 2026
42868ce
Hand back an arrival the target never adopts
nedtwigg Sep 10, 2026
424f826
Exit a quit trigger that finds no window; never reuse a close seq
nedtwigg Sep 11, 2026
0126aa2
Keep arriving shells out of a teardown, and platform reads out of the…
nedtwigg Sep 11, 2026
78ab50d
Correct the comments that still describe updater:* as every window's
nedtwigg Sep 11, 2026
fd68a46
Gate a re-driven teardown intent once, and scope a deferred quit to i…
nedtwigg Sep 11, 2026
54a1e2d
Unwind an adoption whose adopt_done Rust refused
nedtwigg Sep 11, 2026
b603f94
Ask for the boot list retry only when a saved terminal pane could be …
nedtwigg Sep 11, 2026
6cb9f10
Spec: state what a quit mid-transfer, a main-less relaunch and the cl…
nedtwigg Sep 11, 2026
2053992
Merge workspaces-persist into workspaces-multiwindow
nedtwigg Sep 11, 2026
9c2d302
Merge branch 'workspaces-persist' into workspaces-multiwindow
nedtwigg Sep 11, 2026
b3c12af
Reject expired tear-out adoptions before installing the Window
nedtwigg Sep 11, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ standalone/sidecar/iframe-proxy.cjs
standalone/sidecar/recovery.cjs
standalone/sidecar/agent-browser-host.cjs
standalone/sidecar/burrow.cjs
standalone/sidecar/alert-store.cjs
# Kept beside it: a checkout that built before the Burrow rename still holds
# the old bundle, and `bundle.resources` would ship it.
standalone/sidecar/remote-host.cjs
Expand Down
4 changes: 2 additions & 2 deletions docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Rules:
- **Editing the rule set re-derives WATCHING across every live Session immediately**, so a mid-command enable shows what that command is doing *right now* rather than a fresh `NOTHING_TO_SHOW` (rationale).
- **A WATCHING ring outlives the command that raised it.** Watching switches off when the watched command exits; its ring and originating command key remain in `watchingRingingCommand`.
- **Removing a rule silences its WATCHING rings**, even after the command has exited; other dismissal paths follow Clearing And TODO. A command merely ending never clears the ring.
- **The rule set is app-global and persisted** (`dormouse:watched-commands`), starting empty, so WATCHING is off everywhere until the user turns it on. **In VS Code the shared extension host is authoritative**, so a stale webview can neither replace unrelated rules nor keep reporting an obsolete list; the seed/mutation/broadcast wire contract is `docs/specs/transport.md`.
- **The rule set is app-global and persisted** (`dormouse:watched-commands`), starting empty, so WATCHING is off everywhere until the user turns it on. **The host is authoritative wherever one serves several webviews** — the VS Code extension host, and the standalone sidecar — so a stale webview can neither replace unrelated rules nor keep reporting an obsolete list: the first webview's persisted copy is taken as the seed, an edit is a delta, and the host broadcasts its canonical snapshot back. The seed/mutation/broadcast wire contract is `docs/specs/transport.md`.

**Limitation:** WATCHING needs the shell to report command boundaries (`OSC 633` / `OSC 133`). Shells without integration (`docs/specs/terminal-escapes.md`) never report a command name, so WATCHING never engages and the bell reports "nothing is running". Terminal reports still work; command-exit alerting also requires semantic command boundaries. **Never route the keystroke fallback in `docs/specs/terminal-state.md` into the `AlertManager`** (rationale).

Expand Down Expand Up @@ -241,7 +241,7 @@ Clearing behavior:

## Alarm settings

The alarm settings are a second app-global store beside the WATCHING rule set, edited in the app-global **Settings** dialog (below), which also carries the theme picker ([theme.md](./theme.md)), the shell picker ([standalone.md](./standalone.md)), and the remote-control section ([relay.md](./relay.md)). **Each of those keeps its own store — never fold one into `AlertSettings`**, which is relayed wholesale to the VS Code extension host.
The alarm settings are a second app-global store beside the WATCHING rule set, edited in the app-global **Settings** dialog (below), which also carries the theme picker ([theme.md](./theme.md)), the shell picker ([standalone.md](./standalone.md)), and the remote-control section ([relay.md](./relay.md)). **Each of those keeps its own store — never fold one into `AlertSettings`**, which is relayed wholesale to the host. **A host revalidates the blob before installing it** (`normalizeAlertSettings`): a webview must never be able to hand it a NaN or an absurd timer. Both stores run the same two classes in either host (`lib/src/lib/watched-command-host.ts`, `lib/src/lib/alert-settings-host.ts`), bound for standalone by `lib/src/host/alert-store-host.ts`; the shape, its defaults and its validation are the platform-free `lib/src/lib/alert-settings-model.ts`.

| Field | Meaning |
|---|---|
Expand Down
7 changes: 5 additions & 2 deletions docs/specs/auto-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ The standalone app checks for updates on launch and prompts in the Baseboard. **

### Quit-time install

**The updater owns no quit interception** — install runs only when `hasPendingUpdate()` is true, after the quit orchestrator's teardown and save/drain steps (`docs/specs/standalone.md` §Quit flow) (rationale). `installPendingUpdate()` writes the success marker *before* `install()` (§localStorage), and on Windows first awaits bounded sidecar teardown (§Sidecar teardown on Windows). **It never closes the window itself** — exiting the process is `quit_proceed`'s job, after this returns.
**The updater owns no quit interception** — install runs only when `hasPendingUpdate()` is true, after the quit orchestrator's teardown and save/drain steps (`docs/specs/standalone.md` §Quit flow) (rationale). **It runs in `main`, the window the quit walk tears down last and the only one holding `updater:*`** (`capabilities/main-only.json`); every other window has handed on by then, so nothing it could still be writing outlives the install.

**Only `main` ever checks**, so it is the only window that can hold a download at all — and **closing `main` throws away an approved one**, which lives in that webview's memory. Its close confirmation says so, and is shown for that reason alone even with nothing running (`docs/specs/standalone.md` → "Per-window close"); a session that has closed `main` simply has no update to install until it relaunches (rationale). `installPendingUpdate()` writes the success marker *before* `install()` (§localStorage), and on Windows first awaits bounded sidecar teardown (§Sidecar teardown on Windows). **It never closes the window itself** — exiting the process is `quit_proceed`'s job, after this returns.

**In Vite dev mode (`pnpm dev:standalone`) `installPendingUpdate()` drops the pending update and skips `install()`** (rationale), so install must be tested from a packaged app; **`MODE === 'test'` lifts the skip** for `standalone/src/updater.test.ts`.

Expand Down Expand Up @@ -83,7 +85,8 @@ Single key: `dormouse:update-result`
| [`standalone/src/main.tsx`](../../standalone/src/main.tsx) | `<ConnectedUpdateBanner />` (banner + modal) as `<App />`'s `baseboardNotice`; `startUpdateCheck()` after restore |
| [`standalone/src-tauri/tauri.conf.json`](../../standalone/src-tauri/tauri.conf.json) | Updater endpoint, public key, artifact mode, Windows install mode |
| [`standalone/src-tauri/src/lib.rs`](../../standalone/src-tauri/src/lib.rs) | Plugin registration, sidecar teardown, update-log tail |
| [`standalone/src-tauri/capabilities/default.json`](../../standalone/src-tauri/capabilities/default.json) | Updater, version, and shell permissions |
| [`standalone/src-tauri/capabilities/default.json`](../../standalone/src-tauri/capabilities/default.json) | Shell and window permissions, for `main` and every `ws-*` window |
| [`standalone/src-tauri/capabilities/main-only.json`](../../standalone/src-tauri/capabilities/main-only.json) | Updater and app-version permissions, scoped to `main` alone |

## Configuration

Expand Down
2 changes: 2 additions & 0 deletions docs/specs/auto-update.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

**Why install runs last.** A Windows NSIS install force-kills the app the moment it starts, so starting it early interrupts teardown. This ordering originally protected persisted scrollback; what it protects now is the window's structure, which standalone does persist. The retained save/drain hooks and their completion semantics are explained in `docs/specs/standalone.rationale.md` → Quit flow.

**Why `updater:*` stayed scoped to `main`.** Widening it to every window was meant to cover a session whose `main` was closed. It covers nothing: only `main` runs the periodic check, so only `main` can be holding a download, and a `main`-less session has none to install whichever window the walk ends on. The grant gave up a structural guarantee — the install can only happen in the window torn down last — for a case that cannot arise. What that session needs is to be told before it happens, which is the close confirmation's discard warning.

**Why Vite dev mode skips `install()`.** The updater resolves its replacement target from the current executable path, which in dev is the dev executable's directory, not a packaged bundle.

## Sidecar teardown on Windows
Expand Down
23 changes: 17 additions & 6 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ Control direction: `dor` → sidecar JSON-lines net socket → Rust command/even
bridge → `TauriAdapter` `CustomEvent("dormouse:control-request")` → Wall
handler, and back along the same hops.

**A request is routed to the window that owns its `DORMOUSE_SURFACE_ID`**, and
one naming no Surface goes to the focused window. **A Surface no window owns
fails** — `No Dormouse window owns surface '<id>'` — rather than being handed to
a sibling, which would act on the wrong terminal. **A cancel follows its own
request** to the window holding it (`docs/specs/standalone.md` → Routing).

### VS Code

`vscode-ext/package.json` runs `pnpm stage:dor-cli` before bundling the
Expand Down Expand Up @@ -246,8 +252,10 @@ and each host's hop in `standalone/src/tauri-adapter.ts`,

`Window ⊃ Workspace ⊃ Pane ⊃ Surface` (`docs/specs/glossary.md`). **User-facing
`dor` commands expose Surface handles only**, and because a Window can hold
several Workspaces the handle model reserves `workspace:<n|name>` and
`window:<n>` refs.
several Workspaces — and standalone can hold several Windows — the handle model
reserves `workspace:<n|name>` and `window:<label>` refs. `Reserved:` no command
targets another Window yet; a request reaches the window that owns its Surface
instead (§Standalone), which is what `## Future` → `dor workspace` builds on.

Invariants:

Expand Down Expand Up @@ -284,8 +292,11 @@ Invariants:
list output always includes both refs and stable ids.
- `workspace:<n>` selects a container and is **positional**, so a strip reorder
renumbers it; `workspace:<name>` is the stable handle and is staged with the
`dor workspace` commands (see [Future](#future)). `window:<n>` is rejected for
every `n` but 1. **Every Workspace has a `surface:1`**, so a Surface ref alone
`dor workspace` commands (see [Future](#future)). **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, and **rejects
every other Window's**, there being nothing it could do with one.
**Every Workspace has a `surface:1`**, so a Surface ref alone
never identifies a Workspace.
- **One Wall answers each request**, resolved in order: an explicit
`workspace:<n>`, else the Workspace owning the calling Surface, else the
Expand Down Expand Up @@ -313,7 +324,7 @@ and the dispatching webview cannot drift. `surface.list` joins the current
Workspace's Surfaces — visible panes **plus minimized (doored)** ones, each
tagged `view` (`paned` / `zoomed` / `minimized`) — with terminal state and
activity snapshots, and reports the answering Workspace's own `workspace:<n>`
alongside `window:1`. Per the
alongside the answering Window's `window:<label>` (Handle Model). Per the
visible-vs-listed split [Handle Model](#handle-model) states, **a visible split
reference adds a pane in Lath, a minimized one a sibling Door in the
baseboard.** **`dor list` rows sort by the Workspace-stable `surface:N` ref**, a
Expand Down Expand Up @@ -589,7 +600,7 @@ Source of truth: `buildDorSurfacesInternal` in `lib/src/components/Wall.tsx`; `d
workspaces-rollout).
- **Workspace handles and commands** — a `--workspace` target flag and `dor
workspace` management commands (new / rename / close / switch — mutation only)
consuming the reserved `workspace:<n|name>` / `window:<n>` ref grammar above.
consuming the reserved `workspace:<n|name>` / `window:<label>` ref grammar above.
Like every command they ship with snapshot-tested help and the control methods
that back them, not ahead of them. Staged with the workspaces rollout
(`docs/specs/layout.md` `## Future`, workspaces-rollout).
11 changes: 7 additions & 4 deletions docs/specs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ Workspace and Window are containers, not Session layers — they group Surfaces

| Container | Holds | Owner |
|---|---|---|
| **Window** | One or more Workspaces; the OS frame (the standalone Tauri window) or the host frame (a VS Code window). | host (Tauri / VS Code) |
| **Window** | One or more Workspaces; the OS frame (a standalone Tauri window) or the host frame (a VS Code window). A host may hold several, and a Workspace may move between them. Its **Tauri label is its persistence identity** — one snapshot per label (`docs/specs/standalone.md` → Windows). | host (Tauri / VS Code) |
| **Workspace** | "A window's worth of panes": a `WorkspaceId`, a user-facing `name`, its Panes and Surfaces, and the layout arranging them (Lath snapshot + doors). Exactly one **Wall** renders one Workspace. | `lib/src/lib/workspace-store.ts` (the model), `lib/src/components/Wall.tsx` at render time; persisted per `docs/specs/transport.md` |

How many Workspaces a Window shows at once is host-specific:

- **Standalone** mounts every Workspace's Wall at once and shows one, switching between them (`docs/specs/layout.md` → Workspaces).
- **Standalone** mounts every Workspace's Wall at once and shows one, switching between them (`docs/specs/layout.md` → Workspaces); a Window may be one of several.
- **VS Code** maps one Workspace to one webview, several visible at once: the sidebar/panel `WebviewView` is the default Workspace, each `dormouse.open` editor-tab `WebviewPanel` an independent one owning its Sessions' PTYs and browser Surfaces (`docs/specs/vscode.md`).

### Wall chrome
Expand All @@ -88,7 +88,7 @@ A Workspace's **union status** is its display projection of member Surfaces' Act

### Implementation status

The Pane / Surface model, surface kinds, the Workspace model, and per-Workspace persistence are live; a Window still means one OS window (`docs/specs/layout.md` → Workspaces). Ledger: `docs/specs/layout.md` `## Future` (**Scope: workspaces-rollout**); this glossary does not track it.
The Pane / Surface model, surface kinds, the Workspace model, per-Workspace persistence, and several Windows each holding several Workspaces are live (`docs/specs/layout.md` → Workspaces). Ledger: `docs/specs/layout.md` `## Future` (**Scope: workspaces-rollout**); this glossary does not track it.

## Roles

Expand Down Expand Up @@ -202,8 +202,10 @@ A user verb is an intentional action that produces a single observable change.
| `closeWorkspace` | `kill` each member Surface, then remove the Workspace; the last remaining Workspace cannot be closed. |
| `renameWorkspace` | Update a Workspace's `name`; touches no Session |
| `moveWorkspace` | Reorder a Workspace within its Window; renumbers the positional `workspace:<n>` refs and touches no Session |
| `transferWorkspace` | Move a Workspace to another Window, Surfaces and Sessions intact: `release` each member Session (detached, Process still Live) and resume it there. Kills nothing and archives nothing — not a `closeWorkspace`. |
| `tearOut` | `transferWorkspace` into a Window created for it. A Window whose last Workspace leaves closes itself. |

Source of truth: `setActiveWorkspace` / `createWorkspace` / `closeWorkspace` / `renameWorkspace` / `moveWorkspace` in `lib/src/lib/workspace-store.ts`; `closeAll` in `lib/src/components/Wall.tsx`.
Source of truth: `setActiveWorkspace` / `createWorkspace` / `closeWorkspace` / `renameWorkspace` / `moveWorkspace` in `lib/src/lib/workspace-store.ts`; `closeAll` in `lib/src/components/Wall.tsx`; `prepareWorkspaceTransfer` in `lib/src/components/wall/workspace-transfer.ts`.

### System verbs

Expand All @@ -212,6 +214,7 @@ A system verb is a lifecycle transition driven by the runtime.
| Verb | Effect |
|---|---|
| `register` / `dispose` | Create / destroy a Registry entry |
| `release` | Destroy a Registry entry **without** killing its Process (Registry: Mounted → Disposed, Process: Live → Live). The one verb a `transferWorkspace` runs, and never reachable from an unmount. |
| `mount` / `unmount` | Attach / detach the persistent DOM element (low-level op; the Registry entry survives `unmount`). A **parked** leaf stays mounted while `Doored` or `Hidden` (`docs/specs/tiling-engine.md` → "Parked leaves") |
| `exit` | Host observes process death (Process: Live → Exited) |
| `resume` | Webview reopens over retained PTYs (Link: Severed → Resuming → Live; Registry rebuilt from replay data; Process stays Live/Exited) |
Expand Down
Loading