chore(preview): timeline-ux combined branch (PRs 1-10)#794
Closed
kelsonpw wants to merge 24 commits into
Closed
Conversation
Add design system + 10-PR sequenced plan for the premium TUI redesign. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reference timeline-ux.md + timeline-ux-plan.md from the project CLAUDE.md; declare hard rules that every src/ui/tui/ change must follow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First PR in the Timeline UX redesign track. Establishes the foundation primitives that later PRs will build on: - `src/ui/tui/lib/terminalCapabilities.ts` — pure capability detection (truecolor / unicode / rounded corners / interactive), with `WIZARD_FORCE_ASCII=1` opt-out. No stdout writes, safe to call repeatedly, lazy env reads so tests can mutate. - `src/ui/tui/components/StepIndicator.tsx` — generic step rail rendering `❯ ✓ ● ○` (UTF-8) or `> * o o` (ASCII). Pure props in (`steps: string[]`, `currentIndex`); no store coupling, sits alongside existing `JourneyStepper` (which stays as the wizard- specific renderer). - `src/ui/tui/components/ScreenShell.tsx` — canonical 3-region (header / body / footer) layout that composes `StepIndicator` and `HotkeyPills`. Body uses `overflow="hidden"` as a defensive guard against #779-class overdraw. - 45 tests across the three modules (20 unit + 25 snapshot at three capability profiles × three widths). No console output. Acceptance criteria: - [x] `terminalCapabilities.ts` exports `supportsTruecolor`, `supportsUnicode`, `supportsRoundedCorners`, `isInteractive` - [x] Functions are pure, side-effect free, repeatable - [x] `StepIndicator` accepts `{ steps, currentIndex }`, renders UTF-8 and ASCII profiles, uses existing palette tokens - [x] `ScreenShell` accepts `{ step, title, hotkeys, children }`, composes `HotkeyPills`, applies `overflow="hidden"` to body - [x] Snapshot tests at UTF-8 + truecolor, UTF-8 + no-color, ASCII profiles, each at 80 / 60 / 40 cols - [x] All tests pass; no console output during tests - [x] No existing screens refactored (out of scope; later PRs) - [x] No new color tokens introduced - [x] `JourneyStepper` untouched - [x] `src/utils/wizard-abort.ts` untouched Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Adds the infrastructure for the timeline-ux real hotkey rail and the
`/` command palette. SlashPalette is a self-contained component for
now — parent screens own `open` state and dispatch picked commands
through the existing `executeCommand` pipeline. Real handlers for
the new stub commands land in later PRs.
AC checklist:
- [x] ScreenHotkeyBar accepts `{ pills }`, renders inline at >=80
cols, wraps at <80, truncates with `…` at <60 keeping first 2
- [x] HotkeyPills backward-compat: re-exported from
ScreenHotkeyBar.tsx with @deprecated note
- [x] fuzzyRank: prefix > substring > subsequence, keywords supported,
empty query unchanged
- [x] SlashPalette: @inkjs/ui TextInput seeded with `/`, fuzzy list,
↑/↓ navigation, Enter dispatches, Esc closes, outer
<Box overflow="hidden"> against #779
- [x] Catalog: 13+ existing wired through onCommand, 4 stubs
(`/diff`, `/events`, `/snake`, `/resume`) flagged via kind;
stubs route through store.setCommandFeedback
- [x] Snapshot tests at 80/60/40 cols for ScreenHotkeyBar; palette
tests for closed / open-seed / no-match
- [x] Unit tests for fuzzyRank covering all scoring tiers
Verification:
- pnpm tsc / lint / test all green (290 files, 4280 tests)
- src/utils/wizard-abort.ts untouched
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the RunScreen tabs body with a single vertical Timeline view
when `WIZARD_NEW_UX=1` is set. The legacy path is byte-for-byte
unchanged when the env var is unset.
In scope
- [x] `RunTimeline.tsx` composer with narrow store selectors
- [x] `RunTimelineTodos.tsx` (top 5 tasks with ✓/❯/○ glyphs)
- [x] `RunTimelineLedger.tsx` (last 3–5 file writes, +X −Y from
`summarizeLedgerPath`, head-truncated paths)
- [x] `useAtomSelector` hook — `useSyncExternalStore` + memoized
snapshot with optional `isEqual`; ships `shallowArrayEqual`
helper for list selectors
- [x] Inline UTF-8 / `WIZARD_FORCE_ASCII=1` capability check (PR 1
not yet on `feat/timeline-ux`)
- [x] `[l]` log overlay only active under the new UX path
- [x] Outer `Box overflow="hidden"` per #779
- [x] Snapshot tests at 80 / 60 / 40 cols + append-only invariant
- [x] `useAtomSelector` unit tests for narrow subscription + snapshot
identity
Deferred (PR-body notes for reviewer)
- [ ] `agentCostUsd` footer — no cost field on `WizardSession` today
- [ ] `terminalCapabilities` integration — waits on PR 1 (#783)
- [ ] Typed extras for mcp/slack/session-replay — PostAgentStep has no
`type` discriminator; current scope surfaces lilac extras when
the router has any overlay queued
- [ ] `voice.*` integration — PR 2 (#784) not merged yet; raw status
strings for now (PR 10 sweep will replace)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ZARD_NEW_UX=1)
PR 5 / 10 of the Timeline UX redesign. Windowed, scopeable project picker
that scales to orgs with thousands of projects.
Acceptance criteria:
- [x] Windowing — never >100 Text nodes, max 50 visible rows. 5000-project
frame renders only the visible window with a "showing 50 of 5000" footer.
- [x] Filter input supports column scoping: %org, %name, %env (stackable).
Residual fuzzy-matches across all three columns.
- [x] Fuzzy ranking — inline minimal substring + position-bonus ranker (PR 3's
`fuzzyRank` is on un-merged branch; swap on merge).
- [x] `n` opens the inline new-project form when the query is empty. Esc
cancels back to the picker.
- [x] Empty state — "no matches — keep typing or press n to create one" in
Colors.muted.
- [x] Snapshot tests at 10 / 250 / 5000 project sizes. 5000-project test
asserts ≤50 visible rows AND total frame line count <100.
- [x] useScreenInput is used so Esc bubbles to parent screen for back-nav.
- [x] No legacy-screen behavior change unless gated on
`WIZARD_NEW_UX === '1'`. AuthScreen forks the project-picker step
only when the env var is set; the existing PickerMenu path is
preserved verbatim.
Deferred (will swap on merge):
- PR 1 `terminalCapabilities` — inline WIZARD_FORCE_ASCII + LANG check.
- PR 2 `voice.*` strings — plain strings; PR 10 sweeps.
- PR 3 `fuzzyRank` — minimal substring ranker inlined in ProjectPicker.tsx.
Out of scope:
- New-project API call (parent owns `onCreate`).
- Other picker callsites (Org picker, Env picker) — focused swap on the
Project step only.
…deferred)
The killer Tab-to-ask interaction lands in this PR for the UI surface and
the synthetic-pause stub. Tabbing from RunScreen (gated to
`WIZARD_NEW_UX === '1'`) opens AskBar, the user types a free-form
question, and the wizard renders a synchronous `› got it, pausing to
look at that` ack line in the timeline in the same React tick as Enter.
The 500ms-acknowledgement contract is met by strict-zero-ms synchronous
rendering — no setTimeout, no microtask gap.
The Claude Agent SDK pause hook is deferred. `agentInterrupt.ts` is a
self-contained module-state stub with `interrupt() / inject(msg) /
resume() / drainPendingInjections() / subscribe()` — the follow-up PR
hooks it into `src/lib/agent-interface.ts` once a public SDK pause
surface exists. The wizard-side ack is the user-visible "we paused"
signal regardless.
Legacy path (WIZARD_NEW_UX unset) is byte-identical to PR 5 baseline —
RunScreen renders the same TabContainer, ConsoleView still owns Tab,
no Box wrapper, no AskBar mount.
AC checklist
- [x] Tab from RunScreen opens AskBar; `$paused` flips true; "paused"
pill renders next to elapsed counter (RunScreen.tsx).
- [x] AskBar uses `@inkjs/ui` TextInput. Enter submits, Esc cancels,
↑/↓ recall history. Shift+Enter deferred (TextInput single-line).
- [x] Enter handler: trim, drop empty, push to `$askHistory` (cap 5,
dedupe consecutive), render synthetic ack inline, call
`agentInterrupt.inject()`, close AskBar, `paused` stays true
until explicit resume.
- [x] Esc cancels, closes AskBar, `agentInterrupt.resume()`.
- [x] `$askHistory` capped at 5 (`ASK_HISTORY_CAP`), consecutive
duplicates deduped at write time.
- [x] Synchronous ack contract — proved by `RunScreen.askBar.test.tsx`
asserting the ack lands in `lastFrame()` immediately after the
Enter keystroke flushes.
- [x] AskBar snapshot tests at all four states (closed, open-empty,
open-typed, after-submit).
- [x] `agentInterrupt` unit tests: 18 cases covering interrupt /
inject / resume / drain / subscribe / immutability.
- [x] `wizard-abort.ts` untouched.
- [x] No `src/lib/` modifications (agent runner deferred).
- [x] `pnpm tsc / lint / test` all green (4287 / 4287 pass).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the OAuth + manual API-key + logout UX in a per-screen
WIZARD_NEW_UX=1 gate. Underlying OAuth implementation
(client-oauth2, PKCE, token storage) untouched — this is UX
wrapping only.
Acceptance criteria
- [x] OAuth wait state: BrailleSpinner + full URL on its own line;
pairing-phrase slot rendered when session provides one
(deferred — Amplitude OAuth doesn't return one today)
- [x] `[k] paste an api key instead` always visible in the hotkey
rail during OAuth wait; press flips to inline API-key form on
the same screen (no navigation)
- [x] API-key input masked with `●` per character; `[v]` toggles
reveal; raw key never echoed to terminal stdout (no console.log)
- [x] Device-code flow auto-engage: deferred — no device-code
backend call exists today; brief explicitly allows documenting
as deferred and surfacing the structured `auth_required`
payload instead. The payload renders inline when
`session.apiKeyNotice` is set.
- [x] Auth errors surface a structured `auth_required` payload
with copy-paste-able `loginCommand` and `resumeCommand`
(rendered in AuthScreen + LoginScreen error phase)
- [x] Logout receipt:
`removed credentials for <email> from <oauth-session.json path>`
(path resolved via `getOAuthSettingsFile()`)
- [x] All changes gated behind `WIZARD_NEW_UX === '1'`. Legacy
rendering byte-for-byte unchanged when the flag is unset.
- [x] Hard constraints: no edits to `src/utils/oauth*.ts`,
`client-oauth2`, PKCE generation, `wizard-abort.ts`, or
`token-refresh.ts`. No new screens added.
Files modified
- src/ui/tui/screens/AuthScreen.tsx (+372 / -89)
- src/ui/tui/screens/LoginScreen.tsx (+18 / -0)
- src/ui/tui/screens/LogoutScreen.tsx (+30 / -6)
- src/ui/tui/screen-registry.tsx (+1 / -0) — pass `userEmail` to LogoutScreen
Tests
- src/ui/tui/screens/__tests__/AuthScreen.newUx.test.tsx (6 tests)
- src/ui/tui/screens/__tests__/LogoutScreen.newUx.test.tsx (3 tests)
- src/ui/tui/screens/__tests__/SignupEmailScreen.newUx.test.tsx (1 test)
`pnpm tsc/lint/test` green (4264/4264 passing).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ns (gated WIZARD_NEW_UX=1)
Add a single shared ExtrasPanel component that promotes MCP, Slack, and
Session Replay to first-class citizens across the wizard, with a
consistent state matrix (available / queued / installing / done /
skipped) and glyph-first rendering (color never load-bearing).
Acceptance criteria
- [x] ExtrasPanel accepts ExtraItem[] with kind + state + label/detail
- [x] State matrix: lilac+◆ for available/queued, blue+spinner for
installing, lilac+✓ for done, muted+○ for skipped; ASCII fallback
via `ascii` prop (*, o)
- [x] MCP-client detection helper (detectMcpClients) — read-only fs.
existsSync against ~/.claude.json, Cursor (mac/linux/win paths),
and Zed config dir. Never installs / never mutates.
- [x] Slack surfaces session.selectedOrgName via the `detail` field.
No OAuth wired in this PR.
- [x] Session Replay framework-gated to WEB_FRAMEWORKS (Next.js, Vue,
React Router, javascript_web). Native / backend frameworks omit
the SR row entirely.
- [x] IntroScreen returning-user variant lists extras under
WelcomeBackPanel.
- [x] EventPlanFullScreen "Also queued" footer with not-yet-complete
extras.
- [x] RunScreen renders ExtrasPanel inline in ProgressTab between the
FinalizingPanel and InlineEventPlan.
- [x] McpScreen + SlackScreen wrap their content in a rounded overlay
box. Existing behavior unchanged.
- [x] DataIngestionCheckScreen surfaces a "While you wait" panel
during the polling phase.
- [x] OutroScreen success path renders a final "Extras" receipt with
done / skipped per item.
Tests
- 10 unit tests in ExtrasPanel.test.tsx (state matrix, ascii fallback,
framework gating, MCP detection shape, empty list)
- 3 integration tests in ExtrasPanel.integration.test.tsx (OutroScreen
+ EventPlanFullScreen with WIZARD_NEW_UX=1)
- All 4267 existing tests still pass (gate defaults off → byte-identical
legacy paths)
Deferred (out of scope for this PR)
- End-to-end smoke of MCP install + Slack OAuth (needs real servers +
test app) — pre-launch follow-up.
- terminalCapabilities import (PR 1 not on base) — inlined ascii prop.
- voice.* integration (PR 2 not on base).
- Actual install actions for SR — detection + surfacing only.
Constraints honoured
- wizard-abort.ts untouched
- No MCP/Slack/Session Replay config files modified (read-only fs
probes for detection)
- All new behavior gated on WIZARD_NEW_UX === '1'
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…IZARD_NEW_UX=1)
Hardens edge cases for the Timeline UX redesign. All changes are gated
on `WIZARD_NEW_UX === '1'` — legacy paths are unchanged.
- [x] Returning-user welcome: IntroScreen swaps the legacy 3-option
checkpoint picker for a bordered summary (last run age, last
step, framework, org, events wired) + 4 hotkey options
([r] Resume / [s] Start fresh / [m] Install MCP /
[c] Connect Slack).
- [x] OutageBanner: new one-line strip pinned above the JourneyStepper
in App.tsx. Renders only when `degraded` (lilac + ⚠) or `down`
(red + ✗); glyph + label always present so color is never the
only signal. Module-scoped 5-minute TTL cache. Default fetcher
delegates to `checkAmplitudeOverallHealth`.
- [x] OutroScreen error variant: always surfaces structured
`Sign in: npx @amplitude/wizard login` (when `promptLogin`) and
`Resume: npx @amplitude/wizard` hints — same actionable copy a
CI/agent-mode operator gets from `emitAuthRequired` NDJSON.
- [x] OutageScreen rewraps its existing content with the new
OutageBanner.
- [x] ActivationOptionsScreen: token-validity flip deferred (no
session-level `tokenExpiresAt`); documented inline.
- [x] No changes to checkpoint format or `WizardSession` schema.
- [x] `wizard-abort.ts` untouched.
Tests: 14 new (OutageBanner ×6, IntroScreen.returningUser ×3,
OutroScreen.variants ×5). Full suite green (4268 tests, 290 files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final PR in the Timeline UX redesign stack (PRs 1-9 still un-merged into
`feat/timeline-ux`, so this PR ships only the tractable parts: the
flag-flip helper, the ESLint guardrail, and the contract tests).
AC checkboxes
-------------
- [x] `isNewUxEnabled()` helper at `src/ui/tui/lib/newUx.ts`
(new is default; `WIZARD_OLD_UX=1` is the explicit opt-out)
- [x] Contract test (`newUx.test.ts`) covers unset / "1" / "" / other
- [x] ESLint `no-restricted-syntax` block scoped to
`src/ui/tui/screens/**`, matching the literal status-shout
vocabulary (TASK | STEP | PHASE | INITIALIZING | EXECUTING) in
string literals, template strings, and JSX text
- [x] Zero pre-existing violations on this base (`pnpm exec eslint`
clean) — verified rule fires via synthetic probe
- [x] CLAUDE.md design-system addendum already on base (PR 0)
- [x] `WIZARD_NEW_UX=1` retained as a no-op alias (docs still work)
- [x] `src/utils/wizard-abort.ts` untouched
- [x] `pnpm exec tsc --noEmit` green
- [x] `pnpm test` green (288 files, 4258 tests)
Deferred (call-outs in PR body)
-------------------------------
- Voice sweep across screens — needs PR 2's `voice.ts` on base
- Replace `process.env.WIZARD_NEW_UX === '1'` checks with
`isNewUxEnabled()` — one-line find-replace per file, runs as part of
PRs 1-9 landing into `feat/timeline-ux`
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "refreshes the MCP Authorization header when the token rotates between
attempts" test was timing out at its 30s deadline on Node 20 CI. Root
cause: `await import('./agent-runner.js')` inside `runAgent` was hanging
for the entire 70_000ms `vi.advanceTimersByTimeAsync(...)` window — on
Node 20 + vitest 4 with fake timers active, the dynamic-import resolver
only completed when the advance window itself finished. Node 22 / 24
drained the same import chain quickly, which is why those Node lanes
passed and 20 didn't.
Fix:
- Pre-import `'../agent-runner.js'` before `vi.useFakeTimers()` runs, so
the in-run dynamic import is a cache hit (one microtask).
- Inject the SDK driver via `setAgentDriver(...)` instead of waiting on
`await import('@anthropic-ai/claude-agent-sdk')` inside `runAgent`,
same loader-pipeline rationale.
- Drop the 120_000ms advance to 70_000ms — that was always overkill (the
formula is 60s stall + 2-4s first-retry backoff, ~64s worst case).
- Clear the injected driver in `afterEach` so other tests fall back to
the top-level SDK mock.
Test now completes in ~36ms (vs. 30,000ms timeout) on Node 20.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts: # eslint.config.mjs
- RunTimeline: swap PR 4's inline `detectUnicode()` for PR 1's `supportsUnicode()` from `lib/terminalCapabilities.ts`. Same semantics, single source of truth. - RunScreen: inline PR 6's Tab-to-ask handler + AskBar + paused pill into `RunScreenTimeline` so the new-UX path (PR 4's early-return for `WIZARD_NEW_UX=1`) keeps the killer feature. Without this fix PR 6's Tab handler is unreachable when new-UX is on. ProjectPicker's inline `rankMatch` and `shouldForceAscii` are left in place — `fuzzyRank` has an array-of-items API that doesn't slot cleanly into the per-field aggregation ProjectPicker does, and `shouldForceAscii` has subtle different behavior when LANG is unset (returns false vs supportsUnicode's true). Both duplicates work and aren't worth a load-bearing refactor on a preview branch.
Member
Author
|
Closing — redesign track abandoned per user feedback. Not merging. |
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.
Combined preview branch merging all 10 timeline-ux PRs (#783–#792) for end-to-end testing. NOT for merge — use
gh pr checkoutto walk the new UX live.How to test
Merge summary
detectUnicode()swapped for PR 1'ssupportsUnicode()rankMatchandshouldForceAsciileft in place (see below)RunScreenTimelineso new-UX path keeps the killer featurePost-merge fixes (1 squashed commit)
chore(preview): wire upstream PRs through inline fallbacksRunTimeline.tsx: PR 4 inlineddetectUnicode()as a placeholder for PR 1'sterminalCapabilities. Swapped to importsupportsUnicode()fromlib/terminalCapabilities.ts. Same semantics, single source of truth.RunScreen.tsx: PR 4 added an early-return inRunScreenforWIZARD_NEW_UX=1that rendersRunScreenTimeline. That made PR 6's Tab-to-ask handler (added later in the legacyRunScreenbody) unreachable in new-UX. Inlined the Tab handler, AskBar mount, ack lines, and paused pill intoRunScreenTimelineso the new-UX path keeps Tab-to-ask. RunScreen.askBar tests pass.Intentional duplicates left in place
ProjectPicker.rankMatch— PR 5's per-string ranker isn't shape-compatible with PR 3's array-orientedfuzzyRank<T extends FuzzyRankItem>(query, items). ProjectPicker scores individual fields (orgName,envName,name) per entry and aggregates manually. Per the brief, leaving the inline duplicate.ProjectPicker.shouldForceAscii— diverges fromsupportsUnicode()whenLANGis unset (returns false / "ok to render unicode" vs. PR 1's conservative true / "fall back to ASCII"). The behavior split is intentional in the inline ranker. Left alone.Lint / test state
pnpm exec tsc --noEmit: greenpnpm lint: green (prettier + eslint, including PR 10's armed rule)pnpm test: green — 4495 / 4495 tests passpnpm build && node dist/bin.js --help: green (smoke test passes via the existing postbuild hook)no-restricted-syntaxagainstTASK|STEP|PHASE|INITIALIZING|EXECUTINGin screens): armed. PR 2'sWizardVoicesweep had landed enough that the rule fires clean across the screens directory. No// preview branch: rule disabledworkarounds needed.Known rough edges
RunTimelinerather than inline in the header (legacy ProgressTab placed it next to the elapsed timer). Functionally identical, visually different — wire location may need a follow-up sweep.fuzzyRankis generalized to support per-field aggregation, a follow-up can dedupe.