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
2 changes: 0 additions & 2 deletions docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,6 @@ Public `status` is a projection — first match wins:

Persist `status`, `watchingEnabled`, `todo`, and sanitized `notification`. Restore `todo` and `notification`, then restart WATCHING only if `watchingEnabled` is true. Restore must not recreate protocol progress, command-exit arms, or a fresh ring; replay filtering in `docs/specs/terminal-escapes.md` prevents old terminal output from firing notification side effects again.

Source of truth: `migrateTodoState` in `lib/src/lib/alert-manager.ts` defines the legacy-TODO-value migration to boolean.

## Attention

`attentionSessionId` is set only by explicit user actions that plausibly mean "I am looking at this Session":
Expand Down
11 changes: 4 additions & 7 deletions docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ Two independent axes define a browser pane:
| Render | `ab-screencast`, `ab-popout`, `iframe` |

The render axis is a pane parameter, not a separate surface type. The `dor` CLI
still reports `iframe` or `agent-browser` as a legacy/informative surface type;
that is derived from `renderMode`.
still reports `iframe` or `agent-browser` as an informative surface type derived
from `renderMode` (never stored).

Source of truth: `lib/src/components/wall/BrowserPanel.tsx`,
`lib/src/components/wall/browser-surface.ts`, `lib/src/components/Wall.tsx`
Expand All @@ -46,15 +46,12 @@ type BrowserPanelParams = {
wsPort?: number;
binaryPath?: string;
syncEngaged?: boolean;
poppedOut?: boolean; // legacy migration only
};
```

Invariants:

- `renderMode` is canonical. Legacy params (`surfaceType: 'iframe'`,
`surfaceType: 'agent-browser'`, `poppedOut`) are migrated by
`resolveRenderMode`.
- `renderMode` is canonical.
- `url` is the canonical target across render swaps and relaunches. Agent-browser
mirrors the newest non-blank active tab URL into params; iframe persists only
navigations initiated by Dormouse chrome.
Expand Down Expand Up @@ -513,7 +510,7 @@ Source of truth: `lib/src/lib/platform/types.ts`,

When changing browser-surface behavior:

- `renderMode` is canonical. Never reintroduce `surfaceType: 'iframe' | 'agent-browser'` or `poppedOut` as stored state — extend the `resolveRenderMode` migration instead, and update the kind-mapping table in `docs/specs/glossary.md` if adding a render mode.
- `renderMode` is canonical. Never reintroduce `surfaceType: 'iframe' | 'agent-browser'` or `poppedOut` as stored state; update the kind-mapping table in `docs/specs/glossary.md` if adding a render mode.
- Never move a browser surface's DOM. Lath's leaf div is never re-parented, so an `<iframe>` never reloads and the screencast canvas never moves mid-click (which would break click synthesis) — do not add any re-parenting/reordering that would defeat this.
- A new agent-browser subcommand must be added to `AGENT_BROWSER_ALLOWED_SUBCOMMANDS` (`lib/src/lib/platform/types.ts`); the host-side allowlist is the security boundary, not the CLI.
- External-binary spawns go through `spawnAndCapture` (`dor-lib-common`), never raw `child_process` — see `docs/specs/dor-cli.md` → Spawning External Binaries.
Expand Down
8 changes: 4 additions & 4 deletions docs/specs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ Today every Pane holds exactly one Surface, but the model reserves multiple Surf

Three related enums name a Surface's kind at different layers. This table is the canonical mapping:

| Surface | Persisted `surfaceType` (`docs/specs/transport.md`) | `renderMode` (`docs/specs/dor-browser.md`) | CLI `SurfaceType` (`dor`, legacy/derived) |
| Surface | Persisted `surfaceType` (`docs/specs/transport.md`) | `renderMode` (`docs/specs/dor-browser.md`) | CLI `SurfaceType` (`dor`, derived) |
|---|---|---|---|
| terminal Session | `'terminal'` (the default, omitted from the row) | — | `terminal` |
| browser, iframe renderer | `'browser'` | `iframe` | `iframe` |
| browser, screencast renderer | `'browser'` | `ab-screencast` | `agent-browser` |
| browser, popped-out renderer | `'browser'` | `ab-popout` | `agent-browser` |

For browser Surfaces `renderMode` is canonical; the CLI column is derived from it for backwards-compatible `dor` output and is never stored.
For browser Surfaces `renderMode` is canonical; the CLI column is derived from it for `dor` output and is never stored.

A **Session** runs the full six-axis model below. A **browser Surface** participates only where a web view meaningfully can:

Expand Down Expand Up @@ -70,7 +70,7 @@ A Surface never floats free: it sits in a **Pane**, every Pane belongs to exactl
| **Window** | One or more Workspaces. The OS frame (the standalone Tauri window) or the host frame (a VS Code window). | host (Tauri / VS Code) |
| **Workspace** | A named set of Panes and their Surfaces, plus the layout that arranges them (Lath layout snapshot + doors). Exactly one **Wall** renders one Workspace. | `lib/src/components/Wall.tsx` at render time; persisted per `docs/specs/transport.md` |

A **Workspace** is the durable grouping a user thinks of as "a window's worth of panes." It has a `WorkspaceId`, a user-facing `name`, the Surfaces it contains, and the layout that arranges them. The pre-workspace model had exactly one implicit Workspace per Window; the model now allows several.
A **Workspace** is the durable grouping a user thinks of as "a window's worth of panes." It has a `WorkspaceId`, a user-facing `name`, the Surfaces it contains, and the layout that arranges them. The model allows several Workspaces per Window, though the app mounts one at a time today.

How many are visible at once is host-specific:

Expand Down Expand Up @@ -241,7 +241,7 @@ Use glossary names instead of these. The left column retains a meaning only wher
| **session** | The durable identity of a **terminal Surface**. Do not use it for the Activity projection (that is `ActivityState`, not `SessionUiState`), nor for the agent-browser daemon's lowercase `session` string (`dormouse.1.<key>`), which is not a Dormouse durable unit. |
| **terminal** | Keeps its meaning for the `xterm.Terminal` instance. Prose meaning "the whole thing" is **Session** (a terminal Surface). |
| **surface** | A glossary term, not retired: the durable occupant of a Pane (a terminal Session or a browser Surface). Use **Session** only for the terminal kind; use **Surface** when a statement holds for both. |
| **panel / pane / leaf** | Prefer **pane** for the layout slot; **leaf** is Lath's tree node for the same thing (they map 1:1). "panel" survives only in legacy persisted-blob field names. |
| **panel / pane / leaf** | Prefer **pane** for the layout slot; **leaf** is Lath's tree node for the same thing (they map 1:1). "panel" survives only in React component names (`TerminalPanel`, `BrowserPanel`, `IframePanel`). |
| **tether** | Remote-control term only: a display showing "tethering to \<device\>" has ceded terminal size authority to a remote viewer (`docs/specs/remote-api.md`). Not a layout term — do not use it for Pane/Door relationships. |

Remote-only vocabulary (**Viewer**, the wire-level `DirectoryEntry` projection) is defined in `docs/specs/remote-api.md` § Terminology.
Expand Down
16 changes: 8 additions & 8 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ Extreme case: a single door with a very long title, with more doors on both side

A **Workspace** is one Wall's worth of Surfaces (terminal Sessions and browser surfaces) plus its layout, with a user-facing name. The standalone Window hosts several Workspaces but mounts only one — the **active** Workspace — at a time. Each Workspace owns its own Content (Lath layout) and Baseboard (doors).

What exists today lives behind the `dormouse.flags.workspaces` flag (`WORKSPACES_FLAG_KEY` in `lib/src/lib/feature-flags.ts`, **off by default**): the in-memory workspace model and its container verbs (`createWorkspace` / `closeWorkspace` / `renameWorkspace` / `setActiveWorkspace` in `lib/src/lib/workspace-store.ts`), the union projection (`computeWorkspaceUnion` in `lib/src/lib/workspace-union.ts`), and Window persistence (`PersistedWindow`, `docs/specs/transport.md`). `setActiveWorkspace` changes the active id in the model but does not yet re-render the Wall, and the single-Workspace cap is still in place. With the flag off, the app persists a bare `PersistedSession` and runs exactly one implicit Workspace — byte-identical to pre-workspace behavior.
What exists today lives behind the `dormouse.flags.workspaces` flag (`WORKSPACES_FLAG_KEY` in `lib/src/lib/feature-flags.ts`, **off by default**): the in-memory workspace model and its container verbs (`createWorkspace` / `closeWorkspace` / `renameWorkspace` / `setActiveWorkspace` in `lib/src/lib/workspace-store.ts`), the union projection (`computeWorkspaceUnion` in `lib/src/lib/workspace-union.ts`), and Window persistence (`PersistedWindow`, `docs/specs/transport.md`). `setActiveWorkspace` changes the active id in the model but does not yet re-render the Wall, and the single-Workspace cap is still in place. With the flag off, the app persists a bare `PersistedSession` and runs exactly one implicit Workspace.

The strip UI, real switching, and lifecycle UX are staged in [Future](#future) — this spec's `## Future` is the single rollout ledger for the feature; other specs link here.

Expand Down Expand Up @@ -264,7 +264,7 @@ Swaps session **content** between two panes — the layout shape is unchanged. A
- **Neighbor** — the sibling still exists: split beside it on the original edge (at 50/50).
- **Fallback** — split beside a caller-supplied live reference (the selected pane, else the first pane) via `autoEdge`; into an empty tree the leaf becomes the root.

A Door persisted before Lath has no token; the Wall synthesizes a neighbor-tier one from its legacy `{neighborId, direction}` fields (`legacyTokenFromDoor`). A door dragged out of the baseboard skips the token entirely and inserts at the hit-tested drop position the user chose (`onExternalDrop` → `lath.insertLeaf`).
A door dragged out of the baseboard skips the token entirely and inserts at the hit-tested drop position the user chose (`onExternalDrop` → `lath.insertLeaf`).

## Inline rename

Expand Down Expand Up @@ -295,15 +295,15 @@ For a terminal Surface the pane ID is its session ID. `TerminalPane` calls `getO

### Session persistence

Layout, scrollback, cwd, minimized items, user-pinned titles, untouched state, and alert state are saved to persistent storage via a debounced save (500ms). The layout persists as the native Lath format (`lathLayout`; `docs/specs/tiling-engine.md` → "Persistence and migration"); the legacy dockview `layout` key is no longer written. Derived command/app labels shown on minimized doors are display-only and are not persisted as user-pinned titles. Every Lath store commit (add/remove/resize/swap/meta, including the active-pane the layout records) *schedules* the debounced save; content changes (terminal output, activity/TODO, pane title/command state, minimized-door changes) only *mark the session dirty*; a 30s heartbeat persists only when the session is dirty, so an idle app stops writing. Saves are flushed immediately and unconditionally on PTY exit, `pagehide`, and extension shutdown requests — the correctness net for any dirty-trigger gap. The dirty-gating mechanism and the store-level identical-value backstop are specified in `docs/specs/standalone.md` §Persistence.
Layout, scrollback, cwd, minimized items, user-pinned titles, untouched state, and alert state are saved to persistent storage via a debounced save (500ms). The layout persists as the native Lath format (`lathLayout`; `docs/specs/tiling-engine.md` → "Persistence"). Derived command/app labels shown on minimized doors are display-only and are not persisted as user-pinned titles. Every Lath store commit (add/remove/resize/swap/meta, including the active-pane the layout records) *schedules* the debounced save; content changes (terminal output, activity/TODO, pane title/command state, minimized-door changes) only *mark the session dirty*; a 30s heartbeat persists only when the session is dirty, so an idle app stops writing. Saves are flushed immediately and unconditionally on PTY exit, `pagehide`, and extension shutdown requests — the correctness net for any dirty-trigger gap. The dirty-gating mechanism and the store-level identical-value backstop are specified in `docs/specs/standalone.md` §Persistence.

In standalone, each Workspace's snapshot is wrapped in a Window snapshot that records every Workspace (name + layout) and which one is active, so all Workspaces — not just the mounted one — survive a restart. VS Code persists one Workspace per webview exactly as today (one snapshot per `WebviewView` / `WebviewPanel`). The persisted container types (`PersistedWorkspace`, `PersistedWindow`) and their migration live in `docs/specs/transport.md`.
In standalone, each Workspace's snapshot is wrapped in a Window snapshot that records every Workspace (name + layout) and which one is active, so all Workspaces — not just the mounted one — survive a restart. VS Code persists one Workspace per webview exactly as today (one snapshot per `WebviewView` / `WebviewPanel`). The persisted container types (`PersistedWorkspace`, `PersistedWindow`) live in `docs/specs/transport.md`.

Saved snapshots are read through `readPersistedSession()`, which accepts the canonical object shape and defensively parses a JSON-stringified blob before validation and migration. This keeps malformed storage inert while covering hosts that hand back serialized JSON instead of the parsed object.
Saved snapshots are read through `readPersistedSession()`, which accepts the canonical object shape and defensively parses a JSON-stringified blob before validation. A present-but-unreadable blob is logged and discarded, so malformed storage starts fresh rather than blocking startup, while hosts that hand back serialized JSON instead of the parsed object are still covered.

On startup, recovery is priority-based:
1. **Resume** (webview hidden/shown, live PTYs): request PTY list + replay data from platform, `resumeTerminal()` for each (500ms timeout). Saved pane and door titles are seeded back via `setTerminalUserTitle()` (see `docs/specs/transport.md`) so persisted placeholder labels never replay as user pins. If the saved session covers every live PTY, restore the saved Lath layout when its leaf set matches (a pre-Lath save's legacy dockview `layout` blob is migrated into that single channel at the session read boundary) and reattach saved minimized items as doors. This still counts as a live resume when every live session is minimized, so recovery must not fall through to cold restore just because the visible `paneIds` list is empty.
2. **Restore** (app restart, cold start): the Wall's `seed` hydrates from the restored Lath layout (the read boundary already migrated any legacy dockview `layout` blob one-way into it), else falls to (3); `restoreTerminal()` for each pane with saved cwd + scrollback, spawning each PTY with the current default shell selection
1. **Resume** (webview hidden/shown, live PTYs): request PTY list + replay data from platform, `resumeTerminal()` for each (500ms timeout). Saved pane and door titles are seeded back via `setTerminalUserTitle()` (see `docs/specs/transport.md`) so persisted placeholder labels never replay as user pins. If the saved session covers every live PTY, restore the saved Lath layout when its leaf set matches and reattach saved minimized items as doors. This still counts as a live resume when every live session is minimized, so recovery must not fall through to cold restore just because the visible `paneIds` list is empty.
2. **Restore** (app restart, cold start): the Wall's `seed` hydrates from the restored Lath layout, else falls to (3); `restoreTerminal()` for each pane with saved cwd + scrollback, spawning each PTY with the current default shell selection
3. **Fallback/manual pane creation**: when no saved layout can be safely applied, add multiple panes as splits from the previous pane, and spawn each PTY with the current default shell selection
4. **Empty state**: create a single new pane with the current default shell selection

Expand Down Expand Up @@ -401,7 +401,7 @@ When changing layout behavior:

- Changing a command-mode binding or the mode-switch gesture: update the shortcut table here **and** `docs/specs/shortcuts.md` in the same edit.
- Pane-header changes: this spec owns placement and sizing only. Bell/TODO behavior and visual states belong to `docs/specs/alert.md`; the mouse-override icon and banner to `docs/specs/mouse-and-clipboard.md`; the derived label to `docs/specs/terminal-state.md`.
- Persisted-shape changes (`PersistedPane` / `PersistedDoor` / layout blobs) belong to `docs/specs/transport.md` — add the migration note there.
- Persisted-shape changes (`PersistedPane` / `PersistedDoor` / layout blobs) belong to `docs/specs/transport.md` — update it there.
- New pane chrome uses tokens from `lib/src/components/design.tsx` (see AGENTS.md Design); never raw color classes.
- Pane spawn/kill/tween motion is owned by the Lath animator (`docs/specs/tiling-engine.md` → "Animation"); layout.md owns only the interaction behavior around it.
- Anything workspace-strip or switching related stays under `## Future` (workspaces-rollout) until built.
Expand Down
31 changes: 0 additions & 31 deletions docs/specs/standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,19 +246,6 @@ trigger missed; the cache is boot-seeded from disk in `hydrate`, so the compare
is valid from the first write. Source of truth: `session-dirty.ts`,
`use-session-persistence.ts`, `standalone/src/tauri-session-store.ts`.

**Migration.** On the first boot after this change `load_session` returns null;
if a legacy blob is still in `localStorage` under `TauriAdapter.STATE_KEY`, the
adapter adopts it, persists it to the Rust store (through the store's normal
write path, so it shares the coalescing), and removes the key — so WebKit stops
rewriting it and its bloated WAL collapses on the next quit. The legacy blob is
per-origin (window-shared), so adoption is gated to the `main` window — a
second window never races the adopt-and-clear (removal criteria: `## Future`).
Because the
identical-value short-circuit above would swallow a `setItem` whose value equals
the freshly hydrated cache, the migration hydrates with the pre-migration seed
(null) and lets the `setItem` be a genuine change — localStorage is already
cleared, so the Rust store is the blob's only remaining home.

**Durability on quit.** A clean quit durably writes the latest state before the
process exits. `saveState` still returns after updating the cache and *firing*
`save_session`, but the quit orchestrator (§Quit flow) now awaits the pipeline to
Expand Down Expand Up @@ -460,21 +447,3 @@ root `package.json` for the `dev:standalone*` orchestration.
| `standalone/sidecar/clipboard-ops.js` | OS clipboard tiers (owned by `docs/specs/mouse-and-clipboard.md`) |
| `standalone/scripts/build-sidecar-proxy.mjs` | Bundles `lib/src/host/` into the sidecar `.cjs` copies |
| `standalone/scripts/dev-agent-browser.mjs` | `dev:standalone:ab` entry (owned by `docs/specs/transport.md`) |

## Future

Requirements for the next round of session-persistence work — what must be true,
not how. (Detailed working notes live outside the specs while the work is in
flight.)

**Retire the `localStorage` → Rust migration.** The one-time migration branch in
`standalone/src/tauri-adapter.ts` (marked `SUNSET`, gated to the `main` window)
cannot be removed on a version fence: the Tauri updater jumps directly from any
old version to the latest, so there is no release after which "all shipped
builds have migrated" — a pre-migration straggler updating much later would
adopt nothing and lose exactly one session restore. Removal is therefore an
acceptance decision, recorded here (2026-07-07): remove once a one-time lost
restore for pre-0.12 stragglers is acceptable — recommended at least six months
or two minor releases after the migration ships. Multi-window (the
workspaces-rollout scope, `docs/specs/layout.md` `## Future`) does not block
removal: the branch already gates adoption to the `main` window.
Loading
Loading