Skip to content

chore(preview): timeline-ux combined branch (PRs 1-10)#794

Closed
kelsonpw wants to merge 24 commits into
mainfrom
feat/timeline-ux-preview
Closed

chore(preview): timeline-ux combined branch (PRs 1-10)#794
kelsonpw wants to merge 24 commits into
mainfrom
feat/timeline-ux-preview

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

Combined preview branch merging all 10 timeline-ux PRs (#783#792) for end-to-end testing. NOT for merge — use gh pr checkout to walk the new UX live.

How to test

gh pr checkout <this-pr>
pnpm install
WIZARD_NEW_UX=1 pnpm try:prod --install-dir=/tmp/scratch

Merge summary

PR Status Notes
#783 (PR 1) clean terminalCapabilities + ScreenShell + StepIndicator
#784 (PR 2) clean WizardVoice + drafted ESLint rule
#785 (PR 3) clean ScreenHotkeyBar + SlashPalette + fuzzyRank
#786 (PR 4) clean (post-fix) RunTimeline + Ledger + Todos; inline detectUnicode() swapped for PR 1's supportsUnicode()
#787 (PR 5) clean ProjectPicker — inline rankMatch and shouldForceAscii left in place (see below)
#788 (PR 6) clean (post-fix) Tab-to-ask AskBar — wired into PR 4's RunScreenTimeline so new-UX path keeps the killer feature
#789 (PR 7) clean Auth screens redesign
#790 (PR 8) clean ExtrasPanel
#791 (PR 9) clean Resume + OutageBanner + Outro variants
#792 (PR 10) conflict resolved ESLint rule flip; took the armed version (PR 2's commented draft removed)

Post-merge fixes (1 squashed commit)

chore(preview): wire upstream PRs through inline fallbacks

  • RunTimeline.tsx: PR 4 inlined detectUnicode() as a placeholder for PR 1's terminalCapabilities. Swapped to import supportsUnicode() from lib/terminalCapabilities.ts. Same semantics, single source of truth.
  • RunScreen.tsx: PR 4 added an early-return in RunScreen for WIZARD_NEW_UX=1 that renders RunScreenTimeline. That made PR 6's Tab-to-ask handler (added later in the legacy RunScreen body) unreachable in new-UX. Inlined the Tab handler, AskBar mount, ack lines, and paused pill into RunScreenTimeline so 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-oriented fuzzyRank<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 from supportsUnicode() when LANG is 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: green
  • pnpm lint: green (prettier + eslint, including PR 10's armed rule)
  • pnpm test: green — 4495 / 4495 tests pass
  • pnpm build && node dist/bin.js --help: green (smoke test passes via the existing postbuild hook)
  • PR 10's ESLint guardrail (no-restricted-syntax against TASK|STEP|PHASE|INITIALIZING|EXECUTING in screens): armed. PR 2's WizardVoice sweep had landed enough that the rule fires clean across the screens directory. No // preview branch: rule disabled workarounds needed.

Known rough edges

kelsonpw and others added 24 commits May 14, 2026 22:17
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.
@kelsonpw kelsonpw changed the title preview: timeline-ux combined (PRs 1-10) chore(preview): timeline-ux combined branch (PRs 1-10) May 15, 2026
@kelsonpw
Copy link
Copy Markdown
Member Author

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

@kelsonpw kelsonpw closed this May 15, 2026
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