Skip to content

fix(init): wizard UX overhaul + reuse an already-running dashboard#32

Merged
WZ merged 5 commits into
mainfrom
fix/init-host-confirm
May 7, 2026
Merged

fix(init): wizard UX overhaul + reuse an already-running dashboard#32
WZ merged 5 commits into
mainfrom
fix/init-host-confirm

Conversation

@WZ
Copy link
Copy Markdown
Owner

@WZ WZ commented May 7, 2026

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 init wizard

  1. Hint 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.

  2. Confirm step after the multiselect. New PromptHostsConfirm lists what's selected and asks Yes, 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.

  3. 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?\".

  4. 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.

  5. Cert-install defaults to Install. var ok bool zero-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.

  6. Back navigation. New ErrPromptBack sentinel; runner.go is a stage machine that catches it and rewinds. Three-list note and policy summary now show Continue / Back two-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.

  7. Init-complete message now mentions agent-gate dashboard as the long-running variant alongside agent-gate run, so the "keep the UI open" workflow is discoverable.

agent-gate run

  1. Reuse an already-running dashboard. When the dashboard port bind fails, the supervisor now probes the port: if the response looks like an agent-gate dashboard (HTML body containing "agent-gate"), it logs dashboard already running at http://... — reusing it and 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

  1. The host-confirm copy initially said "Pre-trust these hosts?" / "Don't pre-trust any host?", leaking "trust" into wizard prose the project deliberately keeps clear (see 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 ./... clean
  • go vet ./... clean
  • gofmt -l . clean
  • TestRunner_HostsConfirmReject_LoopsBackToMultiselect
  • TestRunner_BackFromThreeList_RewindsToWelcome
  • TestRunner_BackFromPolicySummary_RedoesCustomHosts
  • TestCustomHostsConfirmText_FirstIteration_NoTally + _SubsequentIteration_ShowsTally
  • TestPolicySummaryDescription_ListsEachHost + _EmptyHosts_NoListedNames
  • TestIsAgentGateDashboard_RecognizesOurSignature + _RejectsOtherServer + _RejectsClosedPort
  • Existing TestCopy_NoTrustWord now also covers the new confirm constants
  • Dogfood agent-gate init --force end-to-end across 6 iterations against a dev binary; each issue reported by the user fixed and re-verified

Commits in this PR

  • `f72f73e fix(init): teach the host-multiselect with a hint and a confirm step`
  • `838937c fix(init): show running tally on the custom-hosts loop`
  • `01db36b fix(init): list hosts in summary, default Install, Y/N gate custom hosts, back nav`
  • `2d81f1e feat(run): reuse an already-running dashboard instead of failing the bind`

WZ added 2 commits May 7, 2026 14:18
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.
@WZ WZ changed the title fix(init): make host-multiselect discoverable + confirm before advancing fix(init): teach the host prompts (toggle hint + confirm + running tally) May 7, 2026
WZ added 2 commits May 7, 2026 15:05
…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.
@WZ WZ changed the title fix(init): teach the host prompts (toggle hint + confirm + running tally) fix(init): wizard UX overhaul + reuse an already-running dashboard May 7, 2026
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.
@WZ WZ merged commit 718048d into main May 7, 2026
4 checks passed
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