Skip to content

feat(tui): PR 2 — WizardVoice library + lint guardrail#784

Closed
kelsonpw wants to merge 1 commit into
feat/timeline-uxfrom
feat/timeline-ux-pr-2
Closed

feat(tui): PR 2 — WizardVoice library + lint guardrail#784
kelsonpw wants to merge 1 commit into
feat/timeline-uxfrom
feat/timeline-ux-pr-2

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

@kelsonpw kelsonpw commented May 15, 2026

PR 2 of 10 — Timeline UX redesign

Adds the canonical narration library (voice.*) that the entire wizard
will 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 a
single frozen voice object with 14 fields/functions:

Export Shape Example
voice.thinking string thinking through what to do next
voice.signingIn string i'll open your browser to sign you in
voice.waitingBrowser string waiting on your browser tab...
voice.signedIn(email) fn signed in as jane@acme.com
voice.detecting string looking at your codebase
voice.detected(fw, path) fn found Next.js 15 in apps/web
voice.editing(path) fn editing src/app/layout.tsx
voice.installing(pkg, mgr) fn installing @amplitude/analytics-browser with pnpm
voice.installed(pkg) fn installed @amplitude/analytics-browser
voice.wiringEvent(name) fn wiring up Signup Completed
voice.tabPrompt string what would you like me to do?
voice.done({ events }) fn all set — you're tracking 7 events in production
voice.errorRecoverable(reason) fn couldn't reach the project list. retrying...
voice.errorFatal(reason) fn 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:

  1. Pinned strings (15) — every export's exact output is snapshotted.
  2. Rules (no !, no emoji) (28) — every sample run through both checks.
  3. Lowercase rule (11) — static lines fully lowercase; functions allow uppercase only inside caller-supplied args (proper nouns, event names, paths, emails).
  4. First-person / present-tense markers (5) — regex spot-checks for i'll, i, you're, -ing openings.

eslint.config.mjs (drafted, disabled)

A new files-scoped block targeting src/ui/tui/screens/** with a
no-restricted-syntax rule that forbids the literal tokens TASK,
STEP, PHASE, INITIALIZING, EXECUTING in screen files. The whole
block is commented out with a // ENABLED IN PR 10: marker and an
explanatory 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)

  • Wiring voice.* into screens — PR 10.
  • Activating the ESLint rule — PR 10.
  • Run-timeline status-line helpers — PR 4.
  • Receipts-format helpers — separate from voice.

Verification

  • pnpm exec tsc --noEmit — clean
  • pnpm lint — clean (prettier + eslint, full repo)
  • pnpm test4313 tests pass across 288 files (incl. 59 new in voice.test.ts)
  • src/utils/wizard-abort.ts untouched

Test plan

  • pnpm exec vitest run src/ui/tui/lib/__tests__/voice.test.ts — 59 pass
  • pnpm test full suite — green
  • pnpm lint — green
  • pnpm exec tsc --noEmit — green
  • Confirm ESLint rule block is commented out (no impact on existing files)

🤖 Generated with Claude Code


Note

Low Risk
Low risk: adds a leaf-only voice string 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.ts exporting canonical wizard narration lines (voice.*) for future reuse across TUI screens.

Introduces a comprehensive vitest suite (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-syntax guardrail in eslint.config.mjs intended to later forbid hand-written status tokens in src/ui/tui/screens/** in favor of voice.*.

Reviewed by Cursor Bugbot for commit 218918e. Bugbot is set up for automated code reviews on this repo. Configure here.

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
@kelsonpw kelsonpw requested a review from a team as a code owner May 15, 2026 05:30
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

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 done template literal to output "event" when count is 1 and "events" otherwise, and added a corresponding singular-count test case.

Create PR

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.

Comment thread src/ui/tui/lib/voice.ts
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`,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 218918e. Configure here.

@kelsonpw
Copy link
Copy Markdown
Member Author

Closing — redesign track abandoned per user feedback. Not merging.

@kelsonpw kelsonpw closed this May 15, 2026
kelsonpw added a commit that referenced this pull request May 22, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant