From cfb8495e853b84dbe28225818d23178cd5adc913 Mon Sep 17 00:00:00 2001 From: Ankit Gupta Date: Wed, 15 Jul 2026 20:05:13 -0700 Subject: [PATCH 1/4] consolidate background agent provider selection into General settings 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 --- src/renderer/components/SettingsPanel.tsx | 223 ++++++++++++---------- 1 file changed, 119 insertions(+), 104 deletions(-) diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index 102e9391..7dcf6555 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -223,7 +223,7 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) { // What the main process will actually launch for background drafts, given // the current provider gates — the same resolver prefetch/rerun use, so the - // fallback warning in the Agents tab can't drift from real behavior. + // fallback warning under the Agent Drafter row can't drift from real behavior. const effectiveBackgroundProvider = resolveBackgroundAgentProviderId({ backgroundAgentProvider, opencode: generalConfig?.opencode, @@ -334,12 +334,17 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) { return cleanup; }, []); + // Refetch config when the General tab is shown so provider gates flipped in + // the Extensions tab (OpenCode/Hostler enablement) are reflected in the + // Agent Drafter runtime options without reopening Settings. + useEffect(() => { + if (activeTab !== "general") return; + queryClient.invalidateQueries({ queryKey: ["general-config"] }); + }, [activeTab, queryClient]); + // Check Claude CLI availability and auth status when Agents tab is shown. - // Also refetch config so provider gates flipped in the Extensions tab - // (OpenCode/Hostler enablement) are reflected without reopening Settings. useEffect(() => { if (activeTab !== "agents") return; - queryClient.invalidateQueries({ queryKey: ["general-config"] }); setClaudeAuthStatus("checking"); ( window.api.agent.claudeAuthStatus() as Promise<{ @@ -441,6 +446,7 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) { syncDraftsToGmail, modelConfig, featureProviders, + backgroundAgentProvider, // Only send featureModels here — apiKey and defaultModel are owned by the // ExtensionsTab. Spreading the cached ollamaCloud here can carry a stale // empty apiKey from before the user saved one in ExtensionsTab; the backend @@ -1350,7 +1356,7 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) { { key: "agentDrafter" as const, label: "Agent Drafter", - description: "Background auto-draft generation", + description: "Background auto-drafts for new emails and “Regenerate draft”", }, { key: "agentChat" as const, @@ -1359,70 +1365,125 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) { }, ].map(({ key, label, description }) => { const provider = featureProviders[key] ?? "anthropic"; + // The Agent Drafter row doubles as the background-agent runtime + // picker: OpenCode/Hostler route background drafts to that agent + // provider (backgroundAgentProvider, model configured in the + // Extensions tab), while Anthropic/Ollama keep the built-in + // Claude agent and pick which model it uses. + const isBackgroundAgentRow = key === "agentDrafter"; + const externalRuntime = + isBackgroundAgentRow && backgroundAgentProvider !== "claude"; return (
-
-

- {label} -

-

{description}

-
-
- - {provider === "anthropic" ? ( +
+
+

+ {label} +

+

+ {description} +

+
+
- ) : ( - setOllamaModels((prev) => ({ ...prev, [key]: v }))} - ariaLabel={`Ollama model for ${label}`} - selectClassName="w-48 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-500 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - inputClassName="w-48 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-500 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - /> - )} + {externalRuntime ? ( + + Model set in Extensions + + ) : provider === "anthropic" ? ( + + ) : ( + setOllamaModels((prev) => ({ ...prev, [key]: v }))} + ariaLabel={`Ollama model for ${label}`} + selectClassName="w-48 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-500 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + inputClassName="w-48 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-500 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + )} +
+ {isBackgroundAgentRow && + effectiveBackgroundProvider !== backgroundAgentProvider && ( +

+ {backgroundAgentProvider === "opencode" + ? "OpenCode is disabled — background drafts fall back to Claude until it's re-enabled." + : backgroundAgentProvider === "hostler" + ? `Hostler is ${generalConfig?.hostler?.enabled ? "missing an API key" : "disabled"} — background drafts fall back to Claude until it's configured.` + : `"${backgroundAgentProvider}" isn't available — background drafts fall back to Claude until it's configured.`} +

+ )}
); })} @@ -2759,52 +2820,6 @@ export function SettingsPanel({ onClose, initialTab }: SettingsPanelProps) {

- {/* Background agent — which provider runs the automatic new-email drafter */} -
-
-
-

- Background Agent -

-

- The agent that automatically drafts replies for new emails (and powers - "Regenerate draft"). Enable OpenCode or Hostler in Settings → - Extensions to select them here. -

-
- -
- {effectiveBackgroundProvider !== backgroundAgentProvider && ( -

- {backgroundAgentProvider === "opencode" - ? "OpenCode is disabled — background drafts fall back to Claude until it's re-enabled." - : backgroundAgentProvider === "hostler" - ? `Hostler is ${generalConfig?.hostler?.enabled ? "missing an API key" : "disabled"} — background drafts fall back to Claude until it's configured.` - : `"${backgroundAgentProvider}" isn't available — background drafts fall back to Claude until it's configured.`} -

- )} -
- {/* Authentication */}

From 8a8777f392bc95c7b28866a9bdd974bf62a4742c Mon Sep 17 00:00:00 2001 From: Ankit Gupta Date: Wed, 15 Jul 2026 20:46:36 -0700 Subject: [PATCH 2/4] address review findings: state-sync clobber, save feedback, helper extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/renderer/components/ExtensionsTab.tsx | 24 ++- src/renderer/components/SettingsPanel.tsx | 149 ++++++++++++------- src/shared/types.ts | 40 +++++ tests/e2e/settings.spec.ts | 121 ++++++++++++++- tests/unit/background-agent-provider.spec.ts | 51 +++++++ 5 files changed, 329 insertions(+), 56 deletions(-) diff --git a/src/renderer/components/ExtensionsTab.tsx b/src/renderer/components/ExtensionsTab.tsx index 99968b99..e86e5e8e 100644 --- a/src/renderer/components/ExtensionsTab.tsx +++ b/src/renderer/components/ExtensionsTab.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useCallback } from "react"; +import { useQueryClient } from "@tanstack/react-query"; import type { InstalledExtensionInfo, ExtensionManifest, @@ -19,6 +20,11 @@ interface ExtensionListResult { * Shows bundled and installed extensions, with install/uninstall controls. */ export function ExtensionsTab() { + // Provider gates saved here (opencode.enabled, hostler.enabled/apiKey) feed + // the Agent Drafter runtime options in the General tab, which read from the + // "general-config" query — invalidate it at each save site so those options + // enable/disable without reopening Settings. + const queryClient = useQueryClient(); const [installedExtensions, setInstalledExtensions] = useState([]); const [bundledExtensions, setBundledExtensions] = useState([]); const [isInstalling, setIsInstalling] = useState(false); @@ -100,6 +106,7 @@ export function ExtensionsTab() { } setHostlerSaveState("saved"); + queryClient.invalidateQueries({ queryKey: ["general-config"] }); return true; } catch (error) { setHostlerSaveState("error"); @@ -859,12 +866,18 @@ export function ExtensionsTab() { onChange={async (e) => { const val = e.target.checked; setOpencodeEnabled(val); - await window.api.settings.set({ + const result = (await window.api.settings.set({ opencode: { enabled: val, model: opencodeModel || undefined, }, - }); + })) as { success: boolean } | undefined; + if (!result?.success) { + // Revert so the toggle doesn't show a state that didn't persist + setOpencodeEnabled(!val); + return; + } + queryClient.invalidateQueries({ queryKey: ["general-config"] }); }} />
@@ -892,12 +905,15 @@ export function ExtensionsTab() { ) : provider === "anthropic" ? (