diff --git a/.gitignore b/.gitignore index 082f78933..50f588415 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/docs/specs/alert.md b/docs/specs/alert.md index f9ff4f24a..3d58a72ca 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -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). @@ -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 | |---|---| diff --git a/docs/specs/auto-update.md b/docs/specs/auto-update.md index 573560aac..c763d9934 100644 --- a/docs/specs/auto-update.md +++ b/docs/specs/auto-update.md @@ -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`. @@ -83,7 +85,8 @@ Single key: `dormouse:update-result` | [`standalone/src/main.tsx`](../../standalone/src/main.tsx) | `` (banner + modal) as ``'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 diff --git a/docs/specs/auto-update.rationale.md b/docs/specs/auto-update.rationale.md index 2fd8b4b01..0c69ae1db 100644 --- a/docs/specs/auto-update.rationale.md +++ b/docs/specs/auto-update.rationale.md @@ -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 diff --git a/docs/specs/dor-cli.md b/docs/specs/dor-cli.md index ed8e0337d..11023b1c0 100644 --- a/docs/specs/dor-cli.md +++ b/docs/specs/dor-cli.md @@ -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 ''` — 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 @@ -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:` and -`window:` refs. +several Workspaces — and standalone can hold several Windows — the handle model +reserves `workspace:` and `window: