Conversation
Replaces the 6-step "run a command, get a printed instruction" tut with a full-screen alt-screen runner: each item shows a spinner while pending and becomes a green check when MouseTerm detects the action. Three sections — keyboard navigation, alerts/TODOs, copy/paste — covering 17 detectable interactions. Also fixes the existing spec's wrong "Press Escape to enter command mode" hint: command mode is entered via LShift→RShift / LMeta→RMeta dual-tap. Architecture mirrors ascii-splash-runner: TutRunner drives ANSI output through FakePtyAdapter, TutDetector wires DockviewApi events, the WallEvent stream, subscribeToActivity, and subscribeToMouseSelection to TutorialState.markComplete. Lib additions: - WallEvent.kill / WallEvent.move discriminants (additive) - FakePtyAdapter.playScenarioNow for replaying a scenario on a live pty - SCENARIO_BUSY_TASK_DEMO drives the bell BUSY → RINGING via timing alone - SCENARIO_BOXED_PARAGRAPH exercises Copy Rewrapped's frame stripping Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add "Reset progress" entry below the three sections; selecting it
opens a confirmation screen that requires typing `reset` + Enter
before clearing localStorage. Esc cancels; a wrong word shows a red
warning and clears the buffer so the user can try again.
- Replace the scenario-driven busy task on tut-target with a no-output
FakePtyAdapter.pumpActivity helper. Tut-target's bell still tilts
and rings on its own tab; the visible animation lives entirely
inside the tutorial pane: a 3-second countdown ("Fake task will
finish in 3.. → 2.. → 1..") followed by a static "Fake task
finished. Press s to start another one." Pressing s during or after
the countdown restarts it.
- Append "Press Esc to go back." to the per-section "Section
complete." footer.
- Drop the now-unused SCENARIO_BUSY_TASK_DEMO scenario.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The active-item marker was a braille spinner driven by a 100ms timer that wrote a fresh frame to xterm on every tick. That continuous output fed FakePtyAdapter's activity monitor on whichever pane hosted the runner, which kept the alert-manager out of the silence window — so the bell on that pane could never reach RINGING. Replace the active-item spinner with a static `●` glyph and only run the spinner timer during the 3-second busy-task countdown, where the animation is what the user is meant to see. After the countdown, stop the timer and settle on a static "Fake task finished" line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Tauri (`standalone/src/tauri-adapter.ts:49`) and VSCode (`vscode-ext/src/message-router.ts:33`) adapters call `alertManager.onData(id)` on every PTY data event, which is what makes the bell tilt and ring as a session does work and goes quiet. The fake adapter only fed activity from `writePty` (when no input handler was set), `playScenario`, and the explicit `pumpActivity` helper. In the playground every pane registers an input handler via `PlaygroundShellRegistry`, so the `writePty` path is bypassed; and all browser-side programs (TutorialShell echo, AsciiSplashRunner frames, TutRunner draws) emit bytes through `sendOutput`, which never touched the activity monitor. Result: alerts in the playground never tilted or rang from organic activity. Have `sendOutput` drive `alertManager.onData(id)` so the fake adapter matches the real adapters' "any data arriving from the PTY counts as activity" semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alert When ActivityMonitor's needs-attention-confirm timer fired and the user was attending the pane, the existing branch called `attend()`, which fully reset the monitor to NOTHING_TO_SHOW. With no further data arriving, no future cycle could ever ring — the alert was silently dropped. This was rarely visible in standalone/vscode because real tasks (`npm install`, builds, etc.) take longer than the 15s attention window, so attention has already expired by the time the silence threshold is reached. In the playground, where demo tasks finish in seconds, the bell consistently went BUSY → MIGHT_NEED_ATTENTION → NOTHING_TO_SHOW and never rang. Fix: when about to ring while attention is held, re-arm the same confirm timer instead of resetting. The bell parks at MIGHT_NEED_ATTENTION until either new output arrives (back to BUSY) or attention expires (rings). Updates the two tests that pinned the old swallow-and-reset behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stead Restore `ActivityMonitor.startNeedsAttentionConfirmTimer` to its original "suppress on attention" behavior — the previous "re-arm until attention expires" rewrite parked the bell at MIGHT_NEED_ATTENTION for ~12s after every short task while the attention timer ran down, which is worse than the bug it was meant to fix. The playground's busy demo runs against the alert-manager directly, so it can size itself to outlast the attention idle window. Bump `BUSY_DEMO_DURATION_MS` to `cfg.alert.userAttention + 1` and use it for both the runner's countdown UI and the `pumpActivity` call. By the time the activity-monitor's silence threshold elapses, attention has expired and the bell rings naturally with no special-casing in the alert-manager. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Locks down the rendered output for five views: the top-level menu, each of the three sections in their all-incomplete state, and one section (Keyboard navigation) in its all-complete state. Subsequent edits to section titles, item hints, the active-item glyph, the progress format, the box layout, or any color now produce a clear snapshot diff instead of silently changing what users see. Drives a real `FakePtyAdapter` rather than mocking — captures every `sendOutput` write via `onPtyData` and snapshots the last full frame (everything from the trailing `HOME + CLEAR` onward). The active-item marker is the static `●`, the spinner timer only runs during the busy demo (not exercised here), and `Date.now()` is unused outside that path, so output is fully deterministic. First snapshot-based test in the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The hint shown under the active item was rendered DIM, which read as secondary noise rather than the actionable instruction it actually is. Switch to italic and indent two columns past the title so it sits visibly inside the item it belongs to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two of the runner's prompts already wrapped the action key in cyan (`Press s to start another one.` and `Press Esc to go back.`), but every other key in the UI — section headers, the menu nav line, item hints, prose, and item titles — was rendered in the same color as surrounding text. The user has to scan for "what do I press" each time. Add a `highlightKeys()` helper that converts `` `KEY` `` markers into a cyan span using `\x1b[36m` / `\x1b[39m` (default foreground only), so the highlight composes cleanly with surrounding bold / italic / dim attributes. Apply it to every line in `render()`. Mark every hotkey in `tut-items.ts` and the runner's hardcoded strings with backticks, including: - menu nav line: `Esc`/`q`/`Enter`/`↑↓` - section headers: `Esc` - reset prompt: `Esc`, `reset`, `Enter` - per-item titles where they name a key (`Cmd/Ctrl + arrow`, `Enter`) - per-item hints: `LShift`/`RShift`/`LCmd`/`RCmd`, `-`, `|`, `Shift+\`, `m`, `k`, `a`, `s`, `t`, `arrow keys`, `ascii-splash` - section prose: `%` `"` `d` `x` Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `al-busy` hint duplicated the runner's own "Press s here to start a fake busy task." footer that always sits at the bottom of the Alert section. Shorten it to "Press s to start a fake busy task in this demo pane." so the active-item hint adds information rather than echoing the footer. The `al-ring` item had no hint, which meant the most surprising bit of behavior in the playground — that the bell suppresses ringing while you're attending the pane — was undocumented. Add a hint that spells out the rule and references `cfg.alert.userAttention` (15s) directly so it stays in sync if the threshold changes. That hint is too long to fit a single line at typical pane widths, so `renderItem` now word-wraps the active hint to the current pty width with consistent 8-column indent and italic on every continuation line. Backtick-marked keys (`s`, `Enter`, etc.) still highlight correctly inside wrapped lines because the visible-length calculation excludes them — they expand into zero-width ANSI when `highlightKeys` runs at frame time. Existing snapshots stay untouched (al-busy/al-ring aren't the active item in any snapshotted view, and the al-enable hint still fits one line at the default 80-col fake pty). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Old items just named buttons ("Click Copy Raw", "Click Copy Rewrapped");
new ones describe the action — drag-select, then paste with each copy
mode — and the hint on each one tells the user what to look for in the
pasted result ("notice how it keeps all the line-breaks. Gross!").
The cursor-icon item now explains why ascii-splash still won't paste
(animation cancels the selection) and points at lazygit as a real
program where the override actually unlocks copy.
Section prose is rewritten to one longer paragraph that explains
which programs trap the cursor and why. To keep that paragraph
readable in narrow panes, generalize the word-wrap helper that was
introduced for hints into `wrapText`, and run prose lines through it
the same way — consistent 2-column indent on every wrapped line,
backtick-marked program names (`ascii-splash`, `lazygit`) still
highlight inside the dim prose.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The fake adapter's `setDefaultScenario(SCENARIO_SHELL_PROMPT)` is the fallback for any pane that doesn't have an explicit scenario set. The playground was relying on a comment saying "tut-main has no scenario" to keep the runner pane clean — but with a default in place, spawnPty queued a delayed `user@mouseterm:~$` write that landed underneath the runner's menu and stayed there until the next render cleared it. Compounding the issue: the runner's `start()` writes ENTER_ALT and an initial render *before* the pty is registered, so those bytes get dropped by `sendOutput`'s `terminals.has(id)` guard. By the time the pty exists and the menu is repainted (via the resize subscription), xterm is no longer in alt-screen mode, so the late-arriving prompt goes straight to the visible buffer. Set `tut-main` to an explicit empty scenario so the default doesn't queue anything for that pane. User-spawned panes still get the default shell prompt, and the demo / boxed-paragraph panes still have their own scenarios. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Item ids are persistence keys, so the source-of-truth list now lives in ITEM_IDS (a `readonly` tuple) and ItemId derives from it. Detector and test call sites become typo-proof; an unrecognized id is a type error rather than silently dead code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The method was added to the adapter but never called — busy-demo animation lives entirely in TutRunner now and pumpActivity covers the real need. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces N getItem/setItem/removeItem calls (one per item id) with a single JSON-encoded payload under "mouseterm-tut-v3". One sync read on Playground mount instead of 17. Cache the localStorage handle once and ignore unknown ids on load so a stale or hand-edited payload starts fresh instead of carrying ghost ids. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TutDetector previously split its dependencies across the constructor (state) and attach() (api + stores). Folding the stores into the constructor lets Playground drop the detectorAttachRef bridge — the attach() call inside handleApiReady now reads stores via `this`. Also trim WHAT-narrating comments (file header, busy-demo countdown description, "Auto-launch the tutorial" / "Stash modules" narration). The kept comments explain non-obvious WHYs — alt-screen seeding, default-scenario suppression, BUSY_DEMO_DURATION_MS rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four files independently declared ESC/RESET/BOLD/fg/PROMPT/etc with identical byte sequences: - lib/src/lib/platform/fake-scenarios.ts - website/src/lib/tut-runner.ts - website/src/lib/tutorial-shell.ts - website/src/lib/ascii-splash-runner.ts (alt-screen toggles only) Move them into a single shared module and import from there. Keeps PROMPT visually consistent between the playground shell and canned scenarios (otherwise drift between the two copies would silently diverge the rendering). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The storage scheme in the spec described a per-item v2 key prefix and a nonexistent FakePtyAdapter.playScenarioNow method, neither of which match the code: the runner persists a single JSON-array payload under `mouseterm-tut-v3`, and the only fake-adapter helpers it uses are sendOutput / pumpActivity / setInputHandler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TutDetector seeds prev-state maps in attach(), but a pane added after attach() has no prev entry on its first store-fire. Treating undefined as a transition from "default" credited items the user didn't do — most notably al-todo-manual if a restored pane appeared with todo=true. Now the first observation of any id is recorded silently, and transition detection only runs once we have two snapshots to compare. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each `s` press in the Alert section calls pumpActivity, which starts a fresh setInterval. Holding `s` or pressing it while the demo was still counting down stacked intervals on top of each other; they all fired alertManager.onData(id) until each one's own duration timeout expired. Gate the trigger on a simple `Date.now() - busyDemoStart < duration` check so a new run is only allowed after the prior one has finished. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
startBusyDemo's setInterval kept ticking until BUSY_DEMO_DURATION_MS elapsed even after the user pressed Esc back to the menu, redrawing unrelated screens at 10 fps for a few seconds. Stop the timer in handleEscape's section→menu branch — the spinner only has anything to draw inside the Alert section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without the prev-status check, a pane already in BUSY or ALERT_RINGING at the moment its first activity event fires (restored state, or a pane spawned after attach() that arrives mid-task) would credit the user for work they did not do this session. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The +1ms offset was a clever-but-fragile timing: it relied on the busy demo finishing exactly one millisecond after user-attention expired so the bell could escape the "user is looking at this pane" suppression. Any scheduler jitter could flip the order. 250ms is still well below the spinner's display resolution (Math.ceil to seconds is unchanged) but gives a comfortable margin against clock skew. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The detector was copy-pasting the literal default — if mouse-selection adds a field, the runtime fallback drifts from the type definition. The test file already imports the upstream constant; the runtime now does too. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
processActivity / processMouse only wrote into the prev-state maps; nothing pruned ids that had left the snapshot, so killed panes leaked entries forever. Sweep the maps after each pass so they track the current snapshot exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously, if PANE_MAIN's pty never appeared within ~60 frames the RAF loop just stopped silently and the user got an empty pane with no prompt and no tut. Log a warning so the failure mode is visible in devtools. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The registry used to take a (terminalId) => boolean predicate so Playground could enumerate which panes ended at a prompt — brittle because the answer is a property of the scenario, not the id. Move that knowledge onto FakeScenario as an opt-in flag, expose adapter.scenarioEndsWithPrompt(id), and let the registry consult it directly. Drops the third constructor argument and the predicate at the Playground call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
acceptKill was the only place still calling onEventRef.current?.() directly. The whole point of fireEvent is to centralize that pattern, so use it. Required hoisting fireEvent above acceptKill so the useCallback dependency array can reference it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the requestAnimationFrame poll (60-frame budget, silent warn-and-give-up on slow paint) with a FakePtyAdapter.onPtySpawn subscription. The Playground subscribes before mounting Wall so the TerminalPane mount effect's spawn can't race past us, and falls back to a hasPty() check for the case where the pty already exists by the time we attach.
TutRunner re-renders its alt-screen frames through sendOutput on every state change. With sendOutput unconditionally feeding alertManager.onData, enabling the bell on the runner pane would tilt it on every menu redraw even though the runner is UI chrome, not a running task. Add an option that lets the runner opt out, leaving the default behavior (browser-side echo and ascii-splash frames feeding the activity monitor) intact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Esc out of the Alert section stops the spinner timer but leaves busyDemoStart set so the countdown still tracks elapsed time. On re-entry the countdown line rendered correctly but the spinner glyph was frozen at whichever frame the timer last drew. Restart the timer when the user re-opens Alert while a demo is still in flight. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When entering command mode, the detector seeds commandModePanels with the source pane so the very next arrow press credits kb-arrows. The seed previously preferred currentPaneId (set by selectionChange), which can lag or differ from dockview's active panel — the source the arrow-nav handler actually navigates from. Flip the priority so the seed matches what onDidActivePanelChange fires against, keeping currentPaneId as a fallback for tests that don't expose api.activePanel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pumpActivity returns a dispose handle that cancels its setInterval and setTimeout. The Playground was dropping the handle on the floor, so navigating away from /playground while a demo was in flight left the timers running until they expired. Track the latest handle in a ref, dispose it on unmount, and dispose any prior handle before starting a new demo as a defense-in-depth against stacked intervals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously a localStorage write failure (quota, private mode, etc.) would short-circuit the listener notification, leaving the runner UI out of sync with the in-memory completion set. Reorder so the in-memory update propagates regardless, and swallow persistence errors so they don't escape into callers. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If a pty was killed mid-duration, the interval kept calling alertManager.onData(id) for a terminal that no longer existed. Check terminals.has(id) inside the tick and cancel ourselves on miss, so the activity monitor doesn't accumulate ghost activity for removed panes. Also unified the cancel paths through a single closure so the duration timeout and external dispose can't race past each other's clears. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A Cmd/Ctrl+Arrow swap fires a `move` event then re-selects the swap target, which would grow commandModePanels to 2 and falsely mark `kb-arrows` complete on the user's first modifier-arrow press — before they ever pressed a bare arrow key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
disposeShell() deletes from this.shells, so iterating over this.shells.keys() directly relies on Map's "delete during iteration is safe" semantics. Snapshot the keys so the loop is robust to future changes in either disposeShell or Map iteration.
The previous fallback silently mutated this.screen mid-render and returned the menu — masking a bug instead of surfacing it. The sectionId is always set from SECTIONS via handleEnter, so reaching this branch indicates a real invariant violation.
spawnPty() and scenarioEndsWithPrompt() were each computing "per-id scenario, falling back to the default" inline. Pull both through a single resolveScenario() helper so future call sites inherit the same precedence rule.
The detector only touches activePanel and onDidActivePanelChange. Replace the any escape hatch with a 2-line interface so typos in either property surface at compile time.
A second attach() would register a second onDidActivePanelChange listener and double-process every activity/mouse subscription — the detector would credit each event twice and the dispose path would only tear down half of them. Throw rather than silently accumulating.
The hardcoded 800ms in Playground worked because it was below the 1500ms busyCandidateGap, but the dependency was implicit: tuning cfg.alert would silently break the demo. Tie the interval to busyCandidateGap so the relationship is explicit and self-correcting.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.