fix(init): wizard UX overhaul + reuse an already-running dashboard#32
Merged
Conversation
First user-facing dogfood of v0.3.2 hit the discoverability cliff baked into huh's defaults: the multiselect's toggle key (SPACE or x) lives in a small footer, while ENTER submits the form. Users reading top-down press ENTER expecting it to toggle the highlighted row, accidentally pre-trusting every detected host without realizing. Two fixes, paired: 1. Surface the keys where users actually look. The prompt description now opens with "Press SPACE to toggle a host, ENTER to confirm." — right under the title, not at the bottom edge. 2. Add a Y/N confirm after the multiselect that lists the selected hosts. If the user picks "Back, edit selection" the wizard loops back into the multiselect — no need to abort the wizard and re-run. Selecting nothing routes to a different prompt body that explains what "no pre-trust" actually means in practice. PromptHostsConfirm joins the Prompter interface so the mock + tests exercise the loop. New test asserts that a rejected confirm causes PromptHosts to be re-asked.
Same dogfood pass that exposed the multiselect Enter trap also surfaced this one: the "Add a custom host?" loop creates a new form per iteration, so a user who hits Enter on a typed value sees the field visibly clear, with no signal that the host was captured. Now each iteration after the first shows "Added so far: foo, bar" above the description and flips the title to "Add another? (leave blank to finish)" so the looping nature is visible. Pure formatter `customHostsPromptText` does the work; HuhPrompter just wires it. Drive-by: the host-confirm copy added in the previous commit said "Pre-trust these hosts?" / "Don't pre-trust any host?" — both leak the word "trust" into wizard copy that the project deliberately keeps clear of it (see TestCopy_NoTrustWord). Reworded to "Mark these hosts quiet?" / "Start with no quiet hosts?" matching the existing voice, and added the new strings to the copy-coverage map so the next "trust" slip-up gets caught by the existing test instead of slipping past.
…sts, back nav
Iteration two from dogfooding the wizard. Four issues, paired fixes:
* The policy-summary page only showed counts ("Quiet: 4 hosts") with no
way to see *which* hosts. Now lists each host indented under the
Quiet line, so users can verify what they're committing to before
the config is written. PromptPolicySummary takes []string instead of
a count; new pure helper `policySummaryDescription` does the
rendering and is unit-tested.
* The cert-install confirm defaulted focus to "Skip" because `var ok
bool` zero-inits to false. Setting up an audit gate without trusting
the local CA is the rare path; flipping the default to true makes
Install the focused button so a stray Enter does the obvious thing.
* The custom-hosts loop dropped users straight into a textbox per
iteration with no visible signal that their previous Enter captured
anything. Replaced with a Y/N gate ("Add a custom host?" first time,
"Add another?" after) that opens a clean text box on Yes and shows
"Added so far: foo, bar" between rounds. Default focus is No so a
stray Enter skips the step entirely.
* No way to undo a mis-step. Added an `ErrPromptBack` sentinel any
Prompter method can return; the runner is now a stage machine that
catches it and rewinds. New `backableNote` helper renders a
Continue+Back two-button confirm in place of huh.Note for the
three-list and policy-summary stages. Welcome stays buttonless
(it's first); the host multiselect's existing "Back, edit
selection" already covers that direction. Going back from policy
summary clears the custom-host extras and returns to the Y/N gate
rather than redoing agent detection — that's almost always what
the user wants.
…bind Dogfooding turned up a real friction point: if you've started a long-running `agent-gate dashboard` to keep the UI open between agent runs, then `agent-gate run -- claude` fails with `dashboard listener bind ...: bind: address already in use`. Annoying — the existing dashboard reads from the same store, so a second one isn't needed. The supervisor now probes the dashboard port on bind failure. If a short HTTP GET returns an HTML body containing "agent-gate", it assumes our own dashboard is already up and continues without starting a second one (logging "dashboard already running ... reusing it"). If the port is held by something that *isn't* us, the original bind error still surfaces — we don't want to silently route around an unrelated process listening on the audit port. Pairs with the init-complete-message update in the previous commit, which mentions `agent-gate dashboard` as the long-running variant so the "keep the UI open" workflow is discoverable.
Two follow-ups from the /review on this PR. * The probe used to scrape the response body for the literal string "agent-gate" anywhere in the first 4 KB. Brittle: any local server with "agent-gate" in its index page (a docs link, a status page, a cached CHANGELOG) would falsely match and silently make `agent-gate run` skip its dashboard. Switched to a custom `X-Agent-Gate: 1` response header set by middleware on every dashboard response, and the probe is now a HEAD request that checks the header. Survives template changes, cheap to set, hard to collide with on accident. * The wizard runner's state machine declared its stages as untyped-int constants inside `Run()` (`const ( stageWelcome = iota ... )`), which let `stage := stageWelcome` infer the variable as plain `int` and silently allow `stage = 42` style assignments. Promoted to a named `type stage int` at package level alongside ErrPromptBack. The state-machine intent is now legible to readers and a typo can't compile into a stuck loop. Tests added: dashboard always emits the signature header on every route; the supervisor probe rejects servers that mention "agent-gate" in their body but don't carry the header.
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.
Why
Dogfooding the v0.3.2 wizard end-to-end turned up six real UX bugs that all share a theme: huh's defaults assume the user knows the conventions (toggle keys, focus semantics, "leave blank to skip"), and our wizard didn't make them visible. Plus one supervisor friction point: keeping a long-running dashboard open between runs.
What changes
agent-gate initwizardHint above the multiselect. ENTER on huh's multiselect submits the form; toggling is SPACE/x. The keymap was only documented in a small footer. The prompt description now opens with
"Press SPACE to toggle a host, ENTER to confirm."right under the title.Confirm step after the multiselect. New
PromptHostsConfirmlists what's selected and asksYes, continue/Back, edit selection. Default focus is Yes, continue so a stray Enter just continues. Rejection loops back to the multiselect; selecting nothing routes to a different prompt body explaining the "no quiet hosts" semantic.Custom-hosts step is now a Y/N gate. Replaces the textbox-loop-with-leave-blank-to-finish flow that left users wondering if their Enter captured anything. First time:
\"Add a custom host?\"(Yes, add one / No, continue) — default focus on No so a stray Enter skips the step. On Yes: a clean text box opens; submitting it returns to the Y/N with\"Added so far: foo, bar\"and the title flips to\"Add another?\".Policy summary lists each host. The summary used to show
\"Quiet: 4 hosts\"with no way to see which. Now lists each host indented under the Quiet line so users can verify what they're committing to before the config is written.Cert-install defaults to Install.
var ok boolzero-inits to false, which made Skip the focused button. Setting up an audit gate without trusting the local CA is the rare path; the affirmative is now the default focus.Back navigation. New
ErrPromptBacksentinel;runner.gois a stage machine that catches it and rewinds. Three-list note and policy summary now showContinue / Backtwo-button confirms (replacing huh.Note). Welcome stays buttonless (it's first); the multiselect's existing "Back, edit selection" covers that direction. Going back from policy summary clears the custom-host extras and returns to the Y/N gate — almost always what the user wants.Init-complete message now mentions
agent-gate dashboardas the long-running variant alongsideagent-gate run, so the "keep the UI open" workflow is discoverable.agent-gate rundashboard already running at http://... — reusing itand continues without starting a second one. If the port is held by something that isn't us, the original bind error still surfaces.Voice/style hygiene drive-by
TestCopy_NoTrustWord). Reworded to "Mark these hosts quiet?" / "Start with no quiet hosts?" to match the existing voice. New constants are wired into the copy-coverage map so future slips get caught.Test plan
go test ./...cleango vet ./...cleangofmt -l .cleanTestRunner_HostsConfirmReject_LoopsBackToMultiselectTestRunner_BackFromThreeList_RewindsToWelcomeTestRunner_BackFromPolicySummary_RedoesCustomHostsTestCustomHostsConfirmText_FirstIteration_NoTally+_SubsequentIteration_ShowsTallyTestPolicySummaryDescription_ListsEachHost+_EmptyHosts_NoListedNamesTestIsAgentGateDashboard_RecognizesOurSignature+_RejectsOtherServer+_RejectsClosedPortTestCopy_NoTrustWordnow also covers the new confirm constantsagent-gate init --forceend-to-end across 6 iterations against a dev binary; each issue reported by the user fixed and re-verifiedCommits in this PR