From 3c44ce1859b981ae2e5c376f91e3935068321cde Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Jun 2026 14:45:06 -0700 Subject: [PATCH 01/11] docs: spec pass for the Workspace/Window model Introduce Workspace (one Wall = one Workspace) and Window as containers in the glossary, with a display-only union projection (ringing/todo/count) distributed across the area specs: - glossary: Containers section (Window/Workspace), union status, verbs (switch/create/close/rename), invariants I7-I9, naming conventions - alert: Workspace union projection (derived, includes Doored + inactive) - layout: standalone workspace strip, switching, lifecycle, persistence - transport: PersistedWorkspace/PersistedWindow container + v3 migration - vscode: webview-to-Workspace mapping, union reflection onto tab icon/title and view badge; "not yet implemented" bullet - dor-cli: workspace: / window: handle refs - shortcuts: rename "Workspace mode" -> "Command mode" (frees the word) - DESIGN / AGENTS / mobile-ui: index + navigation updates Spec-only; UI visuals and keybindings deferred to the Storybook pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 10 +++++----- DESIGN.md | 3 ++- docs/specs/alert.md | 23 +++++++++++++++++++++++ docs/specs/dor-cli.md | 25 +++++++++++++++++-------- docs/specs/glossary.md | 38 ++++++++++++++++++++++++++++++++++++-- docs/specs/layout.md | 26 ++++++++++++++++++++++++++ docs/specs/mobile-ui.md | 3 ++- docs/specs/shortcuts.md | 20 ++++++++++++-------- docs/specs/transport.md | 8 +++++++- docs/specs/vscode.md | 16 ++++++++++++++++ 10 files changed, 146 insertions(+), 26 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5956e45f..28d2fab9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,14 +30,14 @@ pnpm build # build lib, vscode extension, and website The primary job of a spec is to be an accurate reference for the current state of the code. Read the relevant spec before modifying a feature it covers — the spec describes invariants, edge cases, and design decisions that are not obvious from the code alone. -- **`docs/specs/glossary.md`** — Canonical vocabulary for Session states, layers (Process / Registry / View / Link / Activity / Snapshot), transition verbs, and the Liskov contract on Registry APIs. Read this first. Other specs defer to it when naming a state or a verb. -- **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, minimize/reattach, inline rename, session lifecycle, session persistence, and theming. Read this when touching: `Wall.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode behavior. +- **`docs/specs/glossary.md`** — Canonical vocabulary for Session states, layers (Process / Registry / View / Link / Activity / Snapshot), the Workspace / Window containers and their union status, transition verbs, and the Liskov contract on Registry APIs. Read this first. Other specs defer to it when naming a state or a verb. +- **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, minimize/reattach, inline rename, the standalone workspace strip (tabs, switching, lifecycle), session lifecycle, session persistence, and theming. Read this when touching: `Wall.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `standalone/src/AppBar.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode/workspace behavior. - **`docs/specs/dor-cli.md`** - the `dor` CLI which is prepended onto the path of every terminal that dormouse launches. -- **`docs/specs/alert.md`** — Activity monitoring state machine, alert trigger/clearing rules, attention model, TODO lifecycle, bell button visual states and interaction, door alert indicators, hardening (a11y, motion, i18n, overflow), notification protocols (`OSC 9` / `OSC 9;4` / `OSC 99` / `OSC 777` / `BEL`), the `ActivityNotification` model, notification text handling and security, and the notification preview/detail UI. Read this when touching: `activity-monitor.ts`, `alert-manager.ts`, `AlertManager` notification/progress paths, the alert bell or TODO pill in `Wall.tsx` (TerminalPaneHeader), alert indicators in `Door.tsx`, the `a`/`t` keyboard shortcuts, or TODO notification preview UI. Layout.md defers to this spec for all alert/TODO behavior. +- **`docs/specs/alert.md`** — Activity monitoring state machine, alert trigger/clearing rules, attention model, TODO lifecycle, bell button visual states and interaction, door alert indicators, hardening (a11y, motion, i18n, overflow), notification protocols (`OSC 9` / `OSC 9;4` / `OSC 99` / `OSC 777` / `BEL`), the `ActivityNotification` model, notification text handling and security, the notification preview/detail UI, and the Workspace union projection (ringing/todo/count). Read this when touching: `activity-monitor.ts`, `alert-manager.ts`, `AlertManager` notification/progress paths, the alert bell or TODO pill in `Wall.tsx` (TerminalPaneHeader), alert indicators in `Door.tsx`, the `a`/`t` keyboard shortcuts, or TODO notification preview UI. Layout.md defers to this spec for all alert/TODO behavior. - **`docs/specs/terminal-state.md`** — Terminal semantic state for CWD, shell prompt/editing/running/finished lifecycle, command runs, terminal title fallback, normalized semantic OSC events (`OSC 7`, `OSC 9;9`, `OSC 133`, `OSC 633`, `OSC 1337`, `OSC 0/2`), title-candidate diagnostics, header derivation, and grouping keys. Read this when touching `terminal-state.ts`, `terminal-state-store.ts`, semantic event parsing in `terminal-protocol.ts`, adapter semantic event forwarding, or derived pane/door labels. - **`docs/specs/terminal-escapes.md`** — Registry of every terminal escape sequence Dormouse parses or responds to: supported OSCs with pointers to the spec defining their behavior (alert.md or terminal-state.md), the canonical parsing-location and `pty:data` strip semantics, supported CSI sequences (`CSI > q`, DECSET/DECRST observation, kitty keyboard protocol) and replay-time CSI filtering, iTerm2 self-identification (env vars, fail-inertly rule), and known-unimplemented iTerm2 and clipboard-capable sequences. Read this when touching: OSC parsing at the PTY data boundary, CSI handlers in `terminal-protocol.ts` / `mouse-mode-observer.ts` / `terminal-report-filter.ts`, the iTerm2 identity env vars (`TERM_PROGRAM`, `LC_TERMINAL`), or adding support for a new escape sequence. -- **`docs/specs/transport.md`** — Adapter-agnostic protocol shared across VS Code, standalone, and fake adapters: PTY lifecycle (decoupled from webview), `replayChunks`/`scrollbackChunks` buffering, reconnection sequence (`dormouse:init` → `pty:list` + `pty:replay`), the full webview ↔ host message protocol, persisted-session types, and universal invariants (shell login args, scrollback trailing newline, replay drop-replies-only). Read this when touching: `pty-manager.ts`, `pty-host.js`, `pty-core.js`, `message-router.ts`, `message-types.ts`, `vscode-adapter.ts`, `fake-adapter.ts`, `reconnect.ts`, `session-save.ts`, `session-restore.ts`, `session-types.ts`, or any code crossing the webview/host boundary. -- **`docs/specs/vscode.md`** — VS Code-specific layer: hosting modes (WebviewView + WebviewPanel), extension manifest, VS Code persistence flow (`workspaceState`, `vscode.setState`, `WebviewPanelSerializer`, deactivate ordering, `mergeAlertStates` rule, `retainContextWhenHidden`), theme integration (`--vscode-*` → `--color-*` with the runtime resolver), CSP, build pipeline, and dream-architecture commands. The transport protocol it speaks (PTY lifecycle, message protocol, persisted-session types) lives in `transport.md`. Read this when touching: `extension.ts`, `webview-view-provider.ts`, `session-state.ts`, `webview-html.ts`, the theme resolver/observer in `terminal-theme.ts`, or VS Code commands and context keys. +- **`docs/specs/transport.md`** — Adapter-agnostic protocol shared across VS Code, standalone, and fake adapters: PTY lifecycle (decoupled from webview), `replayChunks`/`scrollbackChunks` buffering, reconnection sequence (`dormouse:init` → `pty:list` + `pty:replay`), the full webview ↔ host message protocol, persisted-session types (including the `PersistedWorkspace` / `PersistedWindow` container and its migration), and universal invariants (shell login args, scrollback trailing newline, replay drop-replies-only). Read this when touching: `pty-manager.ts`, `pty-host.js`, `pty-core.js`, `message-router.ts`, `message-types.ts`, `vscode-adapter.ts`, `fake-adapter.ts`, `reconnect.ts`, `session-save.ts`, `session-restore.ts`, `session-types.ts`, or any code crossing the webview/host boundary. +- **`docs/specs/vscode.md`** — VS Code-specific layer: hosting modes (WebviewView + WebviewPanel), the webview ↔ Workspace mapping and union-status reflection onto native tab icon/title and view badge, extension manifest, VS Code persistence flow (`workspaceState`, `vscode.setState`, `WebviewPanelSerializer`, deactivate ordering, `mergeAlertStates` rule, `retainContextWhenHidden`), theme integration (`--vscode-*` → `--color-*` with the runtime resolver), CSP, build pipeline, and dream-architecture commands. The transport protocol it speaks (PTY lifecycle, message protocol, persisted-session types) lives in `transport.md`. Read this when touching: `extension.ts`, `webview-view-provider.ts`, `session-state.ts`, `webview-html.ts`, the theme resolver/observer in `terminal-theme.ts`, or VS Code commands and context keys. - **`docs/specs/tutorial.md`** — Playground tutorial on the website: 3-pane layout, interactive `tut` TUI runner with three sections (keyboard navigation, alerts/TODOs, copy/paste), per-item detection wired to `WallEvent` / activity store / mouse-selection store, single-key `dormouse-tut-v3` localStorage scheme, theme picker, and FakePtyAdapter extensions (`sendOutput`, `pumpActivity`, `setInputHandler`). Read this when touching: `website/src/pages/PlaygroundDesktop.tsx`, `website/src/pages/PocketPlayground.tsx`, `website/src/components/PocketTerminalExperience.tsx`, `website/src/lib/playground-routing.ts`, the `website/src/pages/Playground.tsx` and `website/src/pages/Pocket.tsx` redirect dispatchers, `website/src/lib/tut-runner.ts`, `website/src/lib/tut-detector.ts`, `website/src/lib/tutorial-state.ts`, `website/src/lib/tut-items.ts`, `website/src/lib/tutorial-shell.ts`, `lib/src/components/ThemePicker.tsx`, `lib/src/lib/themes/`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), the `WallEvent` union, or the `onApiReady`/`onEvent`/`initialPaneIds` props on Wall. - **`docs/specs/theme.md`** — Theme system: two-layer CSS variable strategy, theme data model, conversion pipeline, bundled themes, localStorage store, shared ThemePicker component, standalone AppBar picker, runtime OpenVSX installer. Read this when touching: `lib/src/lib/themes/`, `lib/src/components/ThemePicker.tsx`, `lib/src/theme.css`, `lib/scripts/bundle-themes.mjs`, `standalone/src/AppBar.tsx` (theme picker), `standalone/src/main.tsx` (theme restore), or `website/src/components/SiteHeader.tsx` (themeAware mode). - **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned text selection, copy (Raw / Rewrapped), bracketed paste, smart URL/path extension, mouse-reporting override UI (icon + banner), and the state matrix for which layer owns mouse events. Read this when touching: `lib/src/lib/mouse-selection.ts`, `lib/src/lib/mouse-mode-observer.ts`, `lib/src/lib/clipboard.ts`, `lib/src/lib/rewrap.ts`, `lib/src/lib/selection-text.ts`, `lib/src/lib/smart-token.ts`, `lib/src/components/SelectionOverlay.tsx`, `lib/src/components/SelectionPopup.tsx`, the mouse icon / override banner / Cmd+C-V handling in `lib/src/components/Wall.tsx`, or the parser hooks + mouse listeners in `lib/src/lib/terminal-registry.ts`. diff --git a/DESIGN.md b/DESIGN.md index d75d379c..6ffcc1b1 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -221,7 +221,8 @@ The system uses **raised surfaces**, not "cards." There are no nested cards. The ### Navigation -The system has no traditional top-nav. Two surfaces play navigational roles: +The system has no traditional product top-nav. Three surfaces play navigational roles: +- **Workspace strip** (standalone app bar, top): horizontal tabs, one per Workspace, for switching between Workspaces within one window. Inactive tabs carry the union alert/TODO indicators (bell + TODO pill) borrowed from the Door vocabulary; the active tab carries none. This is standalone app-bar chrome around the Wall — see `docs/specs/layout.md` and `docs/specs/alert.md` — and its exact visual treatment is being designed in Storybook. VS Code surfaces the same status on its own native tab/badge chrome instead (`docs/specs/vscode.md`). - **Baseboard** (bottom of the app): horizontal strip of doors representing minimized panes plus chrome action buttons. Doors are the primary navigation affordance to a minimized terminal. Button style: `h-5 rounded px-1.5 text-sm font-medium font-mono text-muted` with `hover:bg-surface-raised hover:text-foreground transition-colors`. - **Pane Header (TerminalPaneHeader)**: the tab-replacing strip at the top of each pane. Tab-bar styling is stripped from dockview entirely (`--dv-tabs-and-actions-container-*` overrides); the React header IS the tab. diff --git a/docs/specs/alert.md b/docs/specs/alert.md index a81f4dee..d6c364d6 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -173,6 +173,29 @@ Clearing behavior: `attentionDismissedRing` exists so the next bell click after an attention-based dismissal opens the dialog instead of silently disabling WATCHING. +## Workspace union + +> See `docs/specs/glossary.md` for the Workspace / Window containers. + +A Workspace projects a **union status** over the Activity of the Sessions it contains: + +- `ringing` — any member Session's public `status` is `ALERT_RINGING`. +- `todo` — any member Session has `todo === true`. +- `count` — number of member Sessions owing attention (ringing or `todo`), for the numeric badge a host may show. + +Rules: + +- The union is **display-only** and derived. It never enters the per-Session Activity state machine, never fires a fresh ring, and produces no sound or notification of its own; it only mirrors member state. Every ringing/TODO transition above remains per-Session. +- Membership includes minimized (`Doored`) Sessions and, in standalone, the Sessions of inactive (unmounted) Workspaces. Their Activity entries survive minimize and unmount (glossary I2/I3), so a backgrounded Session can light up its Workspace's indicator. +- Attention suppression needs no special-casing: a per-Session ring is already suppressed while that Session is attended, so the union simply reflects whatever rings survive. + +Where the union surfaces is host-specific: + +- **Standalone:** each inactive Workspace's tab in the strip shows the union `ringing` bell and `todo` pill, reusing the Door indicator vocabulary (`bellIconClass`, the TODO pill). The **active** Workspace's tab shows no union indicator — its rings and TODOs are already visible on its own panes and doors. See `docs/specs/layout.md`. +- **VS Code:** the host reflects each Workspace's union onto the webview's native chrome — an editor tab's icon (and optionally title) and the sidebar view's numeric badge. See `docs/specs/vscode.md`. + +This spec fixes the projection and the surfacing rules; the exact visual treatment of the standalone strip is settled in the Storybook UI pass. + ## UI Contract ### Pane Header diff --git a/docs/specs/dor-cli.md b/docs/specs/dor-cli.md index 8b093bd7..8ee0a2dc 100644 --- a/docs/specs/dor-cli.md +++ b/docs/specs/dor-cli.md @@ -113,11 +113,13 @@ that owns that surface when one is available. ## Handle Model -Dormouse currently exposes one workspace and one window internally, but no -workspace/window targeting CLI flags. Each visible Pane has one selected -surface. Most surfaces are terminals; `dor iframe` introduces a non-terminal -iframe surface. User-facing `dor` commands should expose surface handles; Pane -remains layout vocabulary and compatibility-command terminology. +Dormouse supports multiple Workspaces within one Window (`docs/specs/glossary.md`): +standalone hosts several Workspaces with one active, and VS Code maps each webview +to a Workspace. The handle model therefore reserves `workspace:` and +`window:` refs. Each visible Pane has one selected surface. Most surfaces are +terminals; `dor iframe` introduces a non-terminal iframe surface. User-facing `dor` +commands should expose surface handles; Pane remains layout vocabulary and +compatibility-command terminology. Invariants: @@ -130,14 +132,21 @@ Invariants: `surface:1`, `pane:2`. - List output defaults to refs; commands that list handles accept `--id-format refs|uuids|both`. -- Workspace/window refs and target flags will be added only when Dormouse - actually supports them. +- Workspace/window refs are defined now that Dormouse supports multiple + Workspaces: `workspace:` (and `workspace:` when exactly one Workspace + matches) and `window:` select a container. A `--workspace` target flag and + `dor workspace` management commands (list / new / rename / close / switch) are + the next handles to expose; like every other command they ship with their + snapshot-tested help and the control methods that back them, not ahead of them. ## Current Implemented Commands Implemented commands call private `surface.*` control methods. `surface.list` derives its response from current Dockview panels plus terminal state/activity -snapshots where available, then returns `workspace:1` and `window:1`. +snapshots where available, then returns `workspace:1` and `window:1`. Once +`surface.list` is made Workspace-aware it tags each surface with the real +`workspace:` / `window:` membership defined above; until then it reports the +single active Workspace. Command tails captured after `--` are quoted by `dor` before the private control request is sent. `dor` detects the invoking shell from its parent process when diff --git a/docs/specs/glossary.md b/docs/specs/glossary.md index e72cda88..37edc04d 100644 --- a/docs/specs/glossary.md +++ b/docs/specs/glossary.md @@ -8,6 +8,32 @@ A **Session** is the durable unit. A Session's state lives on six orthogonal axe The **Liskov contract**: a Session is substitutable across most operations regardless of which states it currently occupies. `kill` and `rename` work universally. State-gated operations (`write`, `focus`) document their preconditions in glossary terms rather than failing silently. +## Containers + +A Session never floats free: it belongs to exactly one **Workspace**, and every Workspace belongs to one **Window**. These are containers, not Session layers — they group Sessions rather than describing a single Session's state, so they sit beside the six axes above, not on them. + +| 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) | +| **Workspace** | A named set of Sessions plus its View layout (dockview 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 Sessions it contains, and the layout that arranges them. The pre-workspace model had exactly one implicit Workspace per Window; the model now allows several. + +How many are visible at once is host-specific: + +- **Standalone** hosts many Workspaces in one Window but mounts only one at a time — the **active** Workspace. Switching mounts the target Workspace's Sessions and unmounts the previous one's; Process stays `Live` and Activity keeps flowing. Workspaces appear as a tab strip (see `docs/specs/layout.md`). +- **VS Code** maps one Workspace to one webview: the sidebar/panel `WebviewView` is the default Workspace, and each `dormouse.open` editor-tab `WebviewPanel` is an independent Workspace. Several are visible at once. PTY ownership already partitions Sessions per webview (`docs/specs/transport.md`); see `docs/specs/vscode.md`. + +### Workspace union status + +A Workspace projects a **union status** over its member Sessions' Activity (transition rules in `docs/specs/alert.md`): + +- `ringing` — any member Session has `status === 'ALERT_RINGING'`. +- `todo` — any member Session has `todo === true`. +- `count` — number of member Sessions currently owing attention (ringing or `todo`), for hosts that show a numeric badge. + +The union is **display-only**: it is derived from member Activity, never enters the Activity state machine, and never itself fires a ring. Minimized (`Doored`) and unmounted (inactive-Workspace) Sessions are included, because their Registry and Activity entries survive minimize/unmount (I2, I3). + ## Layers | Layer | Tracks | Owner | @@ -48,7 +74,7 @@ A **Session** is the tuple of its `SessionId` plus one state per layer. `Session | `Paned` | Rendered as a pane in the content area (dockview group) | | `Zoomed` | Subset of `Paned` — the selected pane is maximized | | `Doored` | Rendered as a door on the baseboard | -| `Hidden` | In neither — the webview itself is closed, or the session is mid-transition | +| `Hidden` | In neither pane nor door — the webview is closed, the Session belongs to an inactive Workspace (standalone), or the Session is mid-transition. Process and Activity are unaffected. | ### Link @@ -88,6 +114,10 @@ A user verb is an intentional action that produces a single observable change. | `rename` | Update title; layer-agnostic | | `zoom` / `unzoom` | Paned ↔ Zoomed | | `swap` | Exchange Registry entries across two View slots without touching Processes | +| `switchWorkspace` | Activate a different Workspace: mount its Sessions, unmount the previously active Workspace's (standalone). View: target's Sessions Hidden → Paned/Doored, previous active's Paned/Doored → Hidden. Process and Activity unchanged. | +| `createWorkspace` | Add a new Workspace to the Window; standalone makes it active and spawns its first pane | +| `closeWorkspace` | Remove a Workspace, `kill`-ing each member Session | +| `renameWorkspace` | Update a Workspace's `name`; touches no Session | ### System verbs @@ -123,6 +153,9 @@ A caller holding a `SessionId` can issue universal operations without branching. - I4: `Registry: Orphaned` is transient. Steady states are `Mounted` or `Disposed`. - I5: `kill` is universally valid. It always terminates at (Process: Tombstoned, Registry: Disposed, View: Hidden). - I6: `rename` is universally valid including when `Process = Exited` and `View = Doored`. +- I7: Every Session belongs to exactly one Workspace; every Workspace belongs to one Window. +- I8: `switchWorkspace` preserves Process and Activity for both Workspaces. Mounting an inactive Workspace's Sessions must not fire a fresh ring, the same rule as `mount` / `reattach` in I3. +- I9: A Workspace's union status is a pure projection of its members' Activity. It has no independent state and is destroyed with the Workspace. ## Retired / overloaded terms @@ -143,5 +176,6 @@ Use glossary names instead of these. The left column retains a meaning only wher - Layer names and state names are `PascalCase` nouns (`Paned`, `Tombstoned`). - Verbs are `camelCase` in code and lowercase in prose (`minimize`, not `Minimize`). - Event kind strings match the verb: `'minimizeChange'`, not `'detachChange'`. -- A persisted type is `Persisted` where `` is the glossary noun (`PersistedPane`, `PersistedDoor`). +- A persisted type is `Persisted` where `` is the glossary noun (`PersistedPane`, `PersistedDoor`, `PersistedWorkspace`, `PersistedWindow`). - A handle type is `State` (`ActivityState`, not `SessionUiState`). +- Container names are `PascalCase` nouns (`Workspace`, `Window`); their ids are `WorkspaceId` and `WindowId`. Container verbs keep the container as a suffix (`createWorkspace`, `switchWorkspace`) to stay distinct from the layer-agnostic Session `rename`. diff --git a/docs/specs/layout.md b/docs/specs/layout.md index a137d925..ee4390fe 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -13,6 +13,8 @@ A Session's **View** state places it in one of two containers: Transitioning between Pane and Door does not alter the Session in any way. Minimizing a pane creates a door; reattaching a door creates a pane. The terminal content, scrollback, and process state are preserved across transitions. +A **Workspace** is the named group of Sessions rendered by a single **Wall**, together with their layout (see `docs/specs/glossary.md`). A Window may hold several Workspaces. This spec covers how Workspaces are presented and switched in the standalone app — the tab strip and the one-Wall-per-Workspace mounting below. VS Code maps each Workspace to its own webview instead; see `docs/specs/vscode.md`. + ## Shell layout There are two areas: @@ -133,6 +135,28 @@ Clicking an overflow arrow reveals one door in that direction. A longer title ma Extreme case: a single door with a very long title, with more doors on both sides. Show both arrows with counts, and the single door with as much title as fits (ellipsis for the rest). +## Workspaces + +> See `docs/specs/glossary.md` for the Workspace / Window containers and `docs/specs/alert.md` for the union status. VS Code's per-webview mapping is in `docs/specs/vscode.md`. + +A **Workspace** is one Wall's worth of Sessions 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 (dockview layout) and Baseboard (doors). + +### Workspace strip (standalone) + +The standalone app bar (`standalone/src/AppBar.tsx`) carries a horizontal **workspace strip**: one tab per Workspace, living in the app bar's draggable region at the top of the window. Each tab shows the Workspace `name` and, for **inactive** Workspaces, the union `ringing` bell and `todo` pill from `docs/specs/alert.md`, reusing the Door indicator vocabulary. The **active** Workspace's tab shows no union indicator: its alerts are already visible on its own panes and doors. Exact tab visuals are settled in the Storybook UI pass. + +### Switching + +Activating another Workspace (`switchWorkspace`) mounts the target Workspace's Sessions into the Wall — rebuilding its dockview layout and reattaching its doors — and unmounts the previously active Workspace's Sessions. This reuses the `mount` / `unmount` registry ops: the Registry entry and PTY survive `unmount`, so Process stays `Live`. Because Activity keeps flowing for unmounted Sessions, an inactive Workspace's tab can begin ringing or showing TODO while the user is elsewhere. Mounting must not fire a fresh ring (glossary I8, mirroring the minimize/reattach rule I3). + +### Lifecycle + +- **Create** (`createWorkspace`): adds a new Workspace, gives it a default name (`Workspace N`), makes it active, and spawns a single fresh pane — matching the empty-state behavior in Session persistence below. +- **Close** (`closeWorkspace`): `kill`s each member Session and removes the Workspace. Closing a Workspace that contains touched Sessions confirms first (reusing the kill-confirm vocabulary); the exact confirmation surface is settled in the Storybook UI pass. The last remaining Workspace cannot be closed — there is always one active Workspace, just as there is always one visible pane (corner case #10). +- **Rename** (`renameWorkspace`): edits the Workspace `name` only. It does not touch any Session title or the per-pane inline rename. + +Concrete switch/create/close/rename keyboard shortcuts are chosen alongside the Storybook UI pass. Command mode is the natural home for them, following the tmux *window* bindings the rest of the keymap mirrors (a Dormouse Workspace is the analogue of a tmux window). + ## Modes Wall starts in `command` mode by default. Embedders may pass `initialMode="passthrough"` when the first pane is an already-running interactive surface that should receive keyboard input immediately. @@ -288,6 +312,8 @@ Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on Reac Layout, scrollback, cwd, minimized items, user-pinned titles, untouched state, and alert state are saved to persistent storage via a debounced save (500ms). Derived command/app labels shown on minimized doors are display-only and are not persisted as user-pinned titles. Saves are triggered by layout changes, panel add/remove, and a 30s periodic interval. Saves are flushed immediately on PTY exit, `pagehide`, and extension shutdown requests. +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`. + 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. On startup, recovery is priority-based: diff --git a/docs/specs/mobile-ui.md b/docs/specs/mobile-ui.md index 2e14dda5..672a7c0d 100644 --- a/docs/specs/mobile-ui.md +++ b/docs/specs/mobile-ui.md @@ -78,7 +78,8 @@ The mobile UI is split into fixed and flexible regions: The mobile session header and pane content come from `MobileWall`, a mobile composition that displays one active terminal session at a time. Desktop `Wall` -remains the tiling workspace; mobile does not expose split-pane layout. The +remains the tiling surface; mobile does not expose split-pane layout or multiple +Workspaces. The mobile wrapper owns the two selectors and the fixed-height reserve. The selector block should use one divider between the Touch and Input rows, with no divider above Touch and no divider below Input. The mobile session header should not use diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 43a41f18..c1da34ef 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -4,7 +4,7 @@ Complete reference for Dormouse's keyboard shortcuts. Shortcuts are grouped by t Dormouse has two modes: -- **Workspace mode** (a.k.a. "command" mode internally) — keys drive pane layout. +- **Command mode** — keys drive pane and workspace layout. (Internally `command`; this reference previously called it "Workspace mode," renamed to free the word "Workspace" for the pane-group container — see `docs/specs/glossary.md`.) - **Terminal mode** (a.k.a. "passthrough" mode) — keys go to the running program, except copy/paste and the mode-switch gesture. In the VS Code extension host, selected workbench chords are mirrored: the terminal receives the key, and Dormouse also runs the matching VS Code workbench command. See [the VS Code host spec](vscode.md) for the exact allowlist. @@ -13,11 +13,11 @@ In the VS Code extension host, selected workbench chords are mirrored: the termi | Key | Action | Description | |-----|--------|-------------| -| Left ⌘ → Right ⌘ (within 500 ms) | Toggle mode | Tap left Command, then right Command within 500 ms to swap between workspace and terminal mode. | +| Left ⌘ → Right ⌘ (within 500 ms) | Toggle mode | Tap left Command, then right Command within 500 ms to swap between command and terminal mode. | | Left Shift → Right Shift (within 500 ms) | Toggle mode | Same as above, but with the Shift keys. | -| `Enter` (workspace) | Enter terminal mode | Switch the selected pane into passthrough (or reattach a minimized door). | +| `Enter` (command) | Enter terminal mode | Switch the selected pane into passthrough (or reattach a minimized door). | -## Pane actions (workspace mode) +## Pane actions (command mode) | Key | Action | Description | |-----|--------|-------------| @@ -30,13 +30,17 @@ In the VS Code extension host, selected workbench chords are mirrored: the termi | `a` | Toggle alert | Dismiss or toggle the bell alert for the selected pane. | | `t` | Toggle todo | Toggle the TODO marker on or off for the selected pane. | -## Navigation (workspace mode) +## Navigation (command mode) | Key | Action | Description | |-----|--------|-------------| | `↑` / `↓` / `←` / `→` | Move selection | Move selection to the adjacent pane or door. Press the opposite direction to return. | | `⌘↑` / `⌘↓` / `⌘←` / `⌘→` (macOS)
`Ctrl`+arrows (others) | Swap terminals | Swap terminal sessions between two panes — layout and titles swap; selection follows the terminal. | +## Workspaces (command mode) + +> Workspace switch / create / close / rename shortcuts are being finalized in the Storybook UI pass, following the tmux *window* bindings the rest of the keymap mirrors. They are listed here once bound. See `docs/specs/layout.md` and `docs/specs/glossary.md` for the Workspace model. + ## Selection & drag | Key | Action | Description | @@ -64,11 +68,11 @@ On macOS, `Ctrl+C` / `Ctrl+V` pass through to the running program; only the ⌘- | `Enter` | Confirm rename | Save the new name while renaming a pane. | | `Tab` / `Shift+Tab` | Focus cycle | Cycle focus through elements of an open popover or dialog. | | Prompted character | Confirm kill | Type the character shown in the kill prompt to confirm termination. | -| `a` (alert dialog open) | Toggle alert | Same as workspace `a`. | -| `t` (alert dialog open) | Toggle todo | Same as workspace `t`. | +| `a` (alert dialog open) | Toggle alert | Same as command-mode `a`. | +| `t` (alert dialog open) | Toggle todo | Same as command-mode `t`. | ## Implementation references -- Primary keyboard handler: `lib/src/components/wall/use-wall-keyboard.ts` (workspace key dispatch, mode toggle, dialog key handlers) +- Primary keyboard handler: `lib/src/components/wall/use-wall-keyboard.ts` (command-mode key dispatch, mode toggle, dialog key handlers) - Selection popup copy bindings: `lib/src/components/SelectionPopup.tsx` - Alt-to-toggle-block selection: `lib/src/lib/terminal-mouse-router.ts` diff --git a/docs/specs/transport.md b/docs/specs/transport.md index 44afcc8a..ce36e6de 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -77,6 +77,8 @@ Non-obvious message contracts: VS Code-only workbench chord mirroring uses `dormouse:runWorkbenchCommand` from webview to host. The host validates the requested command against the allowlist in `lib/src/lib/vscode-keybindings.ts` (see [the VS Code host spec](vscode.md)) before calling `vscode.commands.executeCommand`; generic command execution over the webview boundary is not allowed. +Workspace union status (`docs/specs/alert.md`) adds no new message. Standalone computes it in-webview — the app bar's workspace strip and the Walls share one webview, so the strip reads the activity store directly. VS Code computes it host-side from the module-level `AlertManager` filtered to each router's `ownedPtyIds`, then writes it onto native chrome; the host already receives every PTY's alert state (`docs/specs/vscode.md`). + | Direction | Message | Source type | Contract | | --- | --- | --- | --- | | Webview → host | `dormouse:openExternal` | `WebviewMessage` | Request the host to open a user-confirmed external URI from an OSC 8 hyperlink. Hosts must revalidate and reject malformed, control-character-bearing, or blocked pseudo-scheme targets (`javascript:`, `data:`, `blob:`, `about:`). | @@ -90,7 +92,11 @@ The OSC parsing/stripping rules that produce `pty:data` and `terminal:semanticEv ## Persisted session types -Source of truth: `lib/src/lib/session-types.ts` defines the persisted-session interfaces (`PersistedSession` v3, `PersistedPane`, `PersistedAlertState`, `PersistedDoor`) and their v1→v2→v3 migrations. +Source of truth: `lib/src/lib/session-types.ts` defines the persisted-session interfaces (`PersistedSession`, `PersistedPane`, `PersistedAlertState`, `PersistedDoor`, `PersistedWorkspace`, `PersistedWindow`) and their migrations. + +A **Workspace** persists as a `PersistedWorkspace`: a `WorkspaceId`, a user-facing `name`, and the Workspace's `PersistedSession` (its panes, doors, and dockview layout). The standalone Window persists as a `PersistedWindow`: the ordered list of `PersistedWorkspace` plus the active `WorkspaceId`. VS Code does not use `PersistedWindow`; each webview persists exactly one `PersistedSession` — its single Workspace — through the same per-surface state API as today (`workspaceState` for the view, `vscode.setState()` per editor panel; see `docs/specs/vscode.md`). + +Versioning and migration: introducing the Window container advances the standalone top-level snapshot version. A pre-workspace `PersistedSession` (v3) migrates to a single `PersistedWorkspace` named `Workspace 1`, marked active, inside a `PersistedWindow`. The standalone reader applies this migration; a host that hands back a bare `PersistedSession` (VS Code, or legacy standalone storage) is read as one Workspace. Migrations stay additive — older shapes keep flowing v1→v2→v3→(window) without losing panes, doors, or alert state. Every saved-session entry point must pass through `readPersistedSession()`. That reader accepts both the canonical parsed object and a JSON-stringified session blob before validating/migrating; this covers host state APIs that may hand back the inner serialized JSON string. diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index f5746225..1daa2875 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -103,6 +103,21 @@ VS Code-specific consequences: PTY lifecycle, buffering, the reconnection sequence, and the full message protocol live in `docs/specs/transport.md`. +### Workspaces + +> See `docs/specs/glossary.md` for the Workspace / Window containers and `docs/specs/alert.md` for the union status. + +In VS Code, **one webview is one Workspace**. The bottom-panel `WebviewView` ("Dormouse") is the default Workspace; each `dormouse.open` editor-tab `WebviewPanel` is an independent Workspace. Unlike standalone, several Workspaces are visible at once, and VS Code — not Dormouse — owns their tabs, creation, and closing: opening a Dormouse editor tab creates a Workspace and closing the tab closes it, so Dormouse adds no create/rename/close affordances here. Each webview already contains exactly the Sessions whose PTYs its router owns (`ownedPtyIds`, `docs/specs/transport.md`), so a Workspace's Session set is the webview's owned-PTY set. + +#### Surfacing union status on native chrome + +The host computes each Workspace's union status (`ringing` / `todo` / `count`) from the module-level `AlertManager` filtered to that router's `ownedPtyIds`, and reflects it onto VS Code chrome. No new webview↔host message is needed — the host already receives every PTY's alert state. + +- **Editor tab (`WebviewPanel`):** reassign `panel.iconPath` between normal / ringing / TODO icon variants, and optionally fold the Workspace name or a status marker into `panel.title`. Both properties are writable after creation (`title: string`, `iconPath?: Uri | { light, dark }`). +- **Sidebar/panel view (`WebviewView`):** set `view.badge = { value: count, tooltip }` for a numeric attention badge on the activity-bar icon, visible even when the view is collapsed; `view.description` may carry status text. `view.title` is writable too but stays "Dormouse". `ViewBadge` is numeric only (no custom color or glyph), so the editor-tab icon swap carries the ringing-vs-TODO distinction the badge cannot. + +Reflection updates on every `AlertManager.onStateChange` for an owned PTY and on router attach/detach (a Workspace gaining or losing Sessions). When a Workspace's union is clear, the badge is set to `undefined` and the icon returns to the normal variant. Icon artwork is settled in the Storybook/asset pass. + ### Shell selection The VS Code view title contributes `Dormouse: Select Shell` and `Dormouse: New Terminal`. The selected shell name is mirrored into the `WebviewView.description`, and `dormouse:selectedShell` keeps the webview's default-shell slot current for split/spawn/restore paths. @@ -213,3 +228,4 @@ vscode.commands.executeCommand('setContext', 'dormouse.mode', 'normal'); - Commands not registered: `dormouse.newPane`, `closePane`, `nextPane`, `prevPane`, `enterTerminalMode`, `enterNormalMode`, `listSessions`, `reattach` - No status bar item showing active session count - No QuickPick for listing/reattaching PTY sessions +- Workspace union status not yet reflected onto `panel.iconPath` / `panel.title` (editor tabs) or `view.badge` / `view.description` (bottom-panel view) — the model and chrome contract are in the Workspaces section above From a56ab179bec86820a43593134d3599bed6d658a6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Jun 2026 15:30:29 -0700 Subject: [PATCH 02/11] docs: lock down the Surface model so Workspaces span terminals + browser panes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reconcile the glossary's terminal-centric "Session" model with the newer browser-surface model (iframe + agent-browser). Promote **Surface** to the canonical durable unit; a terminal Surface is a **Session** (keeps all six axes), a browser Surface is the other kind (View + Snapshot + TODO-only Activity). Adopt the hierarchy the dor CLI already commits to: Window ⊃ Workspace ⊃ Pane ⊃ Surface. - glossary: new "Panes and Surfaces" section (Pane/Surface nouns, surface kinds, per-kind axis table, containment hierarchy, surface-identity I10); Containers/union/View/Activity/verbs/invariants/retired-terms/naming all reworded over Surfaces; union carries terminal ring + (terminal|browser) TODO - alert: union projects over Surfaces; browser surfaces carry TODO, never ring - layout: conceptual model leads with Pane→Surface; switching/lifecycle and "pane IDs are session IDs" qualified to terminal Surfaces - transport: PersistedPane carries surfaceType + render params, closing the cold-restore stray-PTY bug and the resume drops-browser-panes bug - vscode: a Workspace's Surface set = owned PTYs + browser surfaces; union scoped to Surfaces (browser = TODO only) - dor-cli / dor-browser: Surface is the user-facing handle; defer to glossary - shortcuts: `t` works on any Surface; `a` is terminal-only - AGENTS: glossary index bullet documents the Surface model Spec-only; persistence/union wiring lands in the code pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 2 +- docs/specs/alert.md | 12 +++--- docs/specs/dor-browser.md | 7 +-- docs/specs/dor-cli.md | 17 +++++--- docs/specs/glossary.md | 91 +++++++++++++++++++++++++++++---------- docs/specs/layout.md | 22 +++++----- docs/specs/shortcuts.md | 4 +- docs/specs/transport.md | 4 +- docs/specs/vscode.md | 6 +-- 9 files changed, 110 insertions(+), 55 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 6384116d..4e4206af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,7 +30,7 @@ pnpm build # build lib, vscode extension, and website The primary job of a spec is to be an accurate reference for the current state of the code. Read the relevant spec before modifying a feature it covers — the spec describes invariants, edge cases, and design decisions that are not obvious from the code alone. -- **`docs/specs/glossary.md`** — Canonical vocabulary for Session states, layers (Process / Registry / View / Link / Activity / Snapshot), the Workspace / Window containers and their union status, transition verbs, and the Liskov contract on Registry APIs. Read this first. Other specs defer to it when naming a state or a verb. +- **`docs/specs/glossary.md`** — Canonical vocabulary: the **Surface** model (a Pane's durable occupant — a terminal **Session** or a **browser** surface, with a per-kind axis table), Session states and layers (Process / Registry / View / Link / Activity / Snapshot), the `Window ⊃ Workspace ⊃ Pane ⊃ Surface` containment hierarchy and the Workspace union status, transition verbs, and the Liskov contract on Registry APIs. Read this first. Other specs defer to it when naming a state, a surface kind, or a verb. - **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, minimize/reattach, inline rename, the standalone workspace strip (tabs, switching, lifecycle), session lifecycle, session persistence, and theming. Read this when touching: `Wall.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `standalone/src/AppBar.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode/workspace behavior. - **`docs/specs/dor-cli.md`** - the `dor` CLI which is prepended onto the path of every terminal that dormouse launches. - **`docs/specs/dor-browser.md`** — The unified `dor` browser surface: a single `BrowserPanel` (`surfaceType: 'browser'`) with a swappable `renderMode` (`ab-screencast` / `ab-popout` / `iframe`). Covers the `dor ab` / `dor agent-browser` screencast viewer that delegates to the user's own agent-browser install (canvas screencast, input forwarding, tabs, pop-out modal) and the `dor iframe` renderer (an `