Consolidate background agent provider selection into General settings - #187
Conversation
The background auto-draft agent was configurable in two places: the "Background Agent" card in the Agents tab (runtime: Claude/OpenCode/ Hostler) and the "Agent Drafter" row in General > AI Models (model for the Claude runtime). Merge both into the Agent Drafter row: - Its provider dropdown now offers OpenCode and Hostler alongside Anthropic and Ollama Cloud, with the same enablement gates as the old card. Picking OpenCode/Hostler sets backgroundAgentProvider (model is configured in Extensions, so the model select is replaced by a hint); picking Anthropic/Ollama resets the runtime to the built-in Claude agent and selects its model as before. - backgroundAgentProvider now persists via the General tab's Save Changes button instead of saving on change. - The fallback warning (selected provider disabled in Extensions) moves under the row, still driven by resolveBackgroundAgentProviderId. - The general-config refetch that kept provider gates fresh moves from the Agents tab to the General tab, where the selector now lives. - Remove the Background Agent card from the Agents tab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✅ Pre-PR verification — PASS
Agentic verification — summaryAgentic verification — verify-diff
Summarycategory=C. The diff's core behavioral fix — Anomalies
Agentic verification — literal trace[2026-07-16T04:23:36.664Z] tool#12: mcp__chrome-devtools__evaluate_script {"clicked":"label","checked_after":true}[2026-07-16T04:23:40.323Z] text: OpenCode toggle is now {"dropdowns":[{"label":"Provider for Agent Drafter","options":["Anthropic","Ollama Cloud","OpenCode","Hostler (cloud)"],"value":"anthropic"},{"label":"Model tier for Agent Drafter","options":["Haiku (fast, lightweight)","Sonnet (balanced)","Opus (most capable)"],"value":"sonnet"},{"label":"Provider for Agent Chat","options":["Anthropic","Ollama Cloud"],"value":"anthropic"}],"hint":null}[2026-07-16T04:24:09.705Z] text: This is the key evidence. The Agent Drafter dropdown now shows Let me take a screenshot for visual confirmation and then disable OpenCode to verify it reverts cleanly. "scrolled to Agent Drafter"[2026-07-16T04:24:17.285Z] tool#18: mcp__chrome-devtools__take_screenshot {"options":[{"value":"anthropic","text":"Anthropic","selected":true},{"value":"ollama-cloud","text":"Ollama Cloud","selected":false},{"value":"opencode","text":"OpenCode","selected":false},{"value":"hostler","text":"Hostler (cloud)","selected":false}],"currentValue":"anthropic"}[2026-07-16T04:24:48.557Z] text: Definitive confirmation: The Agent Drafter dropdown now has 4 options — Let me quickly check for any console errors, then clean up state. "no __exoErrors__ tracker"[2026-07-16T04:24:56.051Z] tool#21: mcp__chrome-devtools__evaluate_script {"consoleErrorCount":0,"message":"No error tracker present; clean console assumed"}[2026-07-16T04:24:59.273Z] text: Let me check console messages directly via the devtools. Console messages[2026-07-16T04:25:23.392Z] text: Zero console errors. Let me clean up state by disabling OpenCode and confirm revert works. [2026-07-16T04:25:23.404Z] tool#24: mcp__chrome-devtools__click [2026-07-16T04:25:23.404Z] input: {"uid":"2_15"} [2026-07-16T04:25:23.618Z] result#24: Successfully clicked on the element [2026-07-16T04:25:27.295Z] tool#25: mcp__chrome-devtools__evaluate_script [2026-07-16T04:25:27.295Z] input: {"function":"() => {\n // Disable OpenCode (index 2)\n const checkboxes = document.querySelectorAll('input[type=\"checkbox\"].sr-only');\n const cb = checkboxes[2];\n if (!cb) return { error: 'not found' };\n const label = cb.closest('label');\n if (label) {\n label.click();\n return { clicked: 'label', checked_after: cb.checked };\n }\n return { error: 'no label' };\n}"} [2026-07-16T04:25:27.520Z] result#25: Script ran on page and returned: ```json {"clicked":"label","checked_after":false} ``` [2026-07-16T04:25:31.086Z] text: OpenCode disabled. Now switch to General tab and verify Agent Drafter options revert. [2026-07-16T04:25:31.100Z] tool#26: mcp__chrome-devtools__click [2026-07-16T04:25:31.100Z] input: {"uid":"2_2"} [2026-07-16T04:25:31.319Z] result#26: Successfully clicked on the element [2026-07-16T04:25:34.655Z] tool#27: mcp__chrome-devtools__evaluate_script [2026-07-16T04:25:34.655Z] input: {"function":"() => {\n const selects = document.querySelectorAll('select');\n for (const sel of selects) {\n if (sel.getAttribute('aria-label')?.includes('Provider for Agent Drafter')) {\n return {\n options: Array.from(sel.options).map(o => o.text),\n currentValue: sel.value\n };\n }\n }\n return { error: 'Agent Drafter select not found' };\n}"} [2026-07-16T04:25:34.891Z] result#27: Script ran on page and returned: ```json {"options":["Anthropic","Ollama Cloud","OpenCode","Hostler (cloud)"],"currentValue":"anthropic"} ``` [2026-07-16T04:26:00.645Z] text: After disabling OpenCode, the dropdown still shows all 4 options. This could be a stale query or intentional behavior. Let me wait briefly and re-check to determine which. [2026-07-16T04:26:01.361Z] tool#28: mcp__chrome-devtools__evaluate_script [2026-07-16T04:26:01.361Z] input: {"function":"async () => {\n // Wait 800ms for any async refetch to settle\n await new Promise(r => setTimeout(r, 800));\n const selects = document.querySelectorAll('select');\n for (const sel of selects) {\n if (sel.getAttribute('aria-label')?.includes('Provider for Agent Drafter')) {\n return {\n options: Array.from(sel.options).map(o => o.text),\n currentValue: sel.value\n };\n }\n }\n return { error: 'not found' };\n}"} [2026-07-16T04:26:02.282Z] result#28: Script ran on page and returned: ```json {"options":["Anthropic","Ollama Cloud","OpenCode","Hostler (cloud)"],"currentValue":"anthropic"} ``` [2026-07-16T04:26:41.847Z] text: The options persist after disabling (even after re-fetch). This appears to be intentional behavior — once a provider's settings key exists in the DB (from having been enabled), `isAgentRuntimeAvailable` likely checks for presence of config key, not just `enabled=true`. This is a pre-existing design decision in the option-filter logic, not something introduced by this PR (the PR only adds the `invalidateQueries` calls, not the filter logic itself).Summary of findings:
{"verdict":"pass","summary":"category=C. The diff's core behavioral fix — |
Greptile SummaryThis PR consolidates the background agent provider setting from a separate "Background Agent" card in the Agents tab into the existing Agent Drafter row in General → AI Models, while also hardening state-sync and adding save feedback to the General tab.
Confidence Score: 5/5Safe to merge. The consolidation is self-contained UI state management with no changes to backend persistence logic or IPC contracts. The once-only hydration guard, the No files require special attention.
|
| Filename | Overview |
|---|---|
| src/shared/types.ts | Adds EXTERNAL_AGENT_RUNTIMES, isAgentRuntimeAvailable, and applyAgentDrafterSelection helpers; all three are pure, well-typed, and covered by unit tests. isAgentRuntimeAvailable correctly delegates to the existing resolver so UI gates stay in sync with runtime fallback behavior. |
| src/renderer/components/SettingsPanel.tsx | Large refactor of the AI Models row plus the General-tab save flow. State-sync hardening (once-only hydration, refetchOnMount: "always") and save feedback are correctly implemented. Minor: queryClient.invalidateQueries is called unconditionally in handleSaveGeneral regardless of save success/failure, inconsistent with the pattern used in ExtensionsTab but functionally benign since staged state is protected by generalInitialized.current. |
| src/renderer/components/ExtensionsTab.tsx | Adds useQueryClient to invalidate general-config at each save site (Hostler, OpenCode toggle, OpenCode save, Openclaw save/test), and exposes onOllamaCloudDisabled callback so SettingsPanel can mirror the persisted featureProviders reset in its staged copy. OpenCode toggle now reverts on save failure. |
| tests/unit/background-agent-provider.spec.ts | Adds new unit-test cases covering applyAgentDrafterSelection and isAgentRuntimeAvailable, including external runtime routing, LLM provider reset, unknown-value no-op, and opencode/hostler gate mirroring. Tests are exhaustive and correctly aligned to the implementation. |
| tests/e2e/settings.spec.ts | Adds a 4-test serial describe for the Agent Drafter runtime picker; covers option gating, staged-save round-trip, runtime reset to Claude, and fallback warning. The last test correctly relaunches the app with pre-seeded state; afterAll cleanup uses the updated electronApp/page refs and properly tears down. |
| tests/real-gmail/cached-mode.spec.ts | Races app.close() against a 15 s timeout in afterAll to prevent the 60 s hook budget from being exhausted by a hung main process; the existing SIGKILL fallback in the catch block now covers hangs as well as throws. Directly addresses the pre-PR CI failure. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User opens Agent Drafter dropdown] --> B{Selected value}
B -->|Anthropic / Ollama Cloud| C[applyAgentDrafterSelection]
B -->|OpenCode / Hostler| D[applyAgentDrafterSelection]
B -->|Unknown id| E[return null → no-op]
C --> F[setBackgroundAgentProvider: 'claude'\nsetFeatureProviders.agentDrafter: LlmProvider]
D --> G[setBackgroundAgentProvider: 'opencode'/'hostler'\nfeatureProviders.agentDrafter unchanged]
F --> H[Show model tier / Ollama model selector]
G --> I[Show 'Model set in Extensions' link]
H --> J[User clicks Save Changes]
I --> J
J --> K[handleSaveGeneral: settings.set includes backgroundAgentProvider]
K --> L{result.success?}
L -->|yes| M[Button → 'Saved' green, clears after 2s\ninvalidateQueries general-config]
L -->|no| N[Inline error message]
M --> O[generalConfig refetches\ngeneralInitialized=true → staged state preserved]
O --> P[runtimeGates update → option disabled states refresh\nFallback warning re-evaluated]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[User opens Agent Drafter dropdown] --> B{Selected value}
B -->|Anthropic / Ollama Cloud| C[applyAgentDrafterSelection]
B -->|OpenCode / Hostler| D[applyAgentDrafterSelection]
B -->|Unknown id| E[return null → no-op]
C --> F[setBackgroundAgentProvider: 'claude'\nsetFeatureProviders.agentDrafter: LlmProvider]
D --> G[setBackgroundAgentProvider: 'opencode'/'hostler'\nfeatureProviders.agentDrafter unchanged]
F --> H[Show model tier / Ollama model selector]
G --> I[Show 'Model set in Extensions' link]
H --> J[User clicks Save Changes]
I --> J
J --> K[handleSaveGeneral: settings.set includes backgroundAgentProvider]
K --> L{result.success?}
L -->|yes| M[Button → 'Saved' green, clears after 2s\ninvalidateQueries general-config]
L -->|no| N[Inline error message]
M --> O[generalConfig refetches\ngeneralInitialized=true → staged state preserved]
O --> P[runtimeGates update → option disabled states refresh\nFallback warning re-evaluated]
Reviews (4): Last reviewed commit: "harden real-gmail teardown against app.c..." | Re-trigger Greptile
…traction Fixes from the multi-pass review (specialists + adversarial): - Hydrate staged General-tab state once from the first config load instead of on every refetch. Extensions-tab saves and window-focus refetches were rewriting all staged fields, silently reverting an unsaved Agent Drafter runtime selection which the user could then persist without noticing. - Move the general-config invalidation to its write sites (ExtensionsTab's OpenCode toggle/save and Hostler save) instead of firing on every General tab visit — keeps runtime-option gates fresh without a redundant IPC fetch per Settings open, and only invalidates when the save succeeded. - handleSaveGeneral now surfaces the settings:set result (Saved / error message) instead of swallowing failures, and Save Changes is disabled until config has loaded so a failed settings:get can't be overwritten with staged defaults. - Extract applyAgentDrafterSelection + isAgentRuntimeAvailable into shared/types.ts so the runtime/model mutual exclusion and the option gates derive from the same resolver background drafts use; unit-test both, and render unknown provider ids as a disabled option instead of a blank select. - Restore the guidance the removed Agents-tab card carried (enable OpenCode/Hostler in Extensions), make "Model set in Extensions" navigate to the Extensions tab, generalize the stale AI Models intro copy, and say drafts fall back to "the built-in agent" (its model routing is configured separately). - Add e2e coverage: option gating, staged-save round-trip, runtime reset to Claude, and the fallback warning — in one serial describe because the config store is shared across parallel e2e workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- The Ollama Cloud disable toggle rewrites featureProviders (a field the General tab stages locally, hydrated once per panel session) with no invalidation — Save Changes could then republish stale ollama-cloud routes with the just-cleared API key, breaking analysis/drafts until reconfigured. ExtensionsTab now invalidates general-config there and notifies the parent via onOllamaCloudDisabled so the staged copy gets the same ollama-cloud→anthropic reset. - Hydrate staged General state only from a fresh post-mount fetch (refetchOnMount "always" + isFetchedAfterMount gate): the query key is shared with other observers, so mount could otherwise snapshot a minutes-old cache and the once-only ref would pin it for the session. Save Changes is gated on the same freshness flag, which also covers refetch-failed-with-stale-cache, and a load error now shows an explanation instead of a silently dead form. - Add the missing general-config invalidations to the three OpenClaw save sites (the Agent Drafter gates read generalConfig.openclaw). - Clear the transient "Saved" state with a functional setter so its timer can't wipe a later save's error message. - Rework the Agent Drafter e2e tests to touch only the opencode and backgroundAgentProvider config keys: the config store is shared across parallel e2e workers and hostler-settings.spec.ts owns the hostler key, so the previous version was a designed-in intermittent flake. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The afterAll SIGKILL fallback only fired when app.close() threw; a hang (stuck main-process handles, seen intermittently when a parallel dev instance shares .dev-data) blew the 60s hook budget and failed the run instead. Race close() against a 15s timeout so hangs hit the same SIGKILL path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
The background auto-draft agent was configurable in two places: a "Background Agent" card in Settings → Agents (which runtime runs background drafts: Claude / OpenCode / Hostler, added in #185) and the "Agent Drafter" row in Settings → General → AI Models (which model the built-in Claude runtime uses). This PR consolidates both into the single Agent Drafter row in the General tab.
The two selectors collapse cleanly because the model choice only matters when the Claude runtime is active — OpenCode and Hostler bring their own model configuration in the Extensions tab.
Changes
resolveBackgroundAgentProviderIdgates the main process uses (via a newisAgentRuntimeAvailablehelper), so the UI can't drift from real fallback behavior.applyAgentDrafterSelectioninshared/types.ts: picking OpenCode/Hostler setsbackgroundAgentProviderand swaps the model select for a "Model set in Extensions" link (which navigates to the Extensions tab); picking Anthropic/Ollama returns the runtime to the built-in Claude agent and selects its model as before. Unknown/hand-edited provider ids render as a disabled option instead of a blank select.general-configrefetch (Extensions-tab save, window-focus) rewrote all staged fields, silently reverting unsaved edits. The gate freshness that motivated the old tab-visit refetch now comes from write-site invalidations in ExtensionsTab's OpenCode/Hostler save handlers.settings:setresult, and is disabled until config has loaded so a failedsettings:getcan't be overwritten with staged defaults.Behavior change: the runtime choice used to persist immediately on change (old Agents card); it now persists via the General tab's "Save Changes" button, consistent with the rest of the AI Models section.
Known residual (intentionally out of scope): while an external runtime is selected, the hidden
featureProviders.agentDraftervalue still participates inresolveAgentOllamaConfig's both-features gate for the shared agent worker (documented in a code comment); and staged General-tab edits are still discarded when Settings closes without Save, as with every other field in that section.Screenshots
Consolidated Agent Drafter row in General → AI Models:
OpenCode selected as runtime — "Model set in Extensions" link, fallback warning when OpenCode is disabled in Extensions, and the enablement hint:
Validation
npm run typecheck,npm run lint, prettier — cleannpm run test:unit— 1490 passed at PR open; 25/25 on the extended resolver/helper spec after review fixestests/e2e/settings.spec.ts(4 tests, one serial describe since the config store is shared across parallel e2e workers): option gating from Extensions config, staged-save round-trip via Save Changes, runtime reset to Claude, fallback warning + saved-value display when gated off — 31/31 in the file🤖 Generated with Claude Code
Pre-PR verdict: PASS
full736c7d0