Skip to content
Draft
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
8 changes: 4 additions & 4 deletions TUTORIAL_MOCKUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ Four beats. Beat 2 is the section; the rest is scaffolding around it.

| id | title | hint |
|---|---|---|
| `al-watch-cmd` | Alert me whenever `longtask` runs | Press `s` to start a fake `longtask`, then click that pane's bell. The bell says *Alert on all "longtask"* — you are describing a command, not flagging a tab. |
| `al-spreads` | One rule, every pane running it | The second fake task lit up too, and you never touched its bell. Any pane you open later that runs `longtask` will watch as well. |
| `al-watch-cmd` | Alert me whenever `longtask` runs | Press `s` to start a fake `longtask`, then right-click that pane's header (or press `a`) and turn on *Watch all longtask commands* — you are describing a command, not flagging a tab. |
| `al-spreads` | One rule, every pane running it | The second fake task lit up too, and you never touched it. Any pane you open later that runs `longtask` will watch as well. |

## Beat 2 — Attention

Expand Down Expand Up @@ -77,9 +77,9 @@ Both are rule-free, and both obey the same attention rule as Beat 2.

| id | title | hint |
|---|---|---|
| `al-todo-auto` | A dismissed ring leaves a TODO | Click the bell, or just select the pane. The ring goes away but a TODO stays, so an alert you waved away mid-thought does not vanish. |
| `al-todo-auto` | A dismissed ring leaves a TODO | Press `a`, or just select the pane. The ring goes away but a TODO stays, so an alert you waved away mid-thought does not vanish. |
| `al-todo-clear` | Press `Enter` in the pane to clear it | Dealing with the pane is what clears the reminder — not looking at it. |
| `al-todo-manual` | Add a TODO by hand | Press `t`, or right-click the bell. Same tag, no alert required. |
| `al-todo-manual` | Add a TODO by hand | Press `t`, or right-click the header. Same tag, no alert required. |

## Full step list (10)

Expand Down
29 changes: 11 additions & 18 deletions docs/specs/alert.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ An **await** parks on one Session until it finishes what it is doing, then repor
- **Skip a command-exit ring while a foreground command is running** (rationale).
- **Skip a WATCHING ring once output has resumed since it latched** (`outputSinceWatchingRing`), and **never stand the detector in for that flag** (rationale).
- **Never skip the bell**: an `OSC 9` is a discrete "I need input" that stays true until it is answered.
- **Consuming releases that one track's latch and nothing else** — `todo` is neither set nor cleared, no `ActivityNotification` is dropped, `attentionDismissedRing` is untouched, and `attentionId` is never set.
- **Consuming releases that one track's latch and nothing else** — `todo` is neither set nor cleared, no `ActivityNotification` is dropped, and `attentionId` is never set.

**Absorption: absorb the summons, keep the receipt.**

Expand Down Expand Up @@ -233,16 +233,14 @@ Source of truth: `applyCommandExitRinging` / `formatCommandExitBody` in `lib/src

Clearing behavior:

- Attending a ringing Session clears active rings on all three tracks, sets `todo = true`, and sets `attentionDismissedRing = true`.
- Dismissing the ring from the bell or `a` (Pane Header) sets `todo = true` and opens the alert/TODO dialog.
- Attending a ringing Session clears active rings on all three tracks and sets `todo = true`.
- Dismissing the ring from the alert button or `a` (Pane Header) sets `todo = true` and opens the terminal context. **Dismissing a Session with nothing ringing changes nothing and notifies no one.**
- Marking TODO clears any active ring and leaves the WATCHING rule in place for future cycles.
- **Must clear notification and active rings when clearing TODO, even if `todo` is already false.** Pinned by `clears a WATCHING ring before it has created a TODO` in `lib/src/lib/alert-manager.test.ts`.
- Passthrough `Enter` typed into the Session clears TODO. Command-mode `Enter` that only enters passthrough does not.
- Removing a WATCHING rule turns watching off wherever it matched and silences the WATCHING rings it raised. It does not stop the detector, nor clear protocol progress, command-exit arms, TODO, or notification detail.
- Destroying the Session clears all alert, TODO, notification, attention, protocol, and command-exit state.

`attentionDismissedRing` exists so the next bell click after an attention-based dismissal opens the dialog instead of silently editing a rule. **Only the explicit dismiss path consumes the flag** — turning WATCHING on or off, or advancing another alarm track, does not.

## Live Workspace transfer

- **Must transfer live alert state separately from persisted reminders:** track latches, episode, command watch, deferred notification, and detector history/deadlines travel in the marked transfer content. **Never read this content on cold restore.**
Expand Down Expand Up @@ -329,7 +327,7 @@ Source of truth: `watchPushRings` / `invalidatePushDeviceRefreshes` in `lib/src/
Reached from the baseboard sliders; `docs/specs/layout.md` owns placement. The alarm sections sit under the theme and shell rows; when both are hidden (VS Code owns the theme and the shells), the rule list is first and drops its section divider.

- **Must toggle only the clicked baseboard alarm setting**, as an override for that Workspace, showing the effective value. Components without a Workspace scope edit application defaults. **Must show its shared settings section for 2 seconds, then fade for 250ms**, anchored to the button and bounded by the viewport. The preview is inert, announces the resulting state, preserves keyboard focus and command dispatch, and omits test actions. Each click replaces the preview and restarts its lifetime; opening Settings or unmounting clears it. Reduced motion skips the fade. Pinned by `Baseboard.test.tsx`.
- Lists every watched command with a remove control, and **cannot add one** — WATCHING is keyed on a running command's name, so creating a rule stays a bell click / `a` press in the tab running it, and the empty state says so. With the bell dialog it is one of the two places a rule set on a since-closed Pane can be removed; both render the same `WatchedCommandList`.
- Lists every watched command with a remove control, and **cannot add one** — WATCHING is keyed on a running command's name, so creating a rule stays the terminal context of a Pane running it, and the empty state says so. **It is the only place a rule set on a since-closed Pane can be removed**, the terminal context reaching only the command its own Pane is running.
- The watcher group carries the **Defer alerts until animation stops** switch and explains that only a fully armed watcher delays terminal notifications.
- **Delays are committed on blur or `Enter`, never per keystroke** — typing `3` on the way to `30` must not briefly install a 3-second timer. They are shown in seconds; an out-of-range or empty entry snaps back to whatever the store clamped it to.
- **The push group's device line names every device a push would reach**, and otherwise says why there is none — no Burrow enrolled, nothing subscribed yet, or the server could not be asked (rationale).
Expand Down Expand Up @@ -372,19 +370,14 @@ 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 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.
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 the alert button. Placement, sizing, and width tiers belong to `docs/specs/layout.md`.

Bell interactions — one transition table, in `dismissOrToggleAlert`:
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.

- 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 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 alert button, and `a` on the selected Pane in command mode, dismiss a ringing Session and open the terminal context, whatever the status; they never edit a WATCHING rule.** The button names that action — `Dismiss alert` while ringing, `Alert settings` otherwise — not a toggle.
- **A WATCHING rule is created only in the terminal context** ("Watch all `<cmd>` commands"), which offers the row whenever a foreground command is running, and removed there or in Settings. Removing it anywhere drops it for every Session running that command.
- Right-click always opens the context. Pressing `t` toggles TODO.
- **The mobile header's alert button only dismisses** — the mobile composition has no terminal context (`docs/specs/mobile-terminal-ui.md`).

**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.

Expand All @@ -394,7 +387,7 @@ The TODO pill always displays `TODO`; remote notification text belongs in previe

Spoken-alarm delivery is much louder than the bell: a pointer-transparent treatment spans the whole terminal Pane, labelled `SPEAKING` while the engine actually speaks and `SPOKEN` — quieter, and unbounded — until the ring resolves. **`prefers-reduced-motion` keeps the strong static treatment and suppresses only the pulse**, as does `cfg.alert.ringingPaused` (rationale). The layers, their strengths, placement, and sizing belong to `docs/specs/layout.md` → Spoken-alarm overlay.

Source of truth: `AlertBell` in `lib/src/components/AlertBell.tsx`; `bellIconClass` in `lib/src/components/bell-icon-class.ts`; `latchRing` in `lib/src/lib/alert-manager.ts`; `dismissOrToggleAlert` in `lib/src/lib/session-activity-store.ts`; `lib/src/components/TodoPillBody.tsx`; `lib/src/components/wall/AlertSpeechIndicator.tsx`.
Source of truth: `AlertBell` in `lib/src/components/AlertBell.tsx`; `bellIconClass` in `lib/src/components/bell-icon-class.ts`; `latchRing` in `lib/src/lib/alert-manager.ts`; `dismissSessionAlert` in `lib/src/lib/session-activity-store.ts`; `TerminalContext` in `lib/src/components/wall/TerminalContext.tsx`; `lib/src/components/TodoPillBody.tsx`; `lib/src/components/wall/AlertSpeechIndicator.tsx`.

### Door

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ The label is the `DerivedHeader` from `deriveHeader(...)`; `docs/specs/terminal-

**Must float the context inside its source Pane with a one-rem inset on every side**, overlapping the header, with a theme-derived edge and raised shadow. Render it in the Lath leaf's overlay slot, outside the body's clipping box, so it follows the leaf's layout without remounting the helper. Keep one context per Wall. Outside pointer press and explicit close dismiss it. No separate context heading or clipboard toolbar is shown.

**Must reveal the context from the opening pointer position, clamped to its bounds, over 320ms.** Alert activation uses the alert button center; command-mode `>` uses the header's bottom-left; openings without a position use the context's top-left. Keep final layout dimensions throughout the reveal. Start helper creation, settings reads, and port scanning immediately on mount; fade mounted content, including detail dialogs, in over 140ms after 160ms. Reduced motion or disabled layout animation skips both animations and the delay.
**Must reveal the context from the opening pointer position, clamped to its bounds, over 320ms.** Alert-button activation uses the button center; command-mode `a` and `>` use the header's bottom-left; openings without a position use the context's top-left. Keep final layout dimensions throughout the reveal. Start helper creation, settings reads, and port scanning immediately on mount; fade mounted content, including detail dialogs, in over 140ms after 160ms. Reduced motion or disabled layout animation skips both animations and the delay.

**Must contract dismissals toward the opening origin over 180ms, fading content over 100ms**, starting from the current reveal when interrupted. Make the closing context inert and pause helper polling immediately; release focus without waiting for removal. Reopening cancels pending removal. Reduced motion dismisses immediately; promotion, source removal, and replacement by another context retain their immediate lifecycle transitions.

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ A focused cross-origin iframe surface swallows the gesture; the proxy shim detec
| `m` or `d` | Minimize / reattach | Stays in command mode, unlike `Enter` on a door. |
| `k` or `x` | Kill | Kills the selected pane or door behind a random-letter prompt; an untouched Surface skips it. |
| `,` | Rename | Inline rename of the selected terminal pane's title; consumed no-op on browser surfaces and doors. |
| `a` | Toggle alert | Dismiss or toggle the bell alert. Terminal Surfaces only; doors excluded. |
| `a` | Alert | Dismiss the ring if any, then open the terminal context. Terminal Surfaces only; doors excluded. |
| `t` | Toggle todo | Toggle the TODO marker on the selected Surface, terminal or browser; doors excluded. |
| `>` | Terminal context | Terminal panes only; consumed no-op on browser panes, inert on doors. |

Expand Down
2 changes: 1 addition & 1 deletion lib/src/components/MobileWall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const registry = vi.hoisted(() => ({
activitySnapshot: new Map(),
clearSessionTodo: vi.fn(),
dismissOrToggleAlert: vi.fn(),
dismissSessionAlert: vi.fn(),
disposeSession: vi.fn(),
getActivitySnapshot: vi.fn(),
getOrCreateTerminal: vi.fn(),
Expand Down
23 changes: 7 additions & 16 deletions lib/src/components/MobileWall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import type { MobileTerminalSessionItem } from './MobileTerminalUi';
import {
clearSessionTodo,
DEFAULT_ACTIVITY_STATE,
dismissOrToggleAlert,
dismissSessionAlert,
disposeSession,
getActivitySnapshot,
getOrCreateTerminal,
getTerminalPaneStateSnapshot,
setTerminalUserTitle,
subscribeToActivity,
subscribeToTerminalPaneState,
type SessionStatus,
} from '../lib/terminal-registry';
import {
buildAppTitleResolver,
Expand Down Expand Up @@ -48,16 +47,6 @@ export interface MobileWallProps {

const DEFAULT_MOBILE_SESSION: MobileWallSession = { id: 'mobile-pane' };

const ALERT_BUTTON_LABELS: Record<SessionStatus, { aria: string; tooltip: string }> = {
WATCHING_DISABLED: { aria: 'Enable watching', tooltip: 'Enable watching' },
NOTHING_TO_SHOW: { aria: 'Disable watching', tooltip: 'Disable watching' },
MIGHT_BE_BUSY: { aria: 'Disable watching', tooltip: 'Disable watching' },
BUSY: { aria: 'Disable watching', tooltip: 'Disable watching' },
MIGHT_NEED_ATTENTION: { aria: 'Disable watching', tooltip: 'Disable watching' },
ALERT_RINGING: { aria: 'Alert ringing', tooltip: 'Alert ringing' },
OSC_NOTIF_BUSY: { aria: 'Progress active', tooltip: 'Progress active' },
COMMAND_EXIT_ARMED: { aria: 'Command running', tooltip: 'Command running' },
};

export function useMobileWallSessionItems(
sessions: MobileWallSession[],
Expand Down Expand Up @@ -185,7 +174,9 @@ function MobileWallHeader({
}) {
const status = session.status ?? 'WATCHING_DISABLED';
const todoPill = useTodoPillContent(session.todo === true);
const alertButtonLabels = ALERT_BUTTON_LABELS[status];
// Mobile has no terminal context, so dismissing a ring is the button's whole
// action; it never edits a rule (`docs/specs/alert.md` -> Pane Header).
const alertButtonLabel = status === 'ALERT_RINGING' ? 'Dismiss alert' : 'Alert status';
const showTodoPill = todoPill.visible;

return (
Expand All @@ -197,9 +188,9 @@ function MobileWallHeader({
'flex h-5 min-w-5 items-center justify-center rounded transition-colors shrink-0 hover:bg-current/10',
status === 'ALERT_RINGING' ? 'text-alarm-vs-header-active' : '',
].join(' ')}
onClick={() => dismissOrToggleAlert(session.id, status)}
ariaLabel={alertButtonLabels.aria}
tooltip={alertButtonLabels.tooltip}
onClick={() => dismissSessionAlert(session.id)}
ariaLabel={alertButtonLabel}
tooltip={alertButtonLabel}
tooltipAlign="left"
dataAlertButtonFor={session.id}
>
Expand Down
Loading
Loading