fix(tui): esc back-navigation; multi-select Enter stops force-selecting#264
Conversation
initializ-mk
left a comment
There was a problem hiding this comment.
Review: esc back-navigation; multi-select Enter fix
Both changes are well-motivated. The multi-select half is merge-ready: the Enter fix is exactly right, all three tests are meaningful, and I verified no step on this branch requires a non-empty multi-select result (ToolsStep handles empty — Summary = "none" — and Skills/Fallback are documented empty-legal). Handling esc at the wizard level is the correct architecture, and splitting ctrl+c (cancel) from esc (back) matches TUI conventions.
The back-navigation half has a blocking gap — see the inline comment on wizard.go: esc-back re-enters a step whose complete flag was never reset, and most steps short-circuit Update on it, soft-locking the wizard. Details inline.
2. Medium: Apply is cumulative into the context — redoing a step can leak stale values
advanceStep calls Apply on every forward pass and there is no undo on back. Set-style Applies self-heal on re-advance, but conditional writes don't: complete the tools step having selected web_search + entered a key (EnvVars["TAVILY_API_KEY"] written on advance), esc back, deselect web_search, confirm → BuiltinTools is corrected but the stale TAVILY_API_KEY / WEB_SEARCH_PROVIDER env vars persist in the context and land in the generated .env for a tool the user deselected.
Fix: steps should clear the keys they own at the top of Apply — making it "write my current state," not "add my current state." Worth fixing in this PR since back-nav is exactly what makes redo reachable.
Nits
- Whole-step vs sub-phase back: esc from a step's later sub-phase (e.g. a key input) jumps to the previous step, not the previous sub-phase. Defensible, but users may expect esc to unwind one screen — worth a deliberate one-liner in the PR description.
- Abandoned async validation: esc during a validating phase drops the in-flight
ValidationResultMsg(routed to the now-current previous step, which ignores it). Harmless once the reset contract from the blocking finding lands — re-entry restarts the phase — just noting the interaction.
Verdict
Hold for the soft-lock fix (inline on wizard.go) — as shipped, pressing esc after completing Provider/Channel/Review (most of the wizard) leaves the session stuck, a worse outcome than the missing back-navigation it replaces. The Apply leak should ride along. The multi-select change needs nothing.
| if msg.String() == "esc" { | ||
| if w.current > 0 { | ||
| w.current-- | ||
| return w, w.steps[w.current].Init() |
There was a problem hiding this comment.
Blocking: esc-back lands on an inert step and soft-locks the wizard.
This is w.current-- + Init() with no state reset, and the steps aren't uniform about resetting themselves. On this branch:
SkillsStep.Init()resets (s.complete = false, phase, prompt) — designed for re-entry via the pre-existingStepBackMsgpath.ProviderStep.Init(),ChannelStep.Init(),ReviewStep.Init()do not reset — and every step'sUpdatebegins withif s.complete { return s, nil }.
So the exact flow this feature exists for — complete step N, realize at N+1 you made a mistake, press esc — re-enters a step whose complete is still true: it renders its final view, ignores all input, and can never emit StepCompleteMsg again (which this file's own comment says is the sole advancement path). The user is soft-locked; the only working keys are esc (deeper into more inert steps) or ctrl+c (lose everything). Sub-phase state has the same problem: esc away from a validating phase and return, and the step waits forever for a ValidationResultMsg that was already dropped.
The defect shape predates this PR (StepBackMsg below has it too), but this PR promotes back-nav from three steps to all of them, so it should carry the fix.
Fix: establish a re-entry contract — every step's Init() resets to its initial phase (the SkillsStep precedent), or add a Reset() the wizard calls on back-entry, or recreate the step. Then pin it with a round-trip test on a REAL step: complete → esc → change answer → advance.
| ) | ||
|
|
||
| // navMockStep is a minimal Step for driving the wizard in tests. | ||
| type navMockStep struct{ n string } |
There was a problem hiding this comment.
This mock is why the soft-lock (see wizard.go comment) is invisible to the suite: navMockStep is a stateless value type that never completes, so these tests verify index arithmetic, not usability after back. The scenario that matters — complete a real step (whose complete flag then short-circuits Update), esc back into it, change the answer, advance again — needs a stateful step, either a real one or a mock with the complete guard the real steps share.
| {Key: "⏎", Desc: "confirm"}, | ||
| {Key: "backspace", Desc: "back"}, | ||
| {Key: "esc", Desc: "quit"}, | ||
| {Key: "esc", Desc: "back"}, |
There was a problem hiding this comment.
Nit: the review screen now advertises two back keys — backspace back (step-local StepBackMsg) and esc back (wizard-level). Either drop the backspace hint since wizard-level esc supersedes it, or add a line on why both stay.
…dd Apply (#264 review) Blocking fix: esc-back re-entered a step whose `complete` flag was never reset, and every step's Update short-circuits on `if s.complete` — so pressing esc after completing Provider/Channel/Tools/etc. left the wizard stranded (input ignored, only esc-deeper or ctrl+c worked). This PR promoted back-nav from a few steps to all of them, so it carries the fix. - Establish the re-entry contract (SkillsStep/CompressionStep precedent): every step's Init() resets to its initial phase + complete=false. Fixed name/provider/channel/tools/fallback/auth/review (compression/egress/skills already did). The wizard already calls Init() on esc-back and StepBackMsg, so this closes the soft-lock. - Apply writes CURRENT state, not cumulative (finding #2): back-nav makes redo reachable. ToolsStep.Apply now clears the web-search keys it exclusively owns (WEB_SEARCH_PROVIDER / TAVILY_API_KEY / PERPLEXITY_API_KEY) before re-writing, so deselecting web_search on redo doesn't leak a stale key into .env. ChannelStep.Apply clears ChannelTokens (also exclusively owned) first. - kbd_hint: the review screen no longer advertises two back keys — only `esc` (wizard-level) since it supersedes the step-local backspace path. - Tests: steps/reentry_test.go drives REAL steps (Init resets complete) + the ToolsStep Apply-leak; wizard_nav_test.go now uses a STATEFUL mock (complete guard + Init reset) with a round-trip test that fails on the soft-lock. The provider/fallback/skills Applies share the *_API_KEY namespace, so a naive clear there could wipe another step's key — that broader cumulative-Apply issue is filed as a follow-up rather than risk a wrong clear here.
|
Thanks — thorough catch on the soft-lock. All addressed. Blocking: esc-back soft-lock (wizard.go). Fixed by establishing the re-entry contract you identified — every step's Test mock (wizard_nav_test.go:10). Replaced the stateless value mock with a stateful one that mirrors the real contract — Finding #2 (Apply leak). Fixed for the two cases where the step exclusively owns its keys: The provider/fallback/skills Applies share the Nits:
Tests + lint green (the multi-select half was already merge-ready per your review). |
initializ-mk
left a comment
There was a problem hiding this comment.
Re-review of c3ff280 (+ main merge) — both findings fixed, CI green
All 9 checks pass. Verification:
Blocking finding (soft-lock) — fixed comprehensively. The re-entry contract is now uniform: Init() resets complete + phase in name / provider / channel / fallback / auth / review (skills / compression / egress already did). Pinned at three layers:
TestStepInitResetsCompletedrives seven real steps through force-complete →Init()→ assert usable;- the test mock was rebuilt as a stateful pointer type mirroring the real steps'
if s.completeguard; TestWizard_BackReEntersUsableStepis the exact round-trip from the review — complete → esc back → assert not inert (explicit "SOFT-LOCK" failure message) → re-complete → re-advance.
Medium finding (cumulative Apply) — fixed where safely possible, rest tracked. The tools/web-search step deletes its three env keys before rewriting; ChannelStep clears ChannelTokens. Deliberately NOT clearing the shared *_API_KEY namespace in provider/fallback/skills was the right call (a naive clear could wipe a key another step wrote) — filed as #296. One caveat on "exclusively owns TAVILY_API_KEY": skills like tavily-research also require that key via the skills step, but that overlap is precisely #296's scope.
Cross-PR reconciliation — handled properly. #263 merged into main mid-review (deleting ToolsStep, adding WebSearchStep, which had neither the Init reset nor a write-not-add Apply). The merge commit reconciled it fully: WebSearchStep.Init() resets complete/validating/phase, its Apply clears the env keys and removes web_search from BuiltinTools before re-adding (also fixing the append-duplication landmine flagged in the #263 review context), and reentry_test.go covers WebSearchStep for both the reset and the leak.
Nit — taken: the review screen's redundant backspace back hint was dropped with a rationale comment.
Verdict
Ready to merge. Both findings fixed with regression tests at the right level; the mid-flight merge with #263 was reconciled carefully.
(Housekeeping note, unrelated to this PR: the isolated-world evaluation promised by the snapshot.js trust-boundary comment in #260 is still not tracked in #293 or any open issue — please file it so that comment is true.)
…ck-entry Two wizard fixes plus the #264 review follow-ups, rebased onto main (linear history — no merge commit). - esc is global back-navigation, handled at the wizard level so it works from every step; ctrl+c cancels. Multi-select Enter confirms exactly what Space toggled (including an empty selection) instead of force-checking the cursor row. - Re-entry contract (review blocking fix): every step's Init() resets to its initial phase + complete=false, so esc-back doesn't land on an inert completed step whose Update short-circuits on `if s.complete` and soft-locks the wizard. Fixed name/provider/channel/web_search/fallback/auth/review (compression/egress/skills already did). - Apply writes CURRENT state, not cumulative (review finding #2): WebSearchStep and ChannelStep clear the keys they exclusively own before re-writing, so redo after back-nav doesn't leak a stale key/provider into the generated .env. Broader shared-*_API_KEY cases tracked in #296. - kbd_hint: review screen advertises only `esc back` (wizard-level supersedes the step-local backspace path). - Tests: real-step re-entry contract + WebSearchStep Apply-leak (steps/reentry_test.go); stateful wizard mock with a back-nav round-trip that fails on the soft-lock (wizard_nav_test.go); multi-select Enter behavior.
b74a837 to
bbdfe38
Compare
Two
forge initwizard UX fixes from TUI feedback.1. Back-navigation everywhere (
esc)Previously only a few steps (review / fallback / egress) emitted a backspace-back; most screens had no way to go back. Now
escgoes back one step from any step, handled at the wizard level so it works regardless of a step's sub-phase or text inputs (escis never an editing key, so intercepting it there is safe).esc→ previous step (re-initialized).escon the first step → cancel (nowhere to go back).ctrl+c→ cancel (unchanged).esc quit→esc back.2. Multi-select
Enterstops force-selecting the highlighted rowThe multi-select confirmed on
Enterby auto-checking the cursor row when nothing was toggled — so "just press Enter" silently selected whatever was highlighted, even though Space is the toggle key. NowEnterconfirms exactly what Space toggled, including an empty selection. Steps like Skills / Fallback legitimately allow selecting nothing.Tests
wizard_nav_test.go—escsteps back from step 2→1→0, and cancels on the first step.multi_select_test.go—Enterconfirms empty;Space+Enterselects the cursor row; cursor-move thenSpacetoggles the correct row.Build +
golangci-lintclean;forge-cli/internal/tui/...tests green.Companion to #263 (drop builtin-tool selection screen; web search gets its own step) — same wizard, independent changes.