Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
16efc7c
Key WATCHING on the command a line waits on, and fix alert edge inputs
nedtwigg Sep 23, 2026
22ec725
Simplify phase-1 alert inputs; keep command-line newlines for keying
nedtwigg Sep 23, 2026
cf2013e
Give every alert one ring record with uniform TODO rules
nedtwigg Sep 24, 2026
5ac4ee2
Simplify the ring record and dispatch alert events in one ordered pass
nedtwigg Sep 24, 2026
f316b23
Replace attention with presence, focus, and acknowledgement
nedtwigg Sep 24, 2026
ea418ce
Acknowledge only at human gestures, and write user input atomically
nedtwigg Sep 24, 2026
a0ab770
Move standalone's alert manager into the sidecar
nedtwigg Sep 24, 2026
8943e71
Share one alert host and client between VS Code and standalone
nedtwigg Sep 24, 2026
6107fa2
Schedule spoken alarms and pushes in the host, gated on presence
nedtwigg Sep 24, 2026
25a2f7b
Push from the host, speak from the window, and reset acknowledgement …
nedtwigg Sep 24, 2026
e5f4cfd
Advertise iTerm2 3.6.6 and keep delivery policy across a respawn
nedtwigg Sep 24, 2026
287e833
State why 3.6.6 precisely, and share the timeline test runner
nedtwigg Sep 24, 2026
0636e35
Fix review findings across WATCHING keys, presence, delivery, and mobile
nedtwigg Sep 24, 2026
8879de7
Merge remote-tracking branch 'origin/main' into alert-improve
nedtwigg Sep 24, 2026
7fce3be
Fix review round 1: WATCHING key lexing, Client mouse input, deferral…
nedtwigg Sep 24, 2026
b0d6dd7
Make TODO a two-step: a ring never sets it, a look does
nedtwigg Sep 24, 2026
4b7ab52
Restore the "if not attended" alarm switch labels
nedtwigg Sep 24, 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
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ standalone/sidecar/iframe-proxy.cjs
standalone/sidecar/recovery.cjs
standalone/sidecar/agent-browser-host.cjs
standalone/sidecar/burrow.cjs
standalone/sidecar/alert-store.cjs
# Kept beside it: a checkout that built before the Burrow rename still holds
# the old bundle, and `bundle.resources` would ship it.
# Kept beside it: a checkout that built before the Burrow rename, or before the
# alerts joined its bundle, still holds the old bundle, and `bundle.resources`
# would ship it.
standalone/sidecar/remote-host.cjs
standalone/sidecar/alert-store.cjs
standalone/sidecar/tool-host.cjs
standalone/sidecar/git-info.cjs
standalone/sidecar/node_modules/
Expand Down
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ Doors are the pane-header indicators on the baseboard. The most signature compon
- **Dimensions:** `h-6` (24px), `min-w-[68px]`, `max-w-[220px]`; the title button pads `pl-2.5` (10px), `gap-2` between its glyph, title, and badges, ending `pr-2.5` alone or `pr-1` when the notepad button follows it. The notepad button carries the trailing inset itself (`pl-0.5 pr-2`).
- **Type:** `text-sm font-medium font-mono`.
- **Content:** leading browser-display icon cluster on a browser Surface (`size={12}` each, `gap-0.5` — a wide robot plus the presentation glyph, or the presentation glyph alone for `iframe`; named in the Door's accessible name, `docs/specs/dor-browser.md` → Browser Chrome); truncated title; optional TODO pill (`text-xs font-semibold tracking-[0.08em]`, success-tinted when flourishing); trailing notepad button (`size={12}`, `weight="fill"`) when the minimized Surface holds notes.
- **Alarm:** one 2px inset overlay (`--color-alarm-vs-door`) draws the edge for both the unlabelled ring, where it flashes once on arrival, and `SPOKEN`, which persists until the ring is attended and adds a speaker icon *beside* the TODO pill instead of evicting it. `SPEAKING` instead inverts and pulses the whole Door and takes the badge slot for its speaker-plus-label, for one utterance. Both speech states carry a speaker icon (shape, not color); all three name the state in the accessible name. The row inventory is `docs/specs/layout.md` → Alarm overlay.
- **Alarm:** one 2px inset overlay (`--color-alarm-vs-door`) draws the edge for both the unlabelled ring, where it flashes once on arrival, and `SPOKEN`, which persists until the ring clears and adds a speaker icon *beside* the TODO pill instead of evicting it. `SPEAKING` instead inverts and pulses the whole Door and takes the badge slot for its speaker-plus-label, for one utterance. Both speech states carry a speaker icon (shape, not color); all three name the state in the accessible name. The row inventory is `docs/specs/layout.md` → Alarm overlay.
- **Hover/Focus:** no decorative hover on the door itself; the focus state is conveyed by the parent pane's selection ring, not by a per-door treatment. The door is a labelled `role="group"` wrapper holding one or two buttons rather than one button — the title button reattaches, the notepad button opens the popover and does not (`docs/specs/notepad.md` → Notepad UI) — and only the notepad button takes the standard `hover:bg-current/10` wash.

### Buttons
Expand Down
307 changes: 176 additions & 131 deletions docs/specs/alert.md

Large diffs are not rendered by default.

94 changes: 82 additions & 12 deletions docs/specs/alert.rationale.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,8 +501,9 @@ Source of truth: `lib/src/host/iframe-proxy-rewrite.ts` (`iframeShim`),

- Cross-origin iframe focus blurs the parent window while `document.hasFocus()`
remains true; **focus code must distinguish this from app backgrounding**.
- Proxied frames adopt clicks from shim `pointerdown`; the raw fallback uses the
older `window.blur` + active iframe heuristic.
- Proxied frames adopt clicks from shim `pointerdown`; the raw fallback adopts
focus alone, acknowledging nothing, by the older `window.blur` + active iframe
heuristic.
- **`IframePanel` must apply `transform: translateZ(0)` to its immediate
container**, or Chromium offsets out-of-process iframe pointer events from a
far-away compositing ancestor.
Expand All @@ -511,7 +512,7 @@ Source of truth: `lib/src/host/iframe-proxy-rewrite.ts` (`iframeShim`),
- **The `allow` attribute grants no device or clipboard-read permission** —
`autoplay`, `clipboard-write`, `fullscreen` only. (rationale)

Source of truth: `lib/src/components/wall/IframePanel.tsx`, `lib/src/components/wall/use-window-focused.ts`,
Source of truth: `lib/src/components/wall/IframePanel.tsx`, `subscribeWindowFocus` in `lib/src/lib/window-focus.ts`,
`lib/src/lib/terminal-lifecycle.ts` (`registerSurfaceFocusHandle`, which
focuses/blurs the iframe element like other surfaces).

Expand Down
4 changes: 2 additions & 2 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,12 @@ Source of truth: `toolTakesOverCaller` / `toolRerunsInCaller` / `callerStillPlac
- **Must sanitize and bound the payload before retaining it.** `ToolAnnounce` / `parseToolAnnounce` and `ToolState` / `parseToolState` own the field shapes and validation limits.
- **Must reject a payload naming a version this contract does not speak.** `state` requires `v: 1`; `serve` reads an omitted `v` as 1 and refuses any other value — a future v2's rejection path.
- **Must treat an optional serve `path` as a path/query on the discovered port, never as another authority.** Accept at most 2,048 characters starting with one `/`, with no backslash, ASCII whitespace/control, or DEL; invalid paths are ignored and the default is `/`. The port still must belong to the designated Session's process tree. Live binding memory includes the path; durable saves omit it.
- **Must forward parsed announcements, state reports, and command-start resets in stream order to the owning renderer.** A start clears the previous command's announcement and unsaved state; later reports in that chunk survive. Standalone uses `terminal:protocolEvents`; VS Code uses nullable `terminal:toolAnnounce` and `terminal:toolState` scoped to the owning webview, with null clearing the corresponding record. The fake adapter applies locally.
- **Must forward parsed announcements, state reports, and command-start resets in stream order to the owning renderer.** A start clears the previous command's announcement and unsaved state; later reports in that chunk survive. Both hosts forward each parse's as one `terminal:toolEvents`, which the owning renderer applies with `recordToolEvents`; the fake adapter applies locally.
- **Must reconstruct announcements, state, and resets from raw replay without emitting replies**, preserving transferred announcements when since-mark replay has no command start, and clear the renderer record on Session disposal. Ordinary terminal announcements stay inert.
- Reserved: **Must retain `name`, `dehydrate`, and `persist` as inert parsed fields**, serving the announced-name and D1/D2 items under [Future](#future). Neither `persist: never` nor a `dehydrate` verb changes current persistence.
- Reserved: **Never assign an OSC 367 verb beyond `serve`, `state`, and `dehydrate`**; `dehydrate` belongs to D2 under [Future](#future), while existing title/progress protocols keep those roles.

Source of truth: `TerminalProtocolParser` / `collectTerminalProtocolAlerts` in `lib/src/lib/terminal-protocol.ts`; `parseToolAnnounce` in `lib/src/lib/tool-announce.ts`; `recordToolAnnounce` in `lib/src/lib/tool-announce-store.ts`; `recordToolEvents` in `lib/src/lib/tool-events.ts`; `createOwnerPtyStream` in `vscode-ext/src/message-router.ts`; `ownerStream` in `lib/src/host/remote/sidecar-entry.ts`. Tests: `lib/src/lib/tool-announce.test.ts`, `standalone/scripts/dev-agent-browser-announce.test.mjs`.
Source of truth: `TerminalProtocolParser` / `collectTerminalToolEvents` in `lib/src/lib/terminal-protocol.ts`; `parseToolAnnounce` in `lib/src/lib/tool-announce.ts`; `recordToolAnnounce` in `lib/src/lib/tool-announce-store.ts`; `recordToolEvents` in `lib/src/lib/tool-events.ts`; `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`. Tests: `lib/src/lib/tool-announce.test.ts`, `lib/src/host/remote/sidecar-entry.test.ts`, `vscode-ext/test/message-router.test.ts`, `standalone/scripts/dev-agent-browser-announce.test.mjs`.

## Unsaved changes

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ A Wall is always in exactly one input mode; `docs/specs/layout.md` owns the swit
| **Registry** | xterm.js Terminal + persistent DOM element | `lib/src/lib/terminal-registry.ts` facade over `terminal-store.ts`, `terminal-lifecycle.ts` |
| **View** | Where and how a Surface renders | `lib/src/components/Wall.tsx` plus `lib/src/components/wall/` |
| **Link** | Webview ↔ host relationship | `lib/src/lib/reconnect.ts` |
| **Activity** | Alert / attention state machine + renderer cache | `lib/src/lib/alert-manager.ts`, `lib/src/lib/session-activity-store.ts` |
| **Activity** | Alert / engagement state machine + renderer cache | `lib/src/lib/alert-manager.ts`, `lib/src/lib/session-activity-store.ts` |
| **Snapshot** | Persisted-to-disk projection: cwd, title, `untouched`, alert — never scrollback (`docs/specs/transport.md`) | `lib/src/lib/session-save.ts` / `session-restore.ts` |

A **Session** is the tuple of its `SessionId` plus one state per layer (I1).
Expand Down
9 changes: 5 additions & 4 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Source of truth: `SurfacePaneHeader` in `lib/src/components/wall/SurfacePaneHead

The baseboard (`h-7`, 28px) sits below content, with no top divider. A 2px theme-colored gap preserves pane corners; 7px horizontal padding aligns doors with panes. With no doors above 350px wide, it shows `LCmd → RCmd to enter command mode` on macOS and `LShift → RShift to enter command mode` elsewhere.

**Must group the right-hand controls**: the `N more →` overflow arrow, the host-supplied `notice` slot, then three always-present 24px square Settings buttons with 2px gaps. Their 16px icons are speaker/slashed-speaker for spoken alarms, filled `VibrateIcon`/`DeviceMobileSlashIcon` for push, and sliders for Settings. **Must expose each state through shape and `aria-pressed`.** The status buttons toggle their respective alarm settings; sliders opens Settings (`docs/specs/alert.md` → Settings dialog). **Must use the shared `chromeButton` hover treatment** for Settings and overflow buttons.
**Must group the right-hand controls**: the `N more →` overflow arrow, the host-supplied `notice` slot, then three always-present 24px square Settings buttons with 2px gaps. Their 16px icons are speaker/slashed-speaker for spoken alarms, filled `VibrateIcon`/`DeviceMobileSlashIcon` for push, and sliders for Settings. **Must expose each state through shape and `aria-pressed`.** The status buttons toggle their respective alarm settings; sliders opens Settings (`docs/specs/alert.md` → Settings dialog). **Must use the shared `chromeButton` hover treatment** for Settings and overflow buttons, except a flagged overflow arrow, which like a Door has none.

A minimized session becomes a **door**, showing its label plus the alert/TODO/speech badge cluster (`docs/specs/alert.md` → Door owns which badge shows when; both speech states also name themselves in the Door's `title` and accessible name). **A Door's label is header-derived only for a terminal-backed Surface** (`hasTerminal`); any other keeps its stored title, and a browser Door adds the display glyphs from `docs/specs/dor-browser.md` → "Browser Chrome". A Door uses the window's bottom edge as its bottom border, with left, top, and right borders taking the shared terminal top radius from `lib/src/components/design.tsx` — a mouse hole matching pane rounding. Dimensions: `min-w-[68px] max-w-[220px] h-6`.

Expand All @@ -178,7 +178,8 @@ Doors are measured in a hidden off-screen container first, then fitted:

- **Subtract the measured right cluster and its gap before fitting anything** — that space is never available to doors. Measure only its always-present part (notice + the three settings controls): **never the overflow arrow**, whose presence is an *output* of the fit.
- Add doors until no more fit, reserving room for a `N more →` button whenever items remain after the current one. **At least one door is always shown**, even if it overflows.
- If scrolled, show `← N more` on the left and/or `N more →` on the right. Overflow counts are assumed single-digit (the hidden measurement button is `9 more`).
- If scrolled, show `← N more` on the left and/or `N more →` on the right. Overflow counts are assumed single-digit (the hidden measurement button is `9 more TODO`).
- **An arrow hiding a ringing or TODO Door must say so**, since that Door is otherwise gone from the baseboard: it wears the Door shape and ground, a static `door` alarm inset while one rings, and a static TODO pill while one has TODO, and its accessible name counts them (`3 more, 1 ringing, 1 TODO`). **Every arrow must reserve the measured width of the TODO one**, so the fit never depends on which Doors an arrow hides. Pinned by `Baseboard overflow alerts` in `lib/src/components/Baseboard.test.tsx`.
- Clicking an overflow arrow reveals one door in that direction; a longer title may push more doors off the opposite side. **Must reveal the selected Door when selection or membership changes**, without overriding manual overflow scrolling (`lib/src/components/Baseboard.test.tsx`).
- Extreme case — one door with a very long title and more doors on both sides: show both arrows with counts and as much title as fits, ellipsis for the rest.

Expand Down Expand Up @@ -501,7 +502,7 @@ Startup recovery is priority-based:
**A Window plans once per Workspace off one live-PTY list**: `collectLivePtys` runs the single PTY-list round trip for the whole webview, and each Workspace takes the slice its own saved panes name, so one host answer restores N Workspaces (`docs/specs/standalone.md` → Persistence). A single-Wall host reaches the same behavior through `resumeOrRestore`.

1. **Resume** (webview recreated, retained Live or Exited PTYs): request PTY list + replay data from the platform, `resumeTerminal()` each (500ms timeout). **Saved pane and door titles are seeded back via `setTerminalUserTitle()`** (`docs/specs/transport.md`), so persisted placeholder labels never replay as user pins. If the saved session covers every retained PTY, restore the saved Lath layout when its leaf set matches and reattach saved minimized items as doors. **Never fall through to cold restore just because the visible `paneIds` list is empty** — a wall whose retained sessions are all minimized is still a resume.
2. **Restore** (app restart, cold start): the Wall's `seed` hydrates from the restored Lath layout, else falls to (3); `restoreTerminal()` per pane with its saved cwd and title, plus the single-use agent resume invocation the host captured (`docs/specs/transport.md` → "Consuming it") and, on a host whose AlertManager lives in the webview, the pane's persisted TODO through `PlatformAdapter.alertSeed`. Browser surfaces are rebuilt from their persisted params instead.
2. **Restore** (app restart, cold start): the Wall's `seed` hydrates from the restored Lath layout, else falls to (3); `restoreTerminal()` per pane with its saved cwd and title, plus the single-use agent resume invocation the host captured (`docs/specs/transport.md` → "Consuming it") and the pane's persisted TODO, which rides the spawn (`docs/specs/alert.md` → Public State). Browser surfaces are rebuilt from their persisted params instead.
3. **Fallback/manual pane creation**: with no saved layout safely applicable, add panes as splits from the previous pane.
4. **Empty state**: one new pane.

Expand Down Expand Up @@ -555,7 +556,7 @@ A store commit that empties the tree (last pane killed or minimized) triggers th

> Numbered for cross-spec reference; the numbers are stable, so append rather than renumber and leave a retired one retired.

- **#2 — A focused iframe surface is not a window blur**: it blurs the window while `document.hasFocus()` stays true, so **cross-session attention is cleared only on a *real* blur** — otherwise focusing an embed would wipe attention across the Wall.
- **#2 — A focused iframe surface is not a window blur**: it blurs the window while `document.hasFocus()` stays true, so **presence ends only on a *real* blur** (`docs/specs/alert.md` → Engagement) — otherwise focusing an embed would end it across the window. Source of truth: `subscribeWindowFocus` in `lib/src/lib/window-focus.ts`.
- **#6 — Focus-neutral surface creation (`dor ensure` / `dor iframe` / `dor ab`)**: unlike `dor split`, these open in the background without moving focus off the caller (`docs/specs/dor-cli.md`, `docs/specs/dor-browser.md`). An add never re-parents the caller's subtree or steals activation, and the create does not call `selectPane` (`settleAddSelection` returns false for a focus-neutral, non-selection-replacing add). **The one exception**: `dor iframe` / `dor ab` replacing the pane the user is *currently selected on* moves selection to the replacement, else it would dangle on the removed leaf; any other pane, or a door selection, is left untouched. Cleanup of a `dor ensure` temporary Surface follows `docs/specs/notepad.md` → "Closure"; any completed teardown preserves the caller's live selection.

## Future
Expand Down
14 changes: 11 additions & 3 deletions docs/specs/mobile-terminal-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,13 @@ Chrome rules:
radius. Order: title, secondary detail, TODO pill, minimize, kill (suppressed
by `showKillButton={false}`, as Pocket does). Both consumers wire minimize to
the Sessions reserve, not a desktop Door. A ring shows as the alarm inset on
the bar and on its session-list row, and **a tap on the terminal attends the
Session**, which is how it is dismissed here (`docs/specs/alert.md` -> Pane
Header).
the bar and on its session-list row. **A tap acknowledges the active
Session; a drag never does.** `MobileTerminalUi` tracks each press in its
capture phase, before any touch mode consumes it, and acknowledges a release
that never strayed past `RADIUS_FADE_START`. **Select mode's router consumes
a touch it owns**, so it announces one that never became a drag as
`TERMINAL_TAP_EVENT`. Both consumers route the input bar and gesture keys
through `writeUserInput` (`docs/specs/alert.md` -> Engagement).
* **Must install `useDynamicPalette` in `MobileTerminalUi`** for gesture tokens;
it never mounts the desktop `Wall`. `docs/specs/theme.md` owns publication
and the CSS baselines available before the effect runs.
Expand All @@ -55,6 +59,10 @@ Chrome rules:
CSS height and the root `h-screen` (when `fillViewport`) or `h-full`, so the
terminal region does not bounce as the OS keyboard animates (rationale).

Source of truth: `withinTapSlop` in `lib/src/components/MobileTerminalUi.tsx`;
`TERMINAL_TAP_EVENT` in `lib/src/lib/terminal-mouse-router.ts`. Pinned by
`lib/src/components/mobile-acknowledge.test.tsx`.

## Touch mode selector

The touch selector controls what a pane-content touch does. **Always visible**,
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/remote-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ The wire half, as new event names:
{ event: 'terminal.semantic'; data: TerminalSemanticEvent }
```

`terminal.resize` lets an attached viewer show its own tether state instead of rendering garbled wrap until re-attach; `terminal.semantic` frees the attached pane's header from the coalesced `directory.snapshot` cadence. Attention rides the same stage — a `terminal.attend` on touch, without which a Client cannot put a ring out (`alertAttend` is inert today).
`terminal.resize` lets an attached viewer show its own tether state instead of rendering garbled wrap until re-attach; `terminal.semantic` frees the attached pane's header from the coalesced `directory.snapshot` cadence. Acknowledgement rides the same stage — a `terminal.acknowledge` on touch, without which only a Client's keystrokes put a ring out (`alertAcknowledge` is inert today).

### 6. Graded grants and layout mutations

Expand Down
Loading
Loading