diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 0c2060e0..7a67ae09 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -177,7 +177,7 @@ Clearing behavior: > See `docs/specs/glossary.md` for the Workspace / Window containers. > -> **Partially implemented.** The union *projection* is implemented as a pure function — `computeWorkspaceUnion(surfaceIds, activitySnapshot)` in `lib/src/lib/workspace-union.ts` (stage 2b). Its **surfacing** is not built yet: the standalone strip indicators (stage 3) and the VS Code per-webview union + native-chrome reflection (`docs/specs/vscode.md`). A browser Surface's user-set `todo` already round-trips to the activity store live (stage 2a), so it is counted by the projection and shows on the surface's door today. +> **Partially implemented.** The union *projection* is a pure function — `computeWorkspaceUnion(surfaceIds, activitySnapshot)` in `lib/src/lib/workspace-union.ts` (stage 2b). **VS Code surfacing is implemented**: the editor-tab title and the bottom-panel view badge reflect the union (terminal ring/TODO only; `docs/specs/vscode.md`). Still not built: the **standalone strip indicators** (stage 3). A browser Surface's user-set `todo` round-trips to the activity store live (stage 2a), so it is counted by the projection and shows on the surface's door today. A Workspace projects a **union status** over the attention state of the Surfaces it contains (terminal Sessions and browser Surfaces alike — see `docs/specs/glossary.md`): diff --git a/docs/specs/glossary.md b/docs/specs/glossary.md index edf1c214..ad1be14e 100644 --- a/docs/specs/glossary.md +++ b/docs/specs/glossary.md @@ -82,7 +82,8 @@ This vocabulary is the target model; it lands in stages, so parts of it are spec - **Implemented, live (stage 2a)** — **Pane** and **Surface** as first-class concepts, the terminal/browser surface kinds and their per-axis participation, `surfaceType` in the persisted snapshot, and the restore/resume handling that keeps browser Surfaces (no stray PTY, saved layout preserved; `docs/specs/transport.md`). A browser Surface's user-set `todo` already round-trips to the activity store live. - **Implemented but dormant behind the `dormouse.flags.workspaces` flag (stage 2b)** — the **Workspace** and **Window** containers: `PersistedWorkspace` / `PersistedWindow` and the migration of a pre-workspace snapshot to a single "Workspace 1" (`readPersistedWindow`); the standalone persisting a `PersistedWindow` (the active Workspace's session wrapped, other Workspaces preserved — `window-persistence.ts`, wired into the standalone adapters); the in-memory workspace model and its container verbs (`createWorkspace` / `closeWorkspace` / `renameWorkspace` / `setActiveWorkspace` in `workspace-store.ts`); and the union projection (`computeWorkspaceUnion`). The flag is **off by default**, so the app persists a bare `PersistedSession` and runs exactly one implicit Workspace — behavior is byte-identical to stage 2a. -- **Not yet implemented (stages 3–4)** — the standalone workspace **strip** and the **surfacing** of the union (strip indicators; VS Code per-webview union + native-chrome reflection); the actual Wall **mount/unmount on `switchWorkspace`** (today the model switches the active id but does not yet re-render the Wall); and lifting the cap so a user can create more than one Workspace. The switching UI is stage 3, real multi-Workspace activation is stage 4. Strip/surfacing passages in the area specs are forward-looking until then. +- **Implemented for VS Code chrome** — each webview's union is reflected onto native chrome host-side: the editor-tab title gains a ` 🔔` / ` [TODO]` suffix and the bottom-panel view shows a presence badge (`docs/specs/vscode.md`). Terminal ring/TODO only (browser-surface TODO stays webview-local). Currently always-on in VS Code (the host can't read the localStorage flag). +- **Not yet implemented (stages 3–4)** — the standalone workspace **strip** and its union **surfacing** (strip indicators); the actual Wall **mount/unmount on `switchWorkspace`** (today the model switches the active id but does not yet re-render the Wall); and lifting the cap so a user can create more than one Workspace. The switching UI is stage 3, real multi-Workspace activation is stage 4. Strip/surfacing passages in the standalone-facing specs are forward-looking until then. ## Layers diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index 37241d8d..ac6ef5b5 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -107,18 +107,20 @@ PTY lifecycle, buffering, the reconnection sequence, and the full message protoc > See `docs/specs/glossary.md` for the Workspace / Window containers and `docs/specs/alert.md` for the union status. > -> **Not yet implemented (VS Code side).** The webview↔Workspace mapping is the conceptual frame; VS Code already partitions PTYs per webview. The union *projection* exists as a shared primitive (`computeWorkspaceUnion`, stage 2b), but VS Code does not yet feed it per webview or reflect the result onto native chrome (see the "Not yet implemented" bullet below). The stage-2b Window persistence container is standalone-only and does not touch VS Code, which keeps one bare `PersistedSession` per webview. Stage 2a (`surfaceType` persistence, browser-surface restore/resume) is implemented and adapter-agnostic. +> Each webview's union is computed host-side and reflected onto native chrome (see "Surfacing union status" below) — implemented. It is currently always-on: the extension host has no `localStorage` to read the standalone workspaces flag, so whether to add a host-side gate is a merge-time decision. The stage-2b Window persistence container is standalone-only and does not touch VS Code, which keeps one bare `PersistedSession` per webview. 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. A webview owns the terminal Sessions whose PTYs its router tracks (`ownedPtyIds`, `docs/specs/transport.md`) plus any browser surfaces rendered in it; together those are the Workspace's Surfaces. #### Surfacing union status on native chrome -The host computes each Workspace's native-chrome attention projection (`ringing` / `todo` / `count`) from the module-level `AlertManager`, scoped to that webview's `ownedPtyIds`. That means VS Code native chrome reflects terminal Session ring + TODO only. Browser-surface TODO remains webview-local Surface state and is not included in `panel.iconPath`, `panel.title`, or `view.badge` unless a future webview→host Surface-state channel is added; the existing `alert:state` channel is keyed by PTY-backed Session ids only. +The host computes each webview's union (`ringing` / `todo`) from the module-level `AlertManager` scoped to that router's `ownedPtyIds` (`computeWorkspaceUnion`), delivered via the `attachRouter` `onUnion` callback. Because `ownedPtyIds` are PTY-backed terminals, **VS Code chrome reflects terminal Session ring + TODO only**; a browser surface's TODO stays webview-local until a future webview→host Surface-state channel exists (the `alert:state` channel is keyed by PTY-backed Session ids). -- **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. +The two hosting primitives expose different chrome, so each uses what it supports, following the in-app ` <bell> [TODO]` pattern where possible: -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. +- **Editor tab (`WebviewPanel`):** `panel.title` gains the suffix — `Dormouse` + ` 🔔` (ringing) + ` [TODO]` (todo), both when both apply. The bell is an emoji stand-in for the in-app bell icon (a tab title is plain text); `[TODO]` is the bracketed word. `panel.iconPath` stays the Dormouse mascot. (`workspaceTitle` in `workspace-chrome.ts`.) +- **Panel view (`WebviewView`):** a presence **badge** — `view.badge.value = 1` whenever anything owes attention, `0` to clear it (ring-vs-TODO in the tooltip; `workspaceBadge`). `view.title` is *not* used: on this single-view **bottom-panel** container VS Code shows the static container title (`viewsContainers[].title`), which has no runtime API, so the title can't carry status — the badge is the only runtime indicator that surfaces. **Clear with `0`, not `undefined`:** VS Code hides a 0-value badge but does not clear one set to `undefined` on a panel container. `view.description` stays the shell name. + +Reflection updates on every owned-PTY `AlertManager.onStateChange` and on `claim` / `release` (a webview gaining or losing a PTY). Source of truth: `attachRouter` `onUnion` / `notifyUnion` in `message-router.ts`; `extension.ts` (panel title), `webview-view-provider.ts` (view badge), `workspace-chrome.ts` (formatting). ### Shell selection @@ -230,4 +232,3 @@ 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 diff --git a/vscode-ext/src/extension.ts b/vscode-ext/src/extension.ts index 860b174d..5782777c 100644 --- a/vscode-ext/src/extension.ts +++ b/vscode-ext/src/extension.ts @@ -8,6 +8,7 @@ import { getWebviewHtml } from './webview-html'; import { log } from './log'; import { mergeAlertStates, refreshSavedSessionStateFromPtys } from './session-state'; import { readPersistedSession } from '../../lib/src/lib/session-types'; +import { workspaceTitle } from './workspace-chrome'; import { resolveSelectedShell, setSelectedShellPath, getSelectedShellPath } from './shell-selection'; import type { ExtensionMessage } from './message-types'; @@ -53,6 +54,9 @@ function setupPanel( killOnDispose: true, savedSession: readPersistedSession(initialState), getSelectedShell, + // Reflect this panel's Workspace union onto the editor-tab title + // (`<title> 🔔 [TODO]`). Icon stays the Dormouse mascot. + onUnion: (union) => { panel.title = workspaceTitle(union); }, // Panels persist via vscode.setState() (per-panel, managed by VS Code). // Don't write to workspaceState — that's for the WebviewView only. }); diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index 1f14ff80..06d86451 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -9,6 +9,8 @@ import { } from '../../lib/src/lib/terminal-protocol'; import { normalizeExternalUri } from '../../lib/src/lib/external-links'; import { VSCODE_WORKBENCH_COMMANDS } from '../../lib/src/lib/vscode-keybindings'; +import { computeWorkspaceUnion, type WorkspaceUnion } from '../../lib/src/lib/workspace-union'; +import type { ActivityState } from '../../lib/src/lib/session-activity-store'; import type { TerminalSemanticEvent } from '../../lib/src/lib/terminal-state'; import type { PersistedSession } from '../../lib/src/lib/session-types'; import type { WebviewMessage, ExtensionMessage } from './message-types'; @@ -139,6 +141,10 @@ export function attachRouter( onSaveState?: (state: unknown) => void; savedSession?: PersistedSession | null; getSelectedShell?: () => { shell?: string; args?: string[] } | null; + // Called with this webview's Workspace union status whenever it changes + // (owned-PTY alert state, or a PTY claimed/released). The host reflects it + // onto native chrome (tab title / view badge). See docs/specs/vscode.md. + onUnion?: (union: WorkspaceUnion) => void; }, ): vscode.Disposable { const reconnect = options?.reconnect ?? false; @@ -156,11 +162,24 @@ export function attachRouter( function claim(id: string): void { ownedPtyIds.add(id); globalOwnedPtyIds.add(id); + notifyUnion(); } function release(id: string): void { ownedPtyIds.delete(id); globalOwnedPtyIds.delete(id); + notifyUnion(); + } + + // Project this webview's Workspace union over its owned PTYs and hand it to + // the host so it can update native chrome. Reuses the shared projection so the + // rule (only terminals ring; any surface may TODO; count owing attention) + // matches everywhere (docs/specs/alert.md). + function notifyUnion(): void { + if (!options?.onUnion) return; + const states = new Map<string, ActivityState>(); + for (const id of ownedPtyIds) states.set(id, alertManager.getState(id)); + options.onUnion(computeWorkspaceUnion(ownedPtyIds, states)); } function resolveFlushRequest(requestId: string): void { @@ -259,6 +278,7 @@ export function attachRouter( notification: state.notification, attentionDismissedRing: state.attentionDismissedRing, } satisfies ExtensionMessage); + notifyUnion(); }); return () => { diff --git a/vscode-ext/src/webview-view-provider.ts b/vscode-ext/src/webview-view-provider.ts index 6418ec4f..5f434433 100644 --- a/vscode-ext/src/webview-view-provider.ts +++ b/vscode-ext/src/webview-view-provider.ts @@ -6,6 +6,7 @@ import { getSavedSessionState, saveSessionState, mergeAlertStates } from './sess import type { ExtensionMessage } from './message-types'; import * as ptyManager from './pty-manager'; import { resolveSelectedShell } from './shell-selection'; +import { workspaceBadge } from './workspace-chrome'; import { log } from './log'; export class DormouseViewProvider implements vscode.WebviewViewProvider { @@ -77,6 +78,14 @@ export class DormouseViewProvider implements vscode.WebviewViewProvider { void saveSessionState(this.context, mergeAlertStates(state, getAlertStates())); }, getSelectedShell: () => this.selectedShell, + // Reflect this view's Workspace union onto the panel container's badge. + // On a single-view panel container VS Code shows the static container + // title, so view.title can't carry the status (docs/specs/vscode.md); the + // badge is the only runtime indicator that surfaces here. It's a presence + // flag (1 when anything owes attention). Description stays the shell name. + onUnion: (union) => { + if (this.view) this.view.badge = workspaceBadge(union); + }, }); view.onDidDispose(() => { diff --git a/vscode-ext/src/workspace-chrome.ts b/vscode-ext/src/workspace-chrome.ts new file mode 100644 index 00000000..f1131892 --- /dev/null +++ b/vscode-ext/src/workspace-chrome.ts @@ -0,0 +1,39 @@ +import * as vscode from 'vscode'; +import type { WorkspaceUnion } from '../../lib/src/lib/workspace-union'; + +const BASE_TITLE = 'Dormouse'; + +/** + * Reflect a Workspace's union status onto a webview's native chrome title, + * matching the in-app `<title> <bell> [TODO]` pattern: append ` 🔔` when any + * terminal Session is ringing and ` [TODO]` when any surface is flagged. Both + * can appear, bell first; clear → just the base title. + * + * A tab title is plain text, so the bell is the emoji stand-in for the in-app + * bell icon and TODO is the bracketed word (not an emoji). See + * `docs/specs/vscode.md`. + */ +export function workspaceTitle(union: WorkspaceUnion): string { + let title = BASE_TITLE; + if (union.ringing) title += ' 🔔'; + if (union.todo) title += ' [TODO]'; + return title; +} + +/** + * Presence badge for a panel/sidebar `WebviewView`. On a single-view panel + * container VS Code shows the static container title, so `view.title` can't + * carry status (see docs/specs/vscode.md) — the badge is the only runtime + * indicator that surfaces there. It's a presence flag, not a count: value `1` + * whenever anything owes attention (ringing or TODO), `0` to clear it. + * + * `0` (not `undefined`) is the clear value on purpose: VS Code hides a 0-value + * badge but does NOT clear one set to `undefined` on a bottom-panel container. + * The ring-vs-TODO detail lives in the tooltip. + */ +export function workspaceBadge(union: WorkspaceUnion): vscode.ViewBadge { + const parts: string[] = []; + if (union.ringing) parts.push('Ringing'); + if (union.todo) parts.push('TODO'); + return { value: parts.length > 0 ? 1 : 0, tooltip: parts.join(' · ') }; +}