Skip to content

feat(welcome): never block setup, recognise what already works - #109

Merged
0xMMA merged 6 commits into
mainfrom
feat/setup-wizard-polish
Oct 10, 2026
Merged

0xMMA merged 6 commits into
mainfrom
feat/setup-wizard-polish

Conversation

@0xMMA

@0xMMA 0xMMA commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Why

After going back from a dev-channel build to v4.5.0-beta, the setup wizard opened for the owner and would not let him past the API key field. At that point the Claude Code CLI was signed in and an Anthropic key was already in the keyring. His words: "das ist so ne art von bevormundung die jetzt nicht so sehr spaß macht".

The product rule now: no flow ever blocks the user, and configuration that already exists is recognised and never asked for again.

Root cause: what made completed_setup false

Investigated for a bounded time. The file was read (its log_level: debug took effect) and the keys survived, yet completed_setup was false.

Ruled out:

Paths that do write completed_setup=false:

  1. Settings → Reset to Defaults has no confirmation and does Save(Default()). That writes completed_setup:false, active_provider:openai and log_level:off, and leaves the keyring alone, so the keys survive. If the log level was then set back to debug, you get exactly the observed file. The guard runs only on app start, so the wizard first appears at the next launch, which here was the downgrade. Most likely.
  2. A whole-object Save after a failed Get. wails.service.ts loadSettings falls back to BROWSER_MODE_DEFAULTS (completed_setup:false), and the settings page saves the whole object. Possible, but I found no concrete reason for Get to fail in the real app.

To tell them apart in the affected settings.json: "active_provider":"openai" with "models":null and "app_presets":null means Reset. "claude" with {} and [] means the fallback save.

Fixed here:

  • Save (and therefore Reset to Defaults) no longer touches completed_setup. Only the welcome service moves it.
  • Reset to Defaults now also keeps the active provider. The keys survive a reset, so resetting the provider to OpenAI would leave a CLI or Anthropic user with a broken hotkey, and nothing would repair it once setup is complete. The loadSettings fallback is unchanged. With this PR it can no longer send anyone back to the wizard, but it can still overwrite other settings with defaults. That is worth a follow-up issue.

What changed

Backend: welcome.Service.IsFirstRun

  • First start (no settings file): always the wizard, with no CLI probe and so no wait.
    • An env key or a signed-in CLI belongs to other tools until the user lets KeyLint use it.
    • This is the one screen that shows the shortcut.
    • The wizard preselects what works, so it is one click.
  • Existing settings file with completed_setup=false (the reported case): returns true only if completed_setup is false and nothing is usable:
    • no key for OpenAI or Anthropic, from env or keyring (Bedrock's AWS_SECRET_ACCESS_KEY deliberately doesn't count);
    • no signed-in CLI;
    • no saved active provider that can work.
  • Key status only. A key value is never read into the decision or logged.
  • If something is usable, setup is marked complete silently through settings.Update. If the saved provider is a key provider with no key, empty, or unknown to this build, the active provider moves to what works, in the same order the wizard preselects: signed-in CLI, then a provider with a stored key. An explicit CLI, Ollama or Bedrock choice is never switched (the settings page explains Bedrock).
  • Start-up latency: the CLI is only asked when the answer depends on it. A key on the active provider, or Ollama, answers without a probe. The wait reuses the cached, coalesced settings probe and is capped at 5 s. A probe still running after that counts as unknown: it continues in the background and fills the cache for the wizard. Unknown on its own never counts as unusable when a key exists, and an explicit CLI choice with an unknown probe is kept.
  • Log lines (Info):
    • welcome: showing the setup wizard, nothing usable is configured with the active provider, key sources and CLI state;
    • or welcome: setup already usable, skipping the wizard with the reason, any switch, key sources and CLI state.

Backend: start-up diagnostics

  • settings.LoadOutcome records what NewService found (file found or not, path, completed_setup as loaded). main.go logs it right after logger.Init: settings: loaded at start-up … or settings: no file at start-up, using defaults. Those lines used to be emitted before the logger existed and were lost.
  • An unmarshal failure still exits the app before logging exists, unchanged.

Behaviour matrix (completed_setup=false)

Saved provider Keys CLI Result
no settings file any – (not probed) wizard, preselected with what works
any key provider none missing / signed out wizard
any key provider none probe > 5 s wizard (the CLI option shows "checking" and updates when the probe answers)
X key for X – (not probed) no wizard
openai Anthropic key missing no wizard, active → claude
openai none signed in no wizard, active → claude-code
openai Anthropic key probe > 5 s no wizard, active → claude
claude-code any signed in / probe > 5 s no wizard
claude-code a key signed out no wizard, choice kept (settings page warns)
claude-code none signed out wizard
ollama any – no wizard
unknown / empty a key or signed-in CLI no wizard, active → what works

With completed_setup=true there is never a wizard and nothing is asked.

Wizard (welcome-wizard.component.ts)

  • Two screens.
    • Screen 1: the actual shortcut drawn as keycaps, read from shortcut_fix, with one press animation that respects reduced-motion.
    • Screen 2: one radio list of all four providers, in the same order and with the same labels and status tags as Settings › AI Providers. The shared pieces live in core/providers.ts, which the settings page now uses too.
  • The CLI is a normal option. It shows "checking" while detection runs, is preselected when signed in, and asks for no key. Signed out or not installed, it says what to do and offers Check again.
  • Default selection is whatever already works: saved provider if it can run, then signed-in CLI, then a stored key. With nothing usable, nothing is preselected (no hard-coded OpenAI). A choice the user made is never moved by detection that lands later.
  • Key handling:
    • A stored key reads "A key is already saved for …" and lets you continue, with Replace it if you want to change it.
    • An env key names its variable.
    • A typed key is saved to the keyring before the provider is switched, then checked.
    • WailsService.setKey no longer swallows backend errors, so a keyring refusal shows up in the wizard. It also fixes the settings page's own "Failed to save key" path, which could never fire before.
    • Once a key is typed or "Replace it" is clicked, detection that lands later no longer moves the selection. Keep the saved key undoes "Replace it".
  • Set up later is on both screens. It completes setup without choosing anything and lands in the app.
  • finish() uses SetActiveProvider (one field) instead of saving a whole Settings object.

Tests

  • Go internal/features/welcome: a 16-case decision matrix covering nothing usable, key only, CLI only, probe timeout with and without a key, an explicit CLI or Ollama choice, the completed flag, and Bedrock. Also: the bounded probe wait, silent completion sticking, and CompleteSetup.
  • Go internal/features/settings: Save and Reset leave completed_setup alone; LoadOutcome.
  • Vitest welcome-wizard.component.spec.ts (22 tests, DOM-first). Covers: CLI preselected with no key field; the "checking" state; a late probe not moving the user's choice; nothing preselected when nothing works; a stored key lets you through; the env-key note; replacing a key; a typed key saved; a keyring refusal, including on replace; keep the saved key; the selection held while replacing; Ollama; Check again; Set up later; a save error; no whole-object save.
  • Playwright e2e/setup-wizard.spec.ts, using the extended fake-backend.ts, which now also answers IsFirstRun/CompleteSetup when a spec sets firstRun. Covers: new user plus key, signed-in CLI, stored key, Set up later.
  • Local runs:
    • npm test: 333 passed.
    • go test -race ./internal/...: ok.
    • go build: ok.
    • Full Playwright: 78 passed, 3 skipped. The skips are silent-fix.spec.ts, which is skipped by design.

Review (Opus, review-pr), fixed in the last two commits

  • An unknown or empty saved provider kept a broken provider and still completed setup. It now switches to what works.
  • A fresh install with an env key or a signed-in CLI skipped the wizard (and the shortcut intro) and adopted that credential unasked. A first start now always shows the wizard.
  • Reset to Defaults bypassed the repair path by setting OpenAI with setup complete. It now keeps the active provider.
  • The wizard switched the provider before saving the key, and a refused key replacement looked successful. Both fixed (key first, rejection surfaced).
  • The selection could jump while the user was replacing a key, and "Replace it" could not be undone. Both fixed.
  • Fake backend Save now keeps completed_setup like the real one. A stale binding comment, gofmt, the KEY_PROVIDERS reuse, shared shortcut formatting, and "Hold Ctrl and press G twice" wording are also done.
  • Left as is:
    • Choosing a CLI that isn't signed in, or Ollama, still lets you finish, with an inline hint. That is the no-blocking rule.
    • Linux-only: a locked keyring prompt can stall the decision. This is dev only; Windows wincred is unaffected.
    • The real probe hook-up in probeCLI is not unit-tested (it uses the existing cached settings probe).
    • The wizard is still one component (about 560 lines).

Manual test on Windows

  1. Silent completion. Quit KeyLint. In %APPDATA%\KeyLint\settings.json set "completed_setup": false and "log_level": "info", and keep an Anthropic key stored in the app. Start KeyLint.
    • Expected: no wizard.
    • debug.log has settings: loaded at start-up … completed_setup=false, then welcome: setup already usable, skipping the wizard reason="…" and keys="openai=none claude=keyring".
    • The file now says completed_setup: true.
  2. Same, but with no key stored and the Claude Code CLI signed in. Expected: no wizard, the log reason says the CLI is signed in, and the active provider is claude-code.
  3. First start. Rename %APPDATA%\KeyLint\settings.json and start. Expected: the wizard opens at once, without waiting on the CLI. If you have a key or a signed-in CLI, screen 2 has it preselected, so it is one click to finish. Put the file back afterwards.
  4. Wizard path. Delete the stored keys (Settings › AI Providers › Clear). Sign the CLI out, or rename it so it isn't found. Set completed_setup to false and start.
    • Expected: the wizard opens, and the log line names the failed checks.
    • On screen 2 nothing is preselected and the CLI row shows its status.
    • Set up later lands in the app.
  5. Reset to Defaults. In Settings, use Reset to Defaults and restart. Expected: no wizard, and the provider in use is unchanged.
  6. Visual check. Look over both wizard screens in the real window: keycaps, list, tags.

0xMMA added 6 commits October 9, 2026 22:30
IsFirstRun now opens the wizard only when nothing is usable: no key
for any provider, no signed-in CLI, no saved provider that can run.
Otherwise setup is marked complete silently and the log says why.
Save and Reset to Defaults no longer move completed_setup, and the
settings load outcome is logged once the logger is up.
Two screens: what KeyLint does, then which AI. The CLI is a normal
option with a loading state, what already works is preselected, a
stored key is recognised, and Set up later is always available.
A missing settings file now always opens the (non-blocking) wizard,
so env keys or a CLI are not adopted unasked. An unknown saved
provider moves to what works. Reset to Defaults keeps the active
provider so the surviving keys still match it.
setKey now rejects on a keyring error instead of swallowing it. The
wizard keeps a selection once a key is being typed or replaced and
can go back to the saved key. Shortcut formatting is shared.
Reset now asks inline, naming what goes back to defaults and what is
kept (API keys, the provider in use, completed setup). Cancel and
Escape leave everything untouched; focus starts on Cancel.
@0xMMA
0xMMA merged commit 474e0ad into main Oct 10, 2026
5 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