Skip to content

feat(settings): show and switch the provider in use on AI Providers - #104

Merged
0xMMA merged 20 commits into
mainfrom
feat/provider-visibility
Oct 5, 2026
Merged

0xMMA merged 20 commits into
mainfrom
feat/provider-visibility

Conversation

@0xMMA

@0xMMA 0xMMA commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Stacks on #94 (fix/ui-quick-bugs). Merge it after #94. Until #94 lands, the diff against main includes it; this PR's own change is the commits on top of it.

What the user now sees

Settings › AI Providers shows which provider is used, and that's where you switch it. This is the owner's decision: one source, clearly visible, one click to switch, no automatic fallback.

  • Top line: "Fix sends your text to Anthropic API. Pyramidize uses it too, unless you pick another provider there for this session. To switch, press Use this on another provider."
  • One card per provider: OpenAI, Anthropic API, Claude Code (installed CLI), Ollama (local), always in that order. Bedrock stays hidden.
    • The active card has an accent border and an In use tag.
    • Every other card has Use this. Its screen-reader name, "Use this: ", starts with the visible words (WCAG 2.5.3).
    • Each card name is a heading.
  • Use this: one click saves only the provider, through a new backend RPC, SetActiveProvider.
    • Anything else you've edited on the page stays pending until you press Save. That includes a Sensitive Logging toggle on the hidden General tab and a half-typed Ollama URL.
    • The pressed card shows a spinner. The marker moves only once the backend has the switch.
    • Focus then moves to the heading of the card now in use, and a polite live region announces "KeyLint now uses X."
    • The new provider's status is checked again right away: a fresh Claude Code CLI probe (the same check that skips the cache, as Re-check does) or a fresh key status. For Ollama the model list is reloaded, but the backend keeps a failed check for 30 s and that can't be skipped.
    • A successful switch clears an earlier Save error, and a successful Save clears an earlier switch error.
    • If the switch fails, the marker stays put and the error message announces itself (role="alert") once; the live region stays silent. Focus goes to the card still in use or, when no card is in use (Bedrock saved, or nothing), to the error.
  • A provider that can't work yet can still be chosen. Its card then explains why: no API key; CLI not installed or not signed in; Ollama not reachable (it names the address it tried) or no models pulled.
    • Only the card in use shows this warning. On the others, the status tags already say it.
    • Ollama is judged by whether it answers, not by whether the URL field is filled. An empty field means http://localhost:11434.
  • Env-var keys: a key from an environment variable is labelled as such on its card, with the variable named.
  • Ollama URL: the field now sits in the Ollama card. Its hint says it is saved with Save, not with Use this.
  • Locking: Use this, Save and Reset can't overlap; while one runs, the other buttons are disabled.
  • Save errors: a failed Save shows a message next to the button instead of an unhandled rejection.
  • Label: "Anthropic Claude" is renamed "Anthropic API". Next to "Claude Code", the old name was itself the source of the confusion.

General tab: a pointer instead of the dropdown

The Active Provider dropdown is gone. In its place is one read-only line: "Using: Anthropic API — change under AI Providers". "Change under AI Providers" is a button that opens that tab and moves focus to its summary line. If the provider in use can't work, the same warning shows on this line too. With a saved Bedrock it reads "No provider in use", followed by one warning that explains why.

Why a pointer rather than nothing: General is the tab Settings opens on. A pointer keeps "which one is used" on the first screen and shows where to change it, while the choice itself still lives in only one place.

Pyramidize follows Settings

Before this, Pyramidize took the provider from settings once per app session. A switch made later in Settings was silently ignored until restart.

Now it re-reads active_provider on every visit, unless you picked a different provider in Pyramidize during this session. When your pick differs from Settings, a note under the select says so: "Session override — Settings uses X. Use X". The link drops the override.

  • Picking the provider Settings uses also counts as no override.
  • An override ends once Settings catches up with it. From then on, later switches in Settings are followed again.
  • If Settings names a provider that can't work (a saved AWS Bedrock) and you pick another, that is not an override. The note says why ("Settings names AWS Bedrock, which is not available yet, so Pyramidize uses this provider for now") and has no dead-end link back. That pick holds only until Settings names something else. This doesn't rely on settings:changed, which is registered but never emitted. Pyramidize's labels now match Settings ("Anthropic API", "Ollama (local)"). The welcome wizard keeps its own labels.

Backend

  • settings.Update(svc, mutate) holds saveMu from the read to the write. It is a package function, so Wails does not bind it.
    • It is used by SetActiveProvider, welcome.CompleteSetup, and Pyramidize's SetAppPreset, DeleteAppPreset and SetQualityThreshold.
    • Before, each of these ran Get-then-Save outside the lock, so a switch landing in between could be undone. A Go test shows a switch arriving mid-update now survives.
  • SetActiveProvider(id) accepts only the providers the screen offers: not Bedrock, unknown IDs or an empty string.
  • Save writes the file before it swaps the in-memory settings. Before, a failed write still switched Fix's provider while the screen reverted.
  • No more truncated settings.json. os.WriteFile truncates first, so a write that stopped partway (full disk, or a crash mid-write) left a cut-off settings.json that load() refuses to start on. Now the data goes to a temp file, which is synced and then renamed over the original; until that rename the old file is untouched.
    • On Unix the rename is atomic.
    • On Windows Go does not promise atomicity (os.Rename uses MoveFileEx with replace). What holds there is that the old file stays whole until a complete new one replaces it.
    • An antivirus lock was never a truncation risk: WriteFile failed when opening the file.
  • On Windows the rename retries for about 2 s on ERROR_ACCESS_DENIED or ERROR_SHARING_VIOLATION, the same approach as Go's cmd/internal/robustio (x/tools/go/packages: TestOverlay/Modules is flaky on windows-386-2008 builder golang/go#31247). The retry is build-tagged, so other platforms do a single rename. A virus scanner, the search indexer or a concurrent KeyLint -fix can briefly hold the file open. Tests use a swappable rename to cover the retry, the time limit, and failed renames leaving no temp file.
  • The missing-Anthropic-key error no longer points at the removed dropdown.
  • wire gen produces no change. wails3 generate bindings adds SetActiveProvider, plus doc-comment changes.

Follow-ups (existing problems, not fixed here)

  • fix.component.spec.ts › "output textarea is empty before a fix is run" fails when the whole suite runs in shuffled order (vitest sequence.shuffle, seed 7). The cause is FixComponent's module-level state; this PR doesn't touch FixComponent. With seeds 1 and 42, and with every changed spec shuffled on its own, everything passed.

Also in this PR: preset changes made earlier in the same visit are now copied into the page's settings, so the next Save no longer undoes them.

Screenshots (local, Playwright against a fake settings backend)

Before and after, both tabs, with one provider (CLI only) and with two (Anthropic key plus CLI). After shots also cover "not ready" and a Pyramidize session override.

  • Before, two providers: both the CLI card and the Anthropic key card show green status tags, and nothing on the tab says which one is used. The answer was a dropdown on a different tab.
  • After: exactly one card has the accent border and the In use tag, and its name is in the top line. With no key, the warning shows on the card and on the General line.
  • The look is otherwise unchanged: same card frame, PrimeNG tags and buttons, dark tokens.

Review

Three Opus reviews. Every finding was checked against the code and fixed, with two exceptions: there is no Ollama Re-check button, and there is no way to skip the backend's 30 s cache of a failed Ollama check. The specs that touch Pyramidize reset its module-level provider state before and after each test. Before that, unavailable-provider.spec.ts passed only because of the order its tests ran in.

Verification (local)

main's CI Linux jobs stay red until #103 (empty apt cache) merges, so these results are local.

  • npm test: 15 files, 271 tests, 0 failures.
    • Run on their own: the changed settings (106), Pyramidize (47), unavailable-provider (5), cli-probe-startup (3) and shell-routing (4) specs.
    • The settings, Pyramidize and unavailable-provider specs were also each run on their own in shuffled order (2 seeds).
  • npm run build: OK. The only warning is the style budget on text-enhancement, which was already there.
  • npx playwright test: 67 passed, 3 skipped (the existing skipped specs). New since the last round: a refused switch (the fake backend now validates the ID and can be made to fail).
    • New e2e/provider-switch.spec.ts covers switching, persistence across a reload, a provider that isn't ready, Pyramidize following a switch, and the General pointer.
    • New e2e/support/fake-backend.ts answers the settings service's binding calls from in-memory state. It's documented in .claude/rules/testing.md.
  • go test -race ./internal/...: pass. GOOS=windows go vet and go build: OK. go build -o bin/KeyLint .: OK.
  • git status is clean after the runs.

Conflicts with #31 (frozen, feature/shortcut-double-press)

git merge-tree --write-tree --name-only origin/feature/shortcut-double-press HEAD reports no conflicts.

Release builds are stamped with the git tag (v3.6.0), so the template's
hardcoded "v" produced "vv3.6.0". versionLabel() (core/version-label.ts)
now adds the prefix only to a bare version number: tags and git-describe
output keep their own "v", a dev build shows "dev" rather than "vdev",
and a lone commit hash from `git describe --always` stays as it is. The
About tab uses the same label, so both places read alike.

Closes #21
Bedrock is a stub that only returns "not yet supported", yet it sat in
the Active Provider dropdown with its own key field. It is gone from
both, and README, CLAUDE.md and the website no longer list it. The Go
stub and the settings value stay so #23 can bring it back.

A settings file that already names bedrock is left as it is: switching
it would send the user's text to a service they never chose. Settings
and Pyramidize both show "Choose a provider" with a note that AWS
Bedrock is not available yet. Pyramidize also stays disabled, skips the
key banner and model probe for it, and so never reaches the raw
"unsupported provider" error. Picking any provider clears the note.

Closes #22
Only the dark theme is styled. Choosing Light dropped PrimeNG into its
unstyled light mode, and System did nothing. The theme control is gone
(a dropdown with one entry is not a choice), and applyTheme() keeps
.app-dark whatever theme_preference says, so a value saved by an older
version cannot unstyle the app either. The field stays in the settings
model and is saved unchanged, for the light theme in #25.

The dark-mode E2E spec gains a test that serves a stored "light" through
the Wails runtime endpoint and asserts the page stays dark with no theme
control.

Closes #24
The Sensitive Logging row puts a long hint next to its switch, and the
flex row squeezed the switch (30px at 1280px wide, 10px at 600px), so
the knob slid past the track's edge. The switch no longer shrinks; a
Playwright test at a 600px viewport asserts every toggle row's switch
keeps its 40px width.

Closes #26
PrimeNG declares component tokens on :root as aliases of the theme
tokens (the select panel's background is var(overlay background)), and
an alias resolves on the element that declares it. With .app-dark on
<body>, <html> never saw the dark theme, so every aliased token came out
light: the disabled Pyramidize global-instruction input (slate-200),
every p-select dropdown panel (white), tab bars, and the dim slate key
labels on the AI Providers tab.

The class now sits on <html> (index.html, applyTheme). The existing
overrides in styles.scss are kept so no colour that was right changes;
the card background moves to :root.app-dark so PrimeNG's own :root card
token cannot raise the settings card off the page. Disabled pInputText,
pTextarea and p-select keep a dark zinc-900 surface with muted text,
rather than Aura's zinc-700 block. Two dark-mode E2E tests pin the
disabled-input and dropdown-panel colours.

Closes #28
Apply wore the same accent as the Pyramidize button even while it was
disabled, so a button that could do nothing competed with the page's
main action. It is now secondary while the global instruction is empty
or whitespace, and turns primary as soon as the user types one.

Closes #29
With a saved AWS Bedrock provider, the Pyramidize button was already
guarded, but the Editor canvas is editable before any Pyramidize:
typing a global instruction enabled Apply, and Apply, Ctrl+Enter or the
selection bubble ended in a raw "unsupported provider: bedrock" error.
Apply now stays secondary and disabled, the bubble's Apply is disabled,
and both methods return early. The note on the page now points to
Settings, since a choice made on the page lasts for the session only.

Also: the settings spec's no-AWS-key test asserts the provider keys that
must be there, so an unrendered tab cannot pass it, and the lookup uses
Object.hasOwn so an inherited property name cannot match.

Refs #22
Every provider card on the AI Providers tab now says whether KeyLint
uses it and offers a one-click switch. A provider that cannot work yet
stays selectable and its card says why. The General tab's dropdown is
replaced by a read-only pointer to the tab where the choice is made.
An environment-supplied key now names its variable on the card.
Save swapped the live settings before writing the file, so a failed
write still switched the provider for Fix and Pyramidize while the
settings screen put its marker back. Also reword the missing Anthropic
key error, which pointed at the removed Active Provider dropdown.
Keep presets added this visit, hold Save and Reset during a switch,
drop a stale Ollama warning when the URL changes, move focus to the
marked card or the AI Providers tab, and word the summary so it does
not promise more than Pyramidize's own session choice delivers.
SetActiveProvider saves the provider alone under saveMu, validated
against the providers the screen offers. Settings are written to a temp
file and renamed over settings.json, so an interrupted write can no
longer leave a truncated file that stops the app from starting.
Use this calls SetActiveProvider, so other pending edits stay pending.
Switch, Save and Reset lock each other; Save shows its errors. The
marker moves once saved, focus goes to the card heading and a live
region announces it. Pyramidize re-adopts the Settings provider on every
visit unless the user picked another there, which it now shows as a
session override with a way back.
settings.Update holds saveMu from read to write; SetActiveProvider,
CompleteSetup and the preset and threshold writers use it, so none can
undo a switch landing meanwhile. On Windows the final rename retries
for about 2 s while another process holds settings.json open.
Pyramidize drops an override once Settings agrees with it and treats a
pick forced by an unusable Settings provider as no override. A failed
switch is announced once and focuses the error when no card is in use.
A switch refreshes the target's status and clears a stale Save error.
Specs reset Pyramidize's module state, so test order no longer matters.
@0xMMA

0xMMA commented Oct 4, 2026

Copy link
Copy Markdown
Owner Author

Ready to merge after #94, which it stacks on. Three independent Opus review rounds, each with review-pr fork supplement; all findings addressed (a272639). I checked the last round's new Go code myself: settings.Update holds saveMu across read and write, and the atomic write plus the Windows rename retry clean up their temp file on every error path. Verified locally: Go race tests, 271 Vitest, 67 Playwright. CI Linux jobs stay red until #103 merges (empty apt cache); merge main in afterwards for a green run and the Windows artifact.

@0xMMA

0xMMA commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

@0xMMA
0xMMA merged commit aff2b5e into main Oct 5, 2026
4 checks passed
0xMMA added a commit that referenced this pull request Oct 8, 2026
Brings in settings atomic writes and settings.Update, provider switching
(#104), the dev channel (#107) incl. workflows, the shell footer (#106),
Go 1.27 and the CI apt fixes. Conflicts: shell keeps the subscription
list and silent-fix guard next to the dev-channel update title; wails
service keeps setShortcutPaused next to the dev-channel calls; settings
imports ToggleSwitch and SliderModule. Bindings regenerated. E2E settings
stubs gain the shortcut fields the backend now returns.
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