diff --git a/TUTORIAL_MOCKUP.md b/TUTORIAL_MOCKUP.md index ea45d823d..da0193753 100644 --- a/TUTORIAL_MOCKUP.md +++ b/TUTORIAL_MOCKUP.md @@ -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 @@ -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) diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 3a5176df7..e9cb4a100 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -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.** @@ -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.** @@ -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). @@ -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 `` 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. @@ -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 diff --git a/docs/specs/layout.md b/docs/specs/layout.md index a82c3e2c1..d082e097f 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -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. diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 58e868ac3..4fcdd5ece 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -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. | diff --git a/lib/src/components/MobileWall.test.tsx b/lib/src/components/MobileWall.test.tsx index 1b7f7f2d2..d86f53022 100644 --- a/lib/src/components/MobileWall.test.tsx +++ b/lib/src/components/MobileWall.test.tsx @@ -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(), diff --git a/lib/src/components/MobileWall.tsx b/lib/src/components/MobileWall.tsx index c131f3267..d3c4f2861 100644 --- a/lib/src/components/MobileWall.tsx +++ b/lib/src/components/MobileWall.tsx @@ -12,7 +12,7 @@ import type { MobileTerminalSessionItem } from './MobileTerminalUi'; import { clearSessionTodo, DEFAULT_ACTIVITY_STATE, - dismissOrToggleAlert, + dismissSessionAlert, disposeSession, getActivitySnapshot, getOrCreateTerminal, @@ -20,7 +20,6 @@ import { setTerminalUserTitle, subscribeToActivity, subscribeToTerminalPaneState, - type SessionStatus, } from '../lib/terminal-registry'; import { buildAppTitleResolver, @@ -48,16 +47,6 @@ export interface MobileWallProps { const DEFAULT_MOBILE_SESSION: MobileWallSession = { id: 'mobile-pane' }; -const ALERT_BUTTON_LABELS: Record = { - 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[], @@ -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 ( @@ -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} > diff --git a/lib/src/components/SettingsDialog.tsx b/lib/src/components/SettingsDialog.tsx index 0d36db07d..50dbe0d1b 100644 --- a/lib/src/components/SettingsDialog.tsx +++ b/lib/src/components/SettingsDialog.tsx @@ -83,10 +83,8 @@ function describePushTargets(push: PushDevicesState, remoteControlBelow: boolean * (`lib/src/lib/shell-store.ts`), then the alarm settings * (`docs/specs/alert.md` -> Alarm settings). * - * Rules are removable here but not addable: WATCHING is keyed on a running - * command's name, so a rule is created by pressing `a` in the tab running it. - * This dialog and the bell popover are the two places a rule set on a - * since-closed Pane can be found and removed. + * Rules are removable here but not addable (`docs/specs/alert.md` -> Settings + * dialog). */ export function SettingsDialog({ onClose }: { onClose: () => void }) { const watched = useSyncExternalStore(subscribeToWatchedCommands, getWatchedCommandsSnapshot); @@ -177,8 +175,8 @@ export function SettingsDialog({ onClose }: { onClose: () => void }) { ) : (
- Nothing yet. Start a command, then press a in its tab to - alert on every tab running it. + Nothing yet. Start a command, then press a in its tab and + turn on Watch all … to alert on every tab running it.
)}
diff --git a/lib/src/components/Wall.tsx b/lib/src/components/Wall.tsx index 1aebb8424..cd003fd7f 100644 --- a/lib/src/components/Wall.tsx +++ b/lib/src/components/Wall.tsx @@ -38,7 +38,6 @@ import { clearLocalSurfaceActivity, deriveSessionLabel, disposeSession, - dismissOrToggleAlert, focusSession, refitSession, markSessionAttention, @@ -54,7 +53,6 @@ import { countRunningSessionsIn, setTerminalUserTitle, UNNAMED_PANEL_TITLE, - type SessionStatus, } from '../lib/terminal-registry'; import { buildAppTitleResolver, @@ -1895,9 +1893,6 @@ export function Wall({ exitTerminalMode(); requestKill(id); }, - onAlertButton: (id: string, displayedStatus: SessionStatus) => { - return dismissOrToggleAlert(id, displayedStatus); - }, onToggleTodo: (id: string) => { toggleSessionTodo(id); }, @@ -2223,6 +2218,7 @@ export function Wall({ enterTerminalMode, exitTerminalMode, minimizePane, + openTerminalContext: (id, origin) => contextActions.open(id, { origin }), requestKill, acceptKill, rejectKill, diff --git a/lib/src/components/WatchedCommandList.tsx b/lib/src/components/WatchedCommandList.tsx index 11023ed4f..7da6a91fc 100644 --- a/lib/src/components/WatchedCommandList.tsx +++ b/lib/src/components/WatchedCommandList.tsx @@ -7,17 +7,8 @@ import { subscribeToWatchedCommands, } from '../lib/terminal-registry'; -/** - * The app-global WATCHING rule set, with a remove control per rule - * (`docs/specs/alert.md` -> WATCHING Track). - * - * Rendered by both the bell popover and the Alarm settings dialog — the two - * places a rule set on a since-closed Pane can be found and removed. It is one - * list shown twice, so it lives here rather than in either dialog. - * - * Rules are removable but not addable: WATCHING is keyed on a running command's - * name, so creating one stays a bell click in the tab running it. - */ +/** The app-global WATCHING rule set with a remove control per rule; rules are + * created elsewhere (`docs/specs/alert.md` -> Settings dialog). */ export function WatchedCommandList() { const watched = useSyncExternalStore(subscribeToWatchedCommands, getWatchedCommandsSnapshot); if (watched.length === 0) return null; @@ -29,7 +20,7 @@ export function WatchedCommandList() { {name}