feat(tui): PR 2 — WizardVoice library + lint guardrail#784
Conversation
Adds the canonical narration library used across the timeline UX redesign
and drafts (but disables) an ESLint guardrail that will activate in PR 10.
Acceptance criteria:
- [x] `voice.ts` exports 14 fields/functions (thinking, signingIn,
waitingBrowser, signedIn, detecting, detected, editing, installing,
installed, wiringEvent, tabPrompt, done, errorRecoverable,
errorFatal)
- [x] Voice rules enforced by tests: all-lowercase (proper nouns + event
names preserved), no `!`, no emoji, first-person / present-tense
- [x] Snapshot tests pin every export to its exact canonical string
- [x] ESLint guardrail drafted in `eslint.config.mjs` and commented out
with `// ENABLED IN PR 10:` marker — does not fail any existing
files
- [x] No screens wired yet (out of scope for PR 2 — PR 10 will migrate
callsites)
- [x] `src/utils/wizard-abort.ts` untouched
Verification:
- pnpm tsc/lint/test: all 4313 tests pass across 288 files
- 59 new voice tests
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed:
done()outputs ungrammatical "1 events" for singular count- Added a ternary conditional in the
donetemplate literal to output "event" when count is 1 and "events" otherwise, and added a corresponding singular-count test case.
- Added a ternary conditional in the
Or push these changes by commenting:
@cursor push a422e0dad1
Preview (a422e0dad1)
diff --git a/src/ui/tui/lib/__tests__/voice.test.ts b/src/ui/tui/lib/__tests__/voice.test.ts
--- a/src/ui/tui/lib/__tests__/voice.test.ts
+++ b/src/ui/tui/lib/__tests__/voice.test.ts
@@ -115,6 +115,12 @@
);
});
+ it('done(stats) — singular event', () => {
+ expect(voice.done({ events: 1 })).toBe(
+ "all set — you're tracking 1 event in production",
+ );
+ });
+
it('done(stats) — files arg is accepted but does not change the canonical line', () => {
expect(voice.done({ events: 7, files: 3 })).toBe(
"all set — you're tracking 7 events in production",
diff --git a/src/ui/tui/lib/voice.ts b/src/ui/tui/lib/voice.ts
--- a/src/ui/tui/lib/voice.ts
+++ b/src/ui/tui/lib/voice.ts
@@ -34,7 +34,9 @@
wiringEvent: (name: string): string => `wiring up ${name}`,
tabPrompt: 'what would you like me to do?',
done: (stats: DoneStats): string =>
- `all set — you're tracking ${stats.events} events in production`,
+ `all set — you're tracking ${stats.events} ${
+ stats.events === 1 ? 'event' : 'events'
+ } in production`,
errorRecoverable: (reason: string): string => `${reason}. retrying...`,
errorFatal: (reason: string): string =>
`i couldn't finish this. here's what to try: ${reason}`,You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 218918e. Configure here.
| wiringEvent: (name: string): string => `wiring up ${name}`, | ||
| tabPrompt: 'what would you like me to do?', | ||
| done: (stats: DoneStats): string => | ||
| `all set — you're tracking ${stats.events} events in production`, |
There was a problem hiding this comment.
done() outputs ungrammatical "1 events" for singular count
Low Severity
The done function hardcodes the plural word "events" regardless of count: voice.done({ events: 1 }) produces "all set — you're tracking 1 events in production", which is grammatically incorrect. The rest of the codebase already handles this (e.g. EventPlanFullScreen.tsx uses events.length === 1 ? '' : 's'). For a canonical narration library designed to sound natural and first-person, the singular case needs a conditional event/events form.
Reviewed by Cursor Bugbot for commit 218918e. Configure here.
|
Closing — redesign track abandoned per user feedback. Not merging. |
…806) Implements the approved RunTimeline composer for the redesigned RunScreen. Body forks on `WIZARD_NEW_UX === '1'` — legacy TabContainer path is byte-identical when the flag is unset. Acceptance criteria: - [x] Subscribes narrowly via useWizardStore (useSyncExternalStore) - [x] Voice line: spinner + latest \$statusMessages entry (ASCII fallback |/-\) - [x] Todos block (max 5) with glyphs ✓ / ❯ / ○ (UTF-8) and * / > / o (ASCII) - [x] Receipts ledger: last 5 / 3 / 2 file writes at wide / medium / narrow widths, head-truncated paths - [x] Extras row above ledger (lilac ◆ chips) — hidden at narrow - [x] Footer: elapsed Xs · \$0.YZ used; optional ◆ paused pill - [x] Width-responsive via useStdoutDimensions - [x] Outer Box uses overflow="hidden" - [x] Snapshots at 80 / 60 / 40 cols + ASCII fallback at 80 cols - [x] WIZARD_NEW_UX flag gates the new path; legacy unchanged Notes: - Helper libs `lib/voice.ts` and `lib/terminalCapabilities.ts` are inlined here because the closed-redesign-track PRs #783/#784 didn't land on main. - `\$agentTasks` (PR #801) is not on main — deferred. Today only the legacy `\$tasks` atom feeds the todo block. - The `paused` pill is exposed as a prop today; wiring Tab-to-pause to a `\$paused` atom lives in a follow-up PR. - Diff / Events / Logs overlays referenced by the hotkey rail are labelled-only this PR; key handlers land in sibling PRs.



PR 2 of 10 — Timeline UX redesign
Adds the canonical narration library (
voice.*) that the entire wizardwill route status strings through in PR 10, and drafts (but disables) an
ESLint guardrail that will activate at the same time. This PR is
intentionally leaf-only: no screens are wired yet.
Branches off
feat/timeline-ux. Sibling PR 1 (#783, terminalCapabilities +ScreenShell + StepIndicator) stays on
feat/timeline-ux-pr-1.What changes
src/ui/tui/lib/voice.ts(new)Pure leaf module — no dependencies on the rest of
src/ui/tui/. Exports asingle frozen
voiceobject with 14 fields/functions:voice.thinkingthinking through what to do nextvoice.signingIni'll open your browser to sign you invoice.waitingBrowserwaiting on your browser tab...voice.signedIn(email)signed in as jane@acme.comvoice.detectinglooking at your codebasevoice.detected(fw, path)found Next.js 15 in apps/webvoice.editing(path)editing src/app/layout.tsxvoice.installing(pkg, mgr)installing @amplitude/analytics-browser with pnpmvoice.installed(pkg)installed @amplitude/analytics-browservoice.wiringEvent(name)wiring up Signup Completedvoice.tabPromptwhat would you like me to do?voice.done({ events })all set — you're tracking 7 events in productionvoice.errorRecoverable(reason)couldn't reach the project list. retrying...voice.errorFatal(reason)i couldn't finish this. here's what to try: ...Phrasing matches
docs/design/timeline-ux.md§ "Voice library (canonical lines)" verbatim.src/ui/tui/lib/__tests__/voice.test.ts(new, 59 tests)Four describe blocks:
!, no emoji) (28) — every sample run through both checks.i'll,i,you're,-ingopenings.eslint.config.mjs(drafted, disabled)A new files-scoped block targeting
src/ui/tui/screens/**with ano-restricted-syntaxrule that forbids the literal tokensTASK,STEP,PHASE,INITIALIZING,EXECUTINGin screen files. The wholeblock is commented out with a
// ENABLED IN PR 10:marker and anexplanatory note so it does not fail any existing files yet. PR 10 will
uncomment it after every screen has migrated to
voice.*.Out of scope (deferred)
voice.*into screens — PR 10.Verification
pnpm exec tsc --noEmit— cleanpnpm lint— clean (prettier + eslint, full repo)pnpm test— 4313 tests pass across 288 files (incl. 59 new invoice.test.ts)src/utils/wizard-abort.tsuntouchedTest plan
pnpm exec vitest run src/ui/tui/lib/__tests__/voice.test.ts— 59 passpnpm testfull suite — greenpnpm lint— greenpnpm exec tsc --noEmit— green🤖 Generated with Claude Code
Note
Low Risk
Low risk: adds a leaf-only
voicestring library with tests and only comments out a future ESLint restriction, without changing runtime screen behavior yet.Overview
Adds a new leaf module
src/ui/tui/lib/voice.tsexporting canonical wizard narration lines (voice.*) for future reuse across TUI screens.Introduces a comprehensive
vitestsuite (src/ui/tui/lib/__tests__/voice.test.ts) that snapshots all voice outputs and enforces style rules (lowercase, no!, no emoji, basic first-person/present-tense checks).Drafts (but keeps commented out) an ESLint
no-restricted-syntaxguardrail ineslint.config.mjsintended to later forbid hand-written status tokens insrc/ui/tui/screens/**in favor ofvoice.*.Reviewed by Cursor Bugbot for commit 218918e. Bugbot is set up for automated code reviews on this repo. Configure here.