Skip to content

feat(tui): /version slash command for wizard + protocol version visibility#768

Merged
kelsonpw merged 2 commits into
mainfrom
salvage/tui-v3-version-command
May 14, 2026
Merged

feat(tui): /version slash command for wizard + protocol version visibility#768
kelsonpw merged 2 commits into
mainfrom
salvage/tui-v3-version-command

Conversation

@kelsonpw
Copy link
Copy Markdown
Member

@kelsonpw kelsonpw commented May 14, 2026

Summary

Salvaged from #725 by cherry-picking its unique commit onto a fresh branch from main. The original PR was stacked on the closed feat/v2-tui-redesign (#696); its git history carried ~35 commits of v2 orchestration scaffolding that couldn't be rebased onto main's parallel orchestration implementation.

Original commits preserved: 8cf86032

Skipped: f6b09583 — this followup wraps process.exit in src/utils/wizard-abort.ts, which is on the salvage-policy "do not touch" list (load-bearing). The vitest unhandled-rejection bug it patches can be re-addressed in a dedicated PR if it surfaces on CI.

Notes:

Test plan

  • tsc clean
  • eslint clean
  • pnpm build clean
  • CI will run vitest + Bugbot

Generated with Claude Code


Note

Low Risk
Low risk: changes are isolated to TUI command registration/dispatch and add read-only version formatting with test coverage, without affecting auth, data writes, or run orchestration.

Overview
Adds a new read-only /version slash command to the TUI that surfaces wizard version, agent-mode wire protocol version, and Node/platform runtime info via getVersionText().

Wires /version into ConsoleView command dispatch (shown via setCommandFeedback) and extends unit/render tests to ensure the command stays registered and remains available mid-run.

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

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: Version text renders as single blob, not separate lines
    • Split the getVersionText() return value with .split('\n') so setCommandFeedback receives a string[] array, giving each version line its own <Box> with proper indent prefix, matching how /diagnostics passes multi-line feedback.

Create PR

Or push these changes by commenting:

@cursor push f598f482b1
Preview (f598f482b1)
diff --git a/src/ui/tui/components/ConsoleView.tsx b/src/ui/tui/components/ConsoleView.tsx
--- a/src/ui/tui/components/ConsoleView.tsx
+++ b/src/ui/tui/components/ConsoleView.tsx
@@ -302,7 +302,7 @@
       // exiting the TUI to run `amplitude-wizard --version` in a shell.
       // Multi-line, so we render it through the same long-lived
       // `setCommandFeedback` slot used by /diagnostics.
-      store.setCommandFeedback(getVersionText(), 30_000);
+      store.setCommandFeedback(getVersionText().split('\n'), 30_000);
       break;
     case '/exit':
       store.setOutroData({ kind: OutroKind.Cancel, message: 'Exited.' });

diff --git a/src/ui/tui/components/__tests__/ConsoleView.test.tsx b/src/ui/tui/components/__tests__/ConsoleView.test.tsx
--- a/src/ui/tui/components/__tests__/ConsoleView.test.tsx
+++ b/src/ui/tui/components/__tests__/ConsoleView.test.tsx
@@ -245,10 +245,16 @@
     // Give the dispatch + setCommandFeedback round-trip a tick.
     await new Promise((r) => setTimeout(r, 30));
 
-    const feedback = store.commandFeedback ?? '';
-    expect(feedback).toContain('Amplitude Wizard v');
-    expect(feedback).toContain('Agent-mode protocol: v');
-    expect(feedback).toContain(`Node: ${process.version}`);
+    const feedback = store.commandFeedback;
+    expect(Array.isArray(feedback)).toBe(true);
+    const lines = feedback as string[];
+    expect(lines).toEqual(
+      expect.arrayContaining([
+        expect.stringContaining('Amplitude Wizard v'),
+        expect.stringContaining('Agent-mode protocol: v'),
+        expect.stringContaining(`Node: ${process.version}`),
+      ]),
+    );
 
     unmount();
   });

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ca81bf7. Configure here.

Comment thread src/ui/tui/components/ConsoleView.tsx
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…o 30s (#777)

The "refreshes the MCP Authorization header when the token rotates
between attempts" test simulates a 60s cold-start stall plus jittered
backoff (2-30s) inside fake timers — up to ~90s of virtual time per
attempt. The test logic is correct, but the per-test 15s real-time
timeout left no headroom for the scheduler under CI load on Node 20,
producing intermittent "Test timed out in 15000ms" failures across
multiple PRs (#765, #766, #767, #768, #769, #771).

Double the timeout to 30s. No logic change.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
@kelsonpw kelsonpw force-pushed the salvage/tui-v3-version-command branch from dd8c336 to 98ab797 Compare May 14, 2026 19:14
kelsonpw added a commit that referenced this pull request May 14, 2026
…rtions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
…#767)

* fix(tui): drop digit-shortcut UI hints on >10-option PickerMenu lists

The PickerMenu primitive consumed digit keystrokes 1-9 (+ 0 → index 9)
and rendered a `[N]` chip next to each row. The handler only covers
the first 10 indices, but the chip was emitted for those first 10
rows regardless of total list length — so on lists with >10 options
(e.g. Amplitude users with 20+ orgs, or projects with many envs)
items 11+ looked visually identical to shortcut-enabled rows but
typing their visible number did nothing, while items 1-10 advertised
shortcuts that worked.

Now:
  - options.length <= 10 → render `[1]`-`[9]`/`[0]` chips as before.
  - options.length  > 10 → drop the chips on EVERY row and show a
    muted "Use arrows + Enter to pick" hint at the bottom of the list.

The digit handler itself is unchanged for indices 0-9 so power users
who learned the shortcut keep it; the UI just stops advertising it
once the affordance would be incomplete.

Audit #4 finding (TUI pickers + primitives). Stacks on #728.

* fix(bugbot): account for hint row in PickerMenu chrome budget

PR A12's new "Use arrows + Enter to pick" footer (rendered when
options.length > DIGIT_SHORTCUT_LIMIT) added a row that wasn't
budgeted. `CONSTRAINED_CHROME_RESERVE_ROWS = 3` covered the
PromptLabel header + 2 scroll indicators, so when both indicators
were visible and the hint rendered, total height became
`availableRows + 1` and the parent's `overflow="hidden"` clipped
the bottom — precisely on the >10-option lists this PR targets.

Add a dynamic `hintRows` (0 or 1) to the chrome reserve so the hint
fits inside the budget on both constrained (AuthScreen org/project
/env pickers) and natural-height paths.

* fix(salvage): drop PICKER_FLASH_MS import in PickerMenu test

The PICKER_FLASH_MS export came from the unmerged #715 delight pass
(selection-confirmation flash). On main, PickerMenu's `onSelect` is
synchronous so we just advance fake timers by 5ms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(test): drain PICKER_FLASH_MS in PickerMenu large-list digit-shortcut tests

After merging main, PickerMenu introduced a 250ms selection-confirmation
flash before onSelect fires. The large-list tests advanced fake time
by only 5ms, so they read the still-null `chosen` slot and failed
with `expected null to be 'v3'`.

Advance by `PICKER_FLASH_MS + 5` so the flash timer drains and
`onSelect` is invoked before the assertion.

* fix(test): tolerate 1s tick drift in EventPlanFullScreen elapsed assertions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw added a commit that referenced this pull request May 14, 2026
…te (#769)

* feat(tui): register /help command and accept Tab in slash palette

Tab in the slash palette now accepts the highlighted suggestion
(Raycast/Slack convention) and fills the input with `<cmd> ` so the
user can keep typing arguments. The palette stays open; Enter still
submits. Outside slash mode, Tab continues to open Ask via
ConsoleView's pre-activation handler.

Also adds an `↑↓ navigate · Tab/Enter run · Esc cancel` footer hint
beneath the candidate list so users don't have to guess which key
does what. /help itself was already registered (see prior PR) — this
commit completes the paired affordances from the TUI audit T5 wave.

* fix(bugbot): correct slash palette footer and tolerate trailing space in filter

Bugbot flagged two issues on PR #739:

1. (3220814211) Palette footer read "Tab/Enter run" but Tab only completes
   the highlighted suggestion (setValue) — Enter is required to actually
   submit. Footer now reads "Tab complete · Enter run" so the affordance
   matches the handler at line ~118.

2. (3220814221) Tab fills the input with `<cmd> ` (trailing space, per
   Raycast/Slack convention so the user can keep typing args). The filter
   used `value.slice(1)` as the query, so the trailing space caused both
   `cmd.startsWith(query)` and the description includes-check to fail —
   `filtered` emptied and the palette tore itself down mid-completion.

   Picked option B (trim query at filter time): the filter now keys off
   the first whitespace-delimited word, matching the slash-mode check at
   `computeIsSlashMode`. Reason: trailing space is the universal
   palette/shell convention (zsh, fish, Claude Code itself, every
   modern CLI palette) — it signals "now type your arg". Dropping the
   space (option A) would force the user to type it themselves before
   args, which is a worse UX. The filter should be query-trim-tolerant.

   Verified clampedIndex, scroll math, and Enter-submit behavior still
   work because they all key off `filtered.length`, which is now
   non-empty after Tab.

Tests extended in SlashCommandInput.tab.test.tsx to lock down the new
footer copy and assert the palette stays open after Tab completion.

* fix(bugbot): preserve argv when Enter-submitting slash commands

Bugbot 3220907967 (high). The Tab-trailing-space fix that landed in
24eb3bd keyed the slash filter off the first whitespace-delimited
word, which fixed the palette-collapse-on-trailing-space regression
but introduced a worse one: typing /feedback hello world and pressing
Enter submitted just /feedback because the Enter handler preferred
filtered[clampedIndex].cmd whenever filtered was non-empty, and the
new first-word filter kept it non-empty even when args were typed.

Affected commands in the registry: /feedback <message> and
/create-project <name>. Args were silently dropped.

Fix: Enter now distinguishes command-only (no space in value) from
command-with-args (space present). Command-only takes the palette
pick (so /he + Enter still submits /help). Command-with-args submits
the value verbatim. The Tab fill behavior (trailing space) is
unchanged because Tab does not call onSubmit.

Tests: extends SlashCommandInput.tab.test.tsx with regression coverage
for /feedback hello world, /he, /help, and /feedback<space>.

* fix(bugbot): detect hasArgs on untrimmed value so trailing space survives

`hasArgs = trimmed.includes(' ')` stripped the very signal we wanted —
`/cmd ` (trailing space, e.g. from Tab completion) was indistinguishable
from `/cmd` and silently palette-picked. Now detect args on the
untrimmed value: a trailing space anywhere routes to verbatim submit,
matching the universal palette/shell convention.

Bugbot 3221028494.

* feat(tui): register /help slash command — docs canonical, registry was missing

CLAUDE.md lists `/help` as a canonical slash command, but the
registry in `src/ui/tui/console-commands.ts` was missing it. Users
typing `/help` got nothing — no completion match in the picker, no
dispatch case, no feedback. The empty-result path is the worst kind
of broken: the command appears unsupported even though it should be
the first thing a new user reaches for.

Add `/help` to `COMMANDS` and a dispatch case in `ConsoleView.tsx`
that prints a categorized list of every other slash command + a
one-line key-binding cheatsheet, surfaced through the existing
`setCommandFeedback` channel.

Regression test in `console-commands.test.ts` asserts `/help` is in
the registry and is NOT `requiresIdle` (so it works mid-run).

* fix(test): make SlashCommandInput tab tests resilient to new /d* commands

The "fills the input with the highlighted command + space" test used
`/d` as the prefix and assumed the first cmd-prefix match was
`/debug`. Main now contains `/diff` (added by #599) which sorts
ahead of `/debug` in the COMMANDS array — once this branch merges
with main, the test received `/diff` instead of `/debug`.

Tighten the first test to `/de` (matches /debug only).

The "completes the second match when the user navigates to it" test
relied on DOWN moving from /debug → /diagnostics. With /diff now in
the mix, DOWN once landed on /debug instead. Switch to a
self-contained two-command list so the test doesn't depend on the
production COMMANDS-array ordering at all — the wiring it pins is
"DOWN moves to second match, Tab accepts it".

* fix(lint): remove duplicate /help case from ConsoleView after merge

The salvage branch added an inline `/help` case in ConsoleView's
switch; main later landed a separate `/help` case that calls
`getHelpText()`. After merge, both coexisted and ESLint flagged the
duplicate-case as an error.

Drop the inline version. `getHelpText()` is the canonical helper —
it's covered by console-commands.test.ts and shared with any future
non-TUI surface that wants the same listing.

* fix(test): tolerate 1s tick drift in EventPlanFullScreen elapsed assertions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added a commit that referenced this pull request May 14, 2026
)

* fix(tui): accept Enter on screenError retry shortcut

The screen-error banner advertises `[R] retry` but the dormant ConsoleView
keyboard handler only matched `r`/`R`. Users focused on a prompt below the
banner often press Enter as the default action — accept it as a retry
trigger so the banner clears as advertised.

Adds a regression test that asserts both `r` (existing path) and Enter
(new) call `clearScreenError`. Audit signal from iteration #10 5-subagent
TUI wave (auditor #2, RunScreen + console + slash).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bugbot): gate screenError Enter handler on !pendingPrompt

The previous fix accepted Enter as a screenError retry shortcut but
didn't account for the case where `screenError` and `pendingPrompt`
are simultaneously active (e.g. a `promptChoice` rendering a
PickerMenu below the error banner). Both `useInput` handlers were
active in that state — the ConsoleView's accepted Enter to clear
the error, AND the PickerMenu's `useScreenInput` accepted Enter to
commit the focused selection. Pressing Enter once would fire both,
clearing the error AND committing an unintended picker option.

Split the screenError keystroke matcher into two branches:
- `r`/`R` accepted unconditionally (letters don't collide with
  picker shortcuts)
- `key.return` accepted only when `!pendingPrompt` (picker owns
  Enter while a prompt is in flight)

Regression test pins the new behaviour by setting up a `confirm`
prompt + screenError, asserting Enter does NOT clear the error,
then asserting `R` still does.

* fix(test): tolerate 1s tick drift in EventPlanFullScreen elapsed assertions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kelsonpw added a commit that referenced this pull request May 14, 2026
…t-activity anchor (#771)

* feat(tui): surface error-outro hotkeys as accent-coloured pill bar

The error outro buries `L`, `C`, and `R` (the only interactive
affordances on the screen) inside the muted secondary-text
troubleshooting bullets — "(press L to open)", "Press C to write a
sanitized bug report", "Press R to retry from where we left off".
Users scanning the screen for "what can I press right now?" frequently
skim past those bullets because they look like documentation, not
buttons.

Pull the keys out of prose and render them as a row of high-contrast
`[K] label` pills at the foot of the screen — same visual idiom as the
global KeyHintBar so the affordance reads as interactive at a glance.
The troubleshooting bullets above stay as context but no longer
duplicate the hotkey verbiage. The cancel path's "press R now to
resume" line is unchanged (it's the only hint there, so prose works).

The R pill is gated on the same `showRetryHint` condition as before
(suppressed for auth failures and non-interactive runs), so the bar
never advertises a hotkey that would be a no-op. The preserve-files
prompt suppresses the standard bar entirely — its dedicated K/R prompt
would compete with a second pill row.

Covered by:
  - HotkeyPills.test.tsx (4 tests) — render shape + ordering
  - OutroScreen.error.test.tsx — updates the existing retry-hint
    regression suite to assert the new `[R] Retry` shape
  - OutroScreen.snap.test.tsx — error snapshot updated to show the
    pill bar replacing the inline "press X to …" hints

* feat(tui): surface backoff seconds in sustained-storm retry chip

RetryState already carries `nextRetryAtMs` (set by `publishRetryBanner`
in agent-interface.ts), but `RetryStatusChip` discarded it entirely —
the chip's design intent was calm copy ("slowing down to match
Amplitude rate limits"), no countdown, spinner-as-liveness. That's
correct for transient blips, but once a storm has been going long
enough that the chip already escalated to the "(still trying)" suffix
(attempt ≥ 5), the user has clearly been staring at the chip long
enough to want a concrete number. "Is it actually still working?" is
the implicit question; the backoff seconds answer it.

Sub-second tails clamp to 1 (avoid a misleading "0s" right before the
retry fires); past-due timestamps fall through to plain "(still
trying)" so we never advertise a wait the user won't experience. The
calm-storm invariants are preserved by design — no X/Y attempt
fraction ever surfaces, no raw HTTP code, the chip stays hidden during
the grace window, and the backoff tail is gated on the same sustained
threshold that already guards the "(still trying)" suffix.

`backoffSecondsFromState` is exported as a pure helper so the rounding
contract (Math.floor, min 1 while time remains) is covered
independently of the chip's full pipeline.

Covered by:
  - 6 new tests on `getRetryStatusText` (sustained + various
    nextRetryAtMs shapes)
  - 4 new tests on `backoffSecondsFromState` (rounding, clamping,
    past-due, unset)
  - Existing 11 calm-chip tests still pass — design intent preserved

* feat(tui): anchor error outro with "Started at … · Step: …" line

When a run dies, the user is staring at a wall of suggested next-steps
("Press L to open the log", "--debug for more detail", "Press C to
write a bug report") and the log file is a haystack. They have to
scroll back through dozens of progress messages to find the point of
failure. The session already tracks both `runStartedAt` and the active
task (via `store.tasks` derived from agent TodoWrite); we just weren't
surfacing them at the moment they'd be most useful.

Add a muted sub-line directly under the error message:
  "Started at 14:23:47 · Step: Install Amplitude"

The line gives the user a concrete entry-point into the log file
before they open it. Resolution order:
  1. Latest in_progress task wins — that's where the agent died.
  2. Fall through to the most-recent completed task — "we got through
     install but failed before reaching plan".
  3. If everything is pending OR runStartedAt is null, render nothing
     — better than a confusing "Step: <empty>" line.

Pure helpers (`buildLastActivityFooter`, `formatClockTime`) live in
their own module so the resolution order is verifiable independently
of the rendering pipeline.

Gated to the error outro only — the cancel path leads with "Resume
later" and the success path doesn't need a forensic anchor. Snapshot
tests still pass unchanged: `makeStoreForSnapshot` defaults
runStartedAt to null, so existing fixtures don't pick up the new line.

Covered by:
  - outro-last-activity.test.ts (10 tests) — pure helper contract
  - 6 new render-level tests in OutroScreen.error.test.tsx —
    appears/hides for each outro kind, each task-state shape

* fix(test): tolerate 1s tick drift in EventPlanFullScreen elapsed assertions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
kelsonpw added 2 commits May 14, 2026 12:31
Lets users grab the wizard version, agent-mode NDJSON protocol version,
and Node/platform runtime from inside the TUI instead of exiting to run
`amplitude-wizard --version` in a shell. Useful for bug reports filed
without leaving an active session.

Output:

  Amplitude Wizard v1.17.0
  Agent-mode protocol: v1
  Node: v20.11.0 (darwin arm64)

Registers /version in the COMMANDS registry without `requiresIdle` so it
works mid-run (informational, no side effects). Wires dispatch through
the existing setCommandFeedback slot used by /help and /diagnostics.

`getVersionText` accepts an injected runtime so unit tests pin the shape
against fixed values; the default path reads from `process.*` so the live
command shows the user's actual environment.

Stacks on #719.
…rtions

The revising-screen elapsed timer ticks every 1s via setInterval; on
Node 24 under CI clock pressure the interval can fire one tick before
the test's advanceTimersByTime boundary, so the rendered "elapsed:"
string lands at `1m 29s` instead of `1m 30s`. Both readings are
correct for the test's intent (verifying tier WIRING, not exact
copy), so widen the assertions to a regex that accepts ±1s.

Also includes a trivial prettier fix in console-commands.test.ts
that the file-level linter flagged.
@kelsonpw kelsonpw force-pushed the salvage/tui-v3-version-command branch from 98ab797 to 5e5dcb7 Compare May 14, 2026 19:39
@kelsonpw kelsonpw merged commit 3abc1d7 into main May 14, 2026
10 checks passed
kelsonpw added a commit that referenced this pull request May 14, 2026
…765)

* fix(tui): clear sticky currentFile pill when agent activity is quiet

The "currently editing X" pill on RunScreen latched `lastFileRef` whenever
`rawFile` was truthy but had no clear path — so after the inner agent
finished its last write, the pill kept showing that file forever, into
the Finalizing phase and beyond. The header lied about what the wizard
was doing.

Clear the sticky value when either condition holds:

  1. The most recent `fileWrites` entry is older than 10s AND its
     status is terminal (`applied` / `failed`) — quiet enough that
     "currently editing" is no longer true.
  2. `postAgentSteps.length > 0` — the agent has moved past the
     file-write phase entirely (commit events, create dashboard, …).
     Clear immediately, no grace period needed.

The stale-time check rides the spinner's existing SPINNER_INTERVAL
re-render — no extra timer mounted. Computed inline during render
rather than through useEffect so a quiet stretch (rawFile null, last
entry timestamps unchanged) doesn't skip re-evaluation.

Tests pin both clear conditions plus a heartbeat-keepalive regression:
a new write before the stale window switches the pill to the new path
without clearing.

* fix(test): tolerate 1s tick drift in EventPlanFullScreen elapsed assertions

Same fix as PR #768. The revising-screen elapsed timer ticks every
1s via setInterval; on Node 22 under CI clock pressure the interval
can fire one tick before the test's advanceTimersByTime boundary,
so the rendered "elapsed:" string lands at `1m 29s` instead of
`1m 30s`. Both readings are correct for the test's intent
(verifying tier WIRING, not exact copy), so widen the assertions
to a regex that accepts ±1s.
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