Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
fa374e2
Add terminal context Storybook prototype
nedtwigg Sep 5, 2026
2a953fa
Document agent workflow inside Dormouse
nedtwigg Sep 5, 2026
66ded4c
Remove redundant terminal context heading
nedtwigg Sep 5, 2026
00f886d
Label context actions and soften their accent color
nedtwigg Sep 5, 2026
dcff089
Adopt compact on-off switches across Dormouse
nedtwigg Sep 5, 2026
1fcf7f0
Merge remote-tracking branch 'origin/massive-cleanup' into new-right-…
nedtwigg Sep 5, 2026
a272a9e
Plan terminal context and helper terminal implementation
nedtwigg Sep 5, 2026
36ac54d
Implement unified terminal context and helper terminals
nedtwigg Sep 5, 2026
bcabf92
Give helper suppression, closure, and promotion one owner each
nedtwigg Sep 5, 2026
1759ef3
Mark helper PTYs once per host instead of guarding each alert call
nedtwigg Sep 5, 2026
cb0667a
State the sidecar's helper rules once each
nedtwigg Sep 5, 2026
cd5678f
Point the specs at the moved helper code
nedtwigg Sep 5, 2026
204e29f
Merge remote-tracking branch 'origin/main' into new-right-click-a
nedtwigg Sep 5, 2026
3f8e35c
Guard helper promotion races and preserve conservative lifecycle checks
nedtwigg Sep 5, 2026
ae83694
Exercise unified context in alert and title Storybook interactions
nedtwigg Sep 5, 2026
cd4b7e3
Address terminal context review and stop hidden helper polling
nedtwigg Sep 5, 2026
445fb1c
Publish helper status changes when reopening context
nedtwigg Sep 5, 2026
cb70020
Show confirmation for context popup copy actions
nedtwigg Sep 6, 2026
f6c084f
Clear context action highlights when window loses focus
nedtwigg Sep 6, 2026
81d1335
Show animated opening feedback for directory explorer
nedtwigg Sep 6, 2026
b387ef5
Reveal terminal context as a floating overlay from the pointer
nedtwigg Sep 6, 2026
9d3a858
Render the terminal context from the leaf overlay slot
nedtwigg Sep 6, 2026
ca86651
Keep context snapshots static and preserve focus during launch
nedtwigg Sep 6, 2026
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
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ One implementation map per spec: an exhaustive `Files` / `Code Map` section or s
- **`docs/specs/tiling-engine.md`** — **Lath**, the in-house headless tiling engine: pure split-tree core, never-re-parent LathHost adapter, wall store + engine, Lath-only persistence.
- **`docs/specs/alert.md`** — The Activity layer: alert tracks, attention model, TODO lifecycle, notification protocols with their sanitization rules, the Workspace union projection.
- **`docs/specs/terminal-state.md`** — Per-Session semantic state: CWD, prompt/command lifecycle, title candidates and header derivation, grouping keys.
- **`docs/specs/terminal-context.md`** — Unified terminal context and helper terminal: lifecycle, promotion, source closure, and global autorun settings.
- **`docs/specs/terminal-escapes.md`** — Registry of every escape sequence parsed, answered, or ignored, each row pointing at its owning spec. Read before touching OSC/CSI parsing.
- **`docs/specs/transport.md`** — Adapter-agnostic webview ↔ host protocol: PTY lifecycle and buffering, reconnection, message contracts, persisted-session types, the invariants every adapter honors.
- **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned selection, copy (Raw / Rewrapped), paste tiers, smart URL/path extension, the mouse-ownership state matrix.
Expand Down Expand Up @@ -124,3 +125,25 @@ in [`lib/src/theme-colors.css`](lib/src/theme-colors.css), which a host can
import on its own, and the type scale, fonts, and animation tokens in
[`lib/src/theme.css`](lib/src/theme.css). When adding or removing a color
token, update `theme-colors.css` and `design.tsx` together.

<!-- dor-skill:begin — managed by `dor skill --install`; edits inside are overwritten -->
## Running inside Dormouse

If the `DORMOUSE_SURFACE_ID` environment variable is set, this terminal is
hosted by [Dormouse](https://dormouse.sh) and the `dor` CLI is on your `PATH`.
**Run `dor skill` first and do what it says** — it teaches you to use the
terminal fully (visible panes, browser surfaces, sub-agents). Two rules are
mandatory whether or not you have read it:

- **NEVER start a long-running process — a dev server, a `--watch`, any daemon
— as a background subprocess.** It would be invisible to the user and die with
your shell. ALWAYS run it with `dor ensure -- <command>` (e.g.
`dor ensure -- npm run dev`), which puts it in a visible pane that outlives
you and is reused instead of duplicated on re-runs.
- **NEVER use a built-in, native, or bundled browser tool to open, view, or
drive a web page.** ALWAYS use `dor ab` (agent-browser) — `dor ab open
<url>`, `dor ab click @e3`, and so on — so the page renders in a Dormouse
pane the user can watch.

If `DORMOUSE_SURFACE_ID` is not set, ignore this section — `dor` is not here.
<!-- dor-skill:end -->
3 changes: 2 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ The Windows/Linux native-style window control row in the standalone app bar.

The system uses **raised surfaces**, not "cards." There are no nested cards. There is no resting card grid.
- **Raised surface** (`PopupButtonRow`, tooltips, popups): `bg-surface-raised`, `border border-border`, `rounded` (4px), `shadow-md`, `font-mono text-sm`.
- **Dialog** (`KillConfirm`, `TodoAlertDialog`): `bg-surface-raised`, `border border-border`, `rounded-lg` (8px), `shadow-lg`, generous padding (`px-6 py-4` for kill-confirm).
- **Dialog** (`KillConfirm`, the terminal context's detail dialogs): `bg-surface-raised`, `border border-border`, `rounded-lg` (8px), `shadow-lg`, generous padding (`px-6 py-4` for kill-confirm).
- **Modal** (`ThemePicker` dropdown, `ThemeDebugger`, `ThemeStoreDialog`): `bg-surface-raised`, `border`, `rounded`, `shadow-2xl`, fixed-position with viewport-clamped sizing.

**The Viewport-Bound Rule.** Anything floating over the viewport takes its height cap from `OVERLAY_MAX_HEIGHT` in `design.tsx` — `.modal` for a `ModalFrame` surface (the viewport minus `MODAL_OVERLAY_INSET` doubled), `.popover` for an anchored overlay (matching `clampOverlayPosition`'s margin). Don't hand-write a `vh`/`dvh` literal at the call site: the six that predated this token had drifted to five different budgets, and one silently shadowed its own overlay's padding. Each entry reads its own custom property first (`--overlay-max-h-modal` / `--overlay-max-h-popover`), so a story — or a host with less room than the window — can narrow one bound without touching the component. They are deliberately separate: a popover inside a modal is a DOM descendant of it, and custom properties inherit, so one shared knob would cap the dialog too. A deliberately *smaller* budget than the viewport (a context menu at `max-h-[70vh]`) is a different decision and stays at the call site.
Expand All @@ -234,6 +234,7 @@ The system uses **raised surfaces**, not "cards." There are no nested cards. The
- Used by `ThemePicker`. Style: `bg-input-bg`, `border border-input-border`, `rounded`, `font-mono`, `text-sm`.
- **Focus:** native browser focus outline; this is acceptable because the entire input lives inside a raised surface that already has `shadow-2xl` and a border.
- **Form fields inside a dialog** use the underlined pair in `design.tsx` instead, so a form mixing them reads as one: `NumericInput` for a number (filtered at the keystroke, sized in `ch`) and `TextInput` for a string (full width, `type` passed through — `type="password"` for a credential). The app has no checkbox anywhere: a boolean is an `OnOffSwitch`.
- **On/off switch:** a compact track with the thumb left when off and right when on, followed by only the current `On` / `Off` label. Off is neutral; on uses the host link accent. Its 60×24px button uses the subdued action tint and hover from `design.tsx`, with native keyboard and disabled-fieldset behavior. Nested settings text aligns through `UNDER_SWITCH_INDENT`.

### Navigation

Expand Down
16 changes: 9 additions & 7 deletions docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,21 +347,23 @@ Where it surfaces is host-specific:

### Pane Header

The header shows an alert bell, a fixed-text `TODO` pill when `todo === true`, a hover/focus notification preview when TODO has `notification`, and a dialog opened by right-click or by some left-click actions. Placement, sizing, and width tiers belong to `docs/specs/layout.md`.
The header shows an alert bell, a fixed-text `TODO` pill when `todo === true`, a hover/focus notification preview when TODO has `notification`, and the terminal context opened by right-click or by some left-click actions. Placement, sizing, and width tiers belong to `docs/specs/layout.md`.

Bell rotation follows public status; motion follows latch edges. **When a track latches, ring each mounted bell for four 800ms cycles, then hold 45° until the ring clears** (test: `runs a finite ringing burst and then holds the bell at 45 degrees` in `lib/src/components/bell-icon-class.test.ts`; rationale). **A newly mounted ringing bell may replay once without advancing `ringSeq`** (test: `replays the finite burst when a ringing presentation remounts` in `lib/src/components/AlertBell.test.tsx`; rationale). **A newly latched track replays the burst; further reports on that track only enrich its summons.** `AlertState.ringSeq` counts per-Session latches and is compared by `alertStatesEqual` (tests: `counts a second track ringing behind an already-latched one` and `does not count a track that is already ringing` in `lib/src/lib/alert-manager.test.ts`, `replaces the icon when the ring counter advances` in `lib/src/components/AlertBell.test.tsx`; rationale). **Remote Clients have no counter:** `DirectoryEntry.ringing` is an edgeless boolean, so Pocket rings on mount and holds. **The bell names the command it would act on** ("Alert on all `claude`"), not an abstract toggle — that is the scope of what a click changes.

Bell interactions — one transition table, in `dismissOrToggleAlert`:

- Left-click `ALERT_RINGING`: dismiss, create TODO if needed, open dialog.
- Left-click after `attentionDismissedRing`: consume the flag and open dialog.
- Left-click `ALERT_RINGING`: dismiss, create TODO if needed, open context.
- Left-click after `attentionDismissedRing`: consume the flag and open context.
- Otherwise, with a command running: toggle that command's WATCHING rule on or off. Turning it off drops the rule for every Session running it.
- Exception: from `OSC_NOTIF_BUSY` or `COMMAND_EXIT_ARMED` with no rule set, open the dialog instead. Those alarms need no rule, so a click must not create one by surprise, and must not clear the progress or the arm.
- With no command running: change nothing and open the dialog, which explains that alerts are per command.
- Pressing `a` on the selected Pane in command mode uses the same action. Right-click always opens the dialog.
- Exception: from `OSC_NOTIF_BUSY` or `COMMAND_EXIT_ARMED` with no rule set, open the context instead. Those alarms need no rule, so a click must not create one by surprise, and must not clear the progress or the arm.
- With no command running: change nothing and open the context, which explains that alerts are per command.
- Pressing `a` on the selected Pane in command mode uses the same action. Right-click always opens the context.
- Pressing `t` toggles TODO.

The dialog carries the TODO switch, the WATCHING rule switch for the running command, notification detail, and the same `WatchedCommandList` the Settings dialog renders — load-bearing, not decoration, for the reason under Settings dialog.
**Must keep context alert controls scoped to the source**, with TODO, running-command WATCHING, and notification detail. Settings owns the global watched-command list. **Must suppress helper alerting until promotion, including after exit**, covering bell/notification protocols, watched commands, TODO, speech, push, and attention projections; semantic command/readiness state remains active. Promotion starts ordinary alert behavior without replaying suppressed events.

Source of truth: `TerminalContext` in `lib/src/components/wall/TerminalContext.tsx`; `setHelper` in `lib/src/lib/alert-manager.ts`, which every host calls at helper spawn, listing, and promotion.

The TODO pill always displays `TODO`; remote notification text belongs in preview/detail surfaces, not inside the pill. Clicking the pill clears TODO, and on clear the pill briefly shows the success flourish before unmounting.

Expand Down
49 changes: 9 additions & 40 deletions docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,46 +144,15 @@ Source of truth: `lib/src/components/wall/use-dev-server-ports.ts`,

## Pane Context Menu Connect

The terminal pane header's context menu (`docs/specs/layout.md` → Header context
menu) lists the ports a pane's process tree binds, using the **same** per-port
URL selection as `surface.resolveOpen` (`docs/specs/dor-cli.md` → Browser Open
Target Resolution). Activating a row — click, its `1`–`9` digit accelerator, or
`Enter` — reproduces `dor ab open <url>` against the **default** key/session,
reusing or creating that session's browser surface: the wall-side mirror of the
CLI flow, not the control plane. Host-gated on `agentBrowserCommand`; without it
the rows are inert labels.

**Activation reveals its surface.** Unlike focus-neutral `dor ab`, a menu row is
the human asking to see and control that browser, so every arm of the lookup
below **must end by selecting the surface in passthrough mode**, reattaching it
first when minimized, exactly as clicking its Door chip does — including from
command mode with `>` (rationale).

**Instant create.** The click is fire-and-forget: the menu closes at once and the
pane appears **before** `agent-browser open` runs (rationale).

- The eager surface is placed synchronously and **must carry no `session`** — a
session-less `ab-screencast` pane is inert, so it cannot race the daemon boot
(rationale). It carries `key: 'default'` and the target `url`, and shows a
`Connecting to browser session…` placeholder rather than the idle
`run dor ab open <url>` line (rationale).
- `agent-browser open <url>` runs, then a best-effort `stream status`.
- **Must hand over `{session, wsPort, binaryPath}` in one params refresh**
(rationale). Failed or rejected `open` still hands over session and binary;
a rejected stream-status lookup omits only the port. Failures log into the
console after the menu closes. Pinned by `connect-port.test.ts`.

The lookup reuses before it creates: (a) a surface bound to the default session,
else (b) a still-booting session-less `key: 'default'` pane, so a double-click
doesn't spawn two panes, else (c) a fresh session-less pane. Accepted edge: a
pane persisted mid-boot restores session-less and stays a `Connecting…`
placeholder — kill it, or connect again (arm (b) reuses it).

Source of truth: `lib/src/components/wall/connect-port.ts`
(`connectPortToDefaultBrowser`, `ensureEagerSurface`), `lib/src/components/wall/use-dor-control.ts`
(`useDorControl`'s `connectPort` and `updateSurfaceParams`, shared with
`ensureAgentBrowserSurface`), `lib/src/components/Wall.tsx` (`revealSurface`), `lib/src/components/wall/port-url.ts`
(`listenerUrlsByPort`), `lib/src/components/wall/PaneHeaderContextMenu.tsx`.
**Must scan once per context opening**, using the shared per-port URL selection in `docs/specs/dor-cli.md` → Browser Open Target Resolution. Zero/one port uses an inline row; multiple ports use a selector. Failed scans are distinct from no listeners.

**Must offer System browser, Iframe, Agent browser, and Popout for the selected port**, disabling unavailable host capabilities with a reason. Opening a browser from context always preserves the source terminal, including an untouched one.

**Must reuse targets per source and port**: iframe has a separate Surface; agent screencast and popout share a Session and switch display modes. Reattach minimized targets and recreate closed ones. System browser follows the OS opener's behavior.

**Must create agent-browser Surfaces eagerly without a session**, binding the returned session only after the host launch succeeds; failures are reported in context. A launch completing after its eager Surface has closed releases its browser session. Concurrent requests for the same target are serialized.

Source of truth: `openContextPort` in `lib/src/components/Wall.tsx`; `listenerUrlsByPort` in `lib/src/components/wall/port-url.ts`; `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`.

## Display Modal And Render Swaps

Expand Down
6 changes: 6 additions & 0 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@ Source of truth: `dor/src/commands/skill.ts`, `scripts/generate-dor-skill.mjs`,
`dor/skill.md`, whose byte-identity with `dor skill` output is pinned by
`dor/test/cli-output.test.mjs`.

## Helper exclusion

**Must exclude unpromoted helpers from discovery and control**, including direct internal-id targets and helper-origin requests. Promotion assigns the ordinary public Surface ref without changing Session identity; subsequent CLI operations use ordinary Surface semantics.

Source of truth: `buildDorSurfacesInternal` in `lib/src/components/Wall.tsx`; `dispatchDorControlRequest` in `lib/src/lib/platform/dor-control-dispatch.ts`.

## Future

- **Surface a dead control channel in the UI.** A lost bind leaves one
Expand Down
6 changes: 4 additions & 2 deletions docs/specs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ The containment hierarchy `dor` handles commit to (`docs/specs/dor-cli.md`):
Window ⊃ Workspace ⊃ Pane ⊃ Surface (terminal = Session | browser)
```

**Surface identity:** a Surface's id is its Lath leaf id. A terminal Surface's *is* its `SessionId`, stable (I1); browser replacement and relaunch have different identity effects (I10).
**Surface identity:** a primary Surface's id is its Lath leaf id; a helper receives its Lath leaf only on promotion. A terminal Surface's *is* its `SessionId`, stable (I1); browser replacement and relaunch have different identity effects (I10).

## Containers

**Must keep a helper as an auxiliary terminal Surface in its source's Pane**, with a stable Session id and explicit parent association. It has no independent Lath leaf, public ref, or alerting until promotion; `docs/specs/terminal-context.md` owns its lifetime. A shown helper is `Paned` within the source body; a closed context leaves it `Hidden` and DOM-parked (`Mounted`).

Workspace and Window are containers, not Session layers — they group Surfaces rather than describing one Surface's state (containment is I7).

| Container | Holds | Owner |
Expand Down Expand Up @@ -148,7 +150,7 @@ A **Session** is the tuple of its `SessionId` plus one state per layer (I1).

| State | Meaning |
|---|---|
| `Paned` | Rendered as a pane in the content area (a Lath leaf) |
| `Paned` | Rendered in the content area: a primary Lath leaf or its shown auxiliary helper |
| `Zoomed` | Subset of `Paned` — the passthrough-focused pane is maximized; acquiring zoom gives focus, losing focus returns it to `Paned` |
| `Doored` | Rendered as a door on the baseboard. DOM survival is a rendering decision, not part of this state: browser DOM retention follows **parking** and eviction (`docs/specs/tiling-engine.md` → "Parked leaves"); a terminal Surface unmounts its element (Registry: `Orphaned`) and remounts the same xterm on reattach — nothing replays |
| `Hidden` | In neither pane nor door — webview closed or mid-transition; inactive-Workspace presentation is staged (`docs/specs/layout.md` → Future). Process and Activity unaffected. |
Expand Down
Loading