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: 1 addition & 1 deletion docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`):

Expand Down
3 changes: 2 additions & 1 deletion docs/specs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 7 additions & 6 deletions docs/specs/vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<title> <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

Expand Down Expand Up @@ -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
4 changes: 4 additions & 0 deletions vscode-ext/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
});
Expand Down
20 changes: 20 additions & 0 deletions vscode-ext/src/message-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -259,6 +278,7 @@ export function attachRouter(
notification: state.notification,
attentionDismissedRing: state.attentionDismissedRing,
} satisfies ExtensionMessage);
notifyUnion();
});

return () => {
Expand Down
9 changes: 9 additions & 0 deletions vscode-ext/src/webview-view-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(() => {
Expand Down
39 changes: 39 additions & 0 deletions vscode-ext/src/workspace-chrome.ts
Original file line number Diff line number Diff line change
@@ -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(' · ') };
}
Loading