feat(agents): configurable default agent provider for background auto-drafts - #185
Conversation
…-drafts The automatic agent drafter that runs on every new email (and the 'Regenerate draft' rerun path) previously hardcoded the Claude provider. Add a backgroundAgentProvider config field with a Settings → Extensions dropdown so OpenCode or Hostler can run those background drafts instead. - resolveBackgroundAgentProviderId in shared/types.ts: pure resolver (same pattern as resolveAgentOllamaConfig) that falls back to claude when the chosen provider's config gates aren't met, mirroring each provider's isAvailable() check - prefetch-service + drafts:rerun-agent resolve the provider per launch; rerun-agent returns providerIds so the renderer tracks the right runs - renderer derives provider ids from agent events / persisted traces instead of hardcoding ["claude"] (the store drops events for unregistered provider ids) - Background agent card in Settings → Extensions with fallback warnings - unit tests for the resolver + schema Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ngs card Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile SummaryThis PR replaces the hardcoded
Confidence Score: 5/5Safe to merge — the fallback logic always defaults to Claude when a selected provider's gates aren't met, so no background draft path can be left without a working provider. All three renderer entry points that previously hardcoded the provider are now data-driven, the resolver is exercised by 11 unit tests, and the live agentic verification confirmed provider switching end-to-end. The one gap — that the UI's fallback warning doesn't mirror the main process's extra OpenCode credential check — results in a cosmetic discrepancy rather than functional breakage, and only occurs when OpenCode is enabled with no stored credentials at all. No files require special attention.
|
| Filename | Overview |
|---|---|
| src/shared/types.ts | Adds backgroundAgentProvider config field, DEFAULT_BACKGROUND_AGENT_PROVIDER, and resolveBackgroundAgentProviderId pure resolver with fallback for disabled OpenCode/Hostler/openclaw-agent; schema is free-text to allow future providers without a schema change. |
| src/main/ipc/settings.ipc.ts | Adds getBackgroundAgentProviderId() wrapping the pure resolver with an extra OpenCode LLM-credential check (Anthropic or Ollama key), and adds a type guard that rejects non-string backgroundAgentProvider values on settings:set. |
| src/main/services/prefetch-service.ts | Auto-draft launch now resolves provider per-run via getBackgroundAgentProviderId() instead of hardcoding "claude", with a log line recording the chosen provider. |
| src/main/ipc/drafts.ipc.ts | Rerun IPC handler resolves provider dynamically and returns providerIds in its response so the renderer can correctly track the task under the active provider. |
| src/renderer/components/SettingsPanel.tsx | Adds "Background Agent" card in the Agents tab with a dropdown gated on provider enablement, a resolver-derived fallback warning, and per-tab config invalidation to pick up Extensions-tab toggles. OpenCode's LLM-credential gate (checked in the main process) is not mirrored in the UI's effectiveBackgroundProvider computation. |
| src/renderer/components/AgentPanel.tsx | Regenerate path uses providerIds from the IPC response and strips stale providerConversationIds from the reused context; hasStatefulProvider now filters by active provider ids instead of checking all keys. |
| src/renderer/components/EmailPreviewSidebar.tsx | Trace replay derives provider ids from persisted events via deriveTraceProviderIds instead of hardcoding ["claude"], with fallback to Claude for legacy traces with no stamped events. |
| src/shared/agent-types.ts | Adds deriveTraceProviderIds helper that collects unique provider ids from a persisted trace in first-seen order, falling back to Claude for legacy unstamped traces. |
| tests/unit/background-agent-provider.spec.ts | 11 new unit tests covering resolver fallback, pass-through for unknown providers, schema parsing, and deriveTraceProviderIds edge cases (empty trace, mixed stamped/unstamped, multi-provider). |
| scripts/run-tests.sh | Removes the production exo config/data directories from the test cleanup list and adds a comment explaining that only the dev Electron binary's dirs should be cleaned. |
Sequence Diagram
%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant R as Renderer (SettingsPanel)
participant IPC as Main (settings.ipc)
participant Cfg as Config Store
participant Pf as PrefetchService
participant AC as AgentCoordinator
R->>IPC: "settings:set { backgroundAgentProvider }"
IPC->>Cfg: write (type-guarded)
Cfg-->>IPC: ack
Note over Pf: New email arrives
Pf->>IPC: getBackgroundAgentProviderId()
IPC->>Cfg: getConfig()
Cfg-->>IPC: config
IPC->>IPC: resolveBackgroundAgentProviderId(config)
IPC->>IPC: OpenCode LLM credential check
IPC-->>Pf: providerId (e.g. hostler)
Pf->>AC: runAgent(taskId, [providerId], prompt, ctx)
AC-->>R: agent:event (providerId stamped on each event)
Note over R: Auto-draft handler
R->>R: startAgentTask(taskId, emailId, [event.providerId ?? default])
Note over R: Regenerate draft
R->>IPC: "drafts:rerun-agent { emailId }"
IPC->>IPC: getBackgroundAgentProviderId()
IPC->>AC: runAgent(taskId, [providerId])
IPC-->>R: "{ taskId, providerIds: [providerId] }"
R->>R: startAgentTask(taskId, emailId, providerIds)
%%{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"}}}%%
sequenceDiagram
participant R as Renderer (SettingsPanel)
participant IPC as Main (settings.ipc)
participant Cfg as Config Store
participant Pf as PrefetchService
participant AC as AgentCoordinator
R->>IPC: "settings:set { backgroundAgentProvider }"
IPC->>Cfg: write (type-guarded)
Cfg-->>IPC: ack
Note over Pf: New email arrives
Pf->>IPC: getBackgroundAgentProviderId()
IPC->>Cfg: getConfig()
Cfg-->>IPC: config
IPC->>IPC: resolveBackgroundAgentProviderId(config)
IPC->>IPC: OpenCode LLM credential check
IPC-->>Pf: providerId (e.g. hostler)
Pf->>AC: runAgent(taskId, [providerId], prompt, ctx)
AC-->>R: agent:event (providerId stamped on each event)
Note over R: Auto-draft handler
R->>R: startAgentTask(taskId, emailId, [event.providerId ?? default])
Note over R: Regenerate draft
R->>IPC: "drafts:rerun-agent { emailId }"
IPC->>IPC: getBackgroundAgentProviderId()
IPC->>AC: runAgent(taskId, [providerId])
IPC-->>R: "{ taskId, providerIds: [providerId] }"
R->>R: startAgentTask(taskId, emailId, providerIds)
Reviews (4): Last reviewed commit: "fix(tests): stop clean_test_dbs from del..." | Re-trigger Greptile
✅ Pre-PR verification — PASS
Agentic verification — summaryAgentic verification — verify-diff
Summarycategory=A. The PR makes the background agent provider configurable instead of hardcoded 'claude'. Verified: (1) New 'Background Agent' combobox card renders in Settings → Agents showing 'Claude (Anthropic) — default'. (2) Config field Agentic verification — literal trace[2026-07-15T18:31:33.859Z] tool#8: mcp__chrome-devtools__evaluate_script ["General","Accounts","Blocked","Calendar","Splits","Snippets","Signatures","Prompts","Writing Style","Executive Assistant","AI Memories","Queue","Agents","Extensions","Analytics","Light","Dark","System","Default","Compact","Superhuman","Gmail","Off","5s","10s","15s","30s","Check for Updates","Export Logs","Save Changes","exoemailtest@gmail.com","Compose","Inbox","Sent","Priority16","Other12","Archive Ready11","Drafts5","All28"][2026-07-15T18:31:39.809Z] text: I can see there's an "Agents" tab. Let me navigate to it. "clicked Agents tab"[2026-07-15T18:31:45.061Z] tool#10: mcp__chrome-devtools__take_screenshot "not found"[2026-07-15T18:31:54.132Z] tool#12: mcp__chrome-devtools__take_snapshot Latest page snapshotuid=1_0 RootWebArea "Exo" url="http://localhost:5173/" {"headings":["Built-in","Sender Web Search","Calendar","Ollama Cloud","OpenClaw Agent","OpenCode Agent (experimental)","Hostler Agent (cloud)"],"hasHostler":true}[2026-07-15T18:32:54.805Z] text: Hostler extension exists. Let me now take the most direct verification path: confirm the IPC response for {"hasBackgroundAgentProvider":true,"backgroundAgentProvider":"claude","keys":["backgroundAgentProvider"]}[2026-07-15T18:33:01.249Z] text: The {"error":"Too few parameter values were provided"}[2026-07-15T18:33:08.514Z] tool#18: mcp__chrome-devtools__evaluate_script [{"id":"19e4aa7dfdd0c2dd","from":"Legal Team <legal@example.test>","subject":"Draft contract for review"},{"id":"19e42863f4e5037e","from":"Helen Martinez <helen@vendor.test>","subject":"Calendar invite: contract review"},{"id":"19e2a215cd25af3a","from":"Frank Lopez <frank@bigcustomer.test>","subject":"Re: Touching base — quick call next week?"}][2026-07-15T18:33:16.475Z] text: I have email IDs with drafts. Now let me call {"success":false,"hasTaskId":false,"hasProviderIds":false,"error":"Too few parameter values were provided"}[2026-07-15T18:33:20.673Z] text: Let me check how the rerunAgent IPC is exposed in the preload API. {"draftsKeys":["save","refine","rerunAgent","rerunAllAgents"],"agentKeys":["run","cancel","confirm","providers","authenticate","getTrace","claudeAuthStatus","claudeLogin","onEvent","onConfirmation","onProviders","onDraftSaved","onLocalDraftSaved","removeDraftSavedListeners","removeAllListeners"],"topLevelKeys":["_debugLog","gmail","analysis","drafts","compose","emails","style","contacts","search","settings","memory","sender","onboarding","accounts","sync","prefetch","backgroundSync","splits","snippets","theme","auth","extensions","archiveReady","snooze","network","scheduledSend","calendar","attachments","agent","defaultMailApp","updates","outbox","find","usage"]}[2026-07-15T18:33:26.496Z] tool#21: mcp__chrome-devtools__evaluate_script {"success":true,"hasProviderIds":true,"providerIds":["claude"],"taskId":"auto-draft-19e4aa7dfdd0c2dd-1784140406506"}[2026-07-15T18:33:34.712Z] text: {"traceExists":true,"eventCount":0,"hasContent":false}[2026-07-15T18:33:39.653Z] text: Agent just started — 0 events yet. Let me wait a few seconds and check again. {"traceExists":true,"eventCount":0}[2026-07-15T18:33:47.702Z] text: Let me close settings and check the email's agent panel directly in the UI. "closed"[2026-07-15T18:33:52.078Z] tool#25: mcp__chrome-devtools__evaluate_script "clicked legal email"[2026-07-15T18:33:55.078Z] tool#26: mcp__chrome-devtools__take_screenshot {"eventCount":0}[2026-07-15T18:34:21.848Z] text: The {"errorCount":0,"errors":[]}[2026-07-15T18:35:06.498Z] text: All checks complete. Here is my final assessment: Evidence collected:
{"verdict":"pass","summary":"category=A. The PR makes the background agent provider configurable instead of hardcoded 'claude'. Verified: (1) New 'Background Agent' combobox card renders in Settings → Agents showing 'Claude (Anthropic) — default'. (2) Config field |
Prettier formatting + fixes from Greptile and the adversarial/specialist review passes: - Gate the Hostler dropdown option on an API key being present, matching the resolver (Greptile P2) - Drive the settings-card fallback warning from resolveBackgroundAgentProviderId instead of hand-duplicated conditions, and add copy for non-opencode/hostler provider ids - Treat an empty-string backgroundAgentProvider as unset (|| not ??) so a hand-edited config can't wedge drafts on 'Unknown provider: ' - Gate openclaw-agent in the resolver (its config gates are knowable) - getBackgroundAgentProviderId() main-process wrapper adds OpenCode's credential gate (Anthropic key may come from process.env, which the renderer-safe resolver can't read) — enabling OpenCode without any LLM credential now falls back to claude instead of failing every draft - Guard settings:set against non-string backgroundAgentProvider values - Intersect follow-up providerConversationIds with the task's providerIds and drop them on regenerate, so a provider switch can't strip conversation history from Claude follow-ups - Extract deriveTraceProviderIds into shared/agent-types.ts (single pass) and use DEFAULT_BACKGROUND_AGENT_PROVIDER for renderer fallbacks - 8 new unit tests (19 total in background-agent-provider.spec.ts) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review triage summary (automated multi-pass review)Ran the full pre-landing review: critical-pass checklist, 4 specialist reviewers (testing, maintainability, security, performance), an adversarial chaos pass, plus Greptile. All fixes landed in a8721be. Fixed
Deliberately not fixed (with reasoning)
Codex cross-model review was attempted but the local Codex CLI install is broken (missing native binary), so adversarial coverage is Claude-only this round. 🤖 Generated with Claude Code |
The dropdown lived in Settings → Extensions next to the OpenCode/Hostler provider cards, but the Agents tab is where agent behavior is configured (authentication, browser automation, MCP servers) — the picker belongs there. It now sits at the top of Agent Settings. Provider gates derive from the react-query general-config data instead of copied state, and the config refetches whenever the Agents tab is shown so enablement toggled in the Extensions tab is reflected without reopening Settings. Verified live over CDP: option gating, selection persistence, fallback warning after disabling Hostler from another tab, and the card's removal from Extensions. PR screenshots updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
clean_test_dbs() listed the packaged app's userData dirs (~/Library/Application Support/exo and ~/.config/exo) alongside the dev Electron binary's dirs, and deleted exo-config.json from all of them on every test run. exo-config.json under the exo dir is the PRODUCTION electron-store config — real API keys, model config, signatures, EA settings. Tests launch via node_modules/electron whose userData dir is "Electron", so the exo dirs never held test state to begin with. Restrict cleanup to the Electron dirs only, with a comment explaining why the exo dirs must never be re-added. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note on the 🤖 Generated with Claude Code |
…#187) ## 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 - The Agent Drafter provider dropdown now offers **Anthropic, Ollama Cloud, OpenCode, and Hostler (cloud)**. Option enablement derives from the same `resolveBackgroundAgentProviderId` gates the main process uses (via a new `isAgentRuntimeAvailable` helper), so the UI can't drift from real fallback behavior. - The runtime/model mutual exclusion lives in a new pure helper `applyAgentDrafterSelection` in `shared/types.ts`: picking OpenCode/Hostler sets `backgroundAgentProvider` and 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. - The fallback warning (selected provider's gates unmet → drafts fall back to the built-in agent) moves under the row, plus a guidance hint (enable OpenCode/Hostler in Extensions) replacing the copy the old card carried. - Removed the Background Agent card from the Agents tab. - **State-sync hardening** (from review): General-tab staged state now hydrates once from the first config load — previously any `general-config` refetch (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. - **Save feedback** (from review): the General tab's Save Changes button now reports success ("Saved") and failure (inline error) instead of swallowing the `settings:set` result, and is disabled until config has loaded so a failed `settings:get` can'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.agentDrafter` value still participates in `resolveAgentOllamaConfig`'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 — clean - `npm run test:unit` — 1490 passed at PR open; 25/25 on the extended resolver/helper spec after review fixes - New e2e coverage in `tests/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 - Multi-pass review before push: 5 specialist reviewers + fresh-context adversarial pass; all CRITICAL/high findings fixed (state-sync clobber, save-result swallowing), informational findings either fixed (guidance copy, unknown-id select, intro copy, warning copy) or documented as residual above - Exercised end-to-end in the running app via CDP; screenshots above are from the final state 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- PRE-PR-REPORT-START SHA=736c7d0 mode=full --> **Pre-PR verdict**: PASS - mode: `full` - sha: `736c7d0` - generated: 2026-07-16T04:26:54.860Z | Phase | Status | Duration | |---|---|---| | eval:analyzer | ✅ exit 0 | 19.2s | | eval:features | ✅ exit 0 | 38.0s | | agentic-verify | ✅ exit 0 | 331.2s | | real-gmail:cached | ✅ exit 0 | 12.9s | <!-- PRE-PR-REPORT-END --> --------- Co-authored-by: Ankit Gupta <ankit@ycombinator.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Summary
The automatic agent drafter that runs on every new email (and the "Regenerate draft" rerun path) previously hardcoded the Claude provider. This PR adds a
backgroundAgentProviderconfig field and a "Background Agent" dropdown in Settings → Agents (next to the rest of the agent configuration) so OpenCode or Hostler can run those background drafts instead of Anthropic.Note: before this PR, enabling OpenCode/Hostler only made them selectable in the sidebar agent picker — there was no way to route the background drafter to them at all. This PR introduces that mechanism.
How it works
resolveBackgroundAgentProviderId()insrc/shared/types.tsis a pure resolver (same pattern asresolveAgentOllamaConfig) that returns the configured provider id, falling back toclaudewhen the chosen provider's config-level gates aren't met:opencoderequiresopencode.enabledhostlerrequireshostler.enabled+ a non-empty API key (mirrorsHostlerAgentProvider.isAvailable())The fallback exists so disabling a provider while it's selected doesn't strand every background draft on a dead backend.
The provider is resolved per launch via
getConfig(), so switching takes effect immediately — no worker restart or config propagation needed.The renderer store drops agent events whose provider id has no registered run, so the three renderer spots that hardcoded
["claude"]now derive the id from real data: the first streamed event (auto-draft auto-create inApp.tsx), the rerun IPC response (AgentPanel), and the persisted trace events (EmailPreviewSidebarreplay).Changes
src/shared/types.ts—backgroundAgentProviderconfig field,DEFAULT_BACKGROUND_AGENT_PROVIDER,resolveBackgroundAgentProviderId()src/main/services/prefetch-service.ts— auto-draft launches resolve the provider per run (+ log line with the chosen provider)src/main/ipc/drafts.ipc.ts—drafts:rerun-agentresolves the provider and returnsproviderIdsin the responsesrc/renderer/App.tsx— auto-draft tracking entry derives provider id from the first event instead of hardcoding["claude"]src/renderer/components/AgentPanel.tsx— regenerate usesproviderIdsfrom the rerun responsesrc/renderer/components/EmailPreviewSidebar.tsx— trace replay derives provider ids from persisted eventssrc/renderer/components/SettingsPanel.tsx— "Background Agent" card at the top of the Agents settings tab, with a dropdown (options gated on provider enablement) and a resolver-derived fallback warning; config refetches when the tab is shown so provider toggles made in the Extensions tab are reflected immediatelytests/unit/background-agent-provider.spec.ts— 11 unit tests for the resolver + schemaScreenshots
Default state (Claude selected; OpenCode disabled because it's off, Hostler selectable because it's enabled):
Hostler selected:
Fallback warning when Hostler is disabled while still selected:
Background draft running through Hostler (Agent tab shows "Completed · hostler" with the locally-executed read_email tool call):
Testing
npm run typecheck,npm run lint, fullnpm test(unit + integration + e2e): all pass locallyscripts/mock-hostler-server.mjs: selected Hostler via the real dropdown; the prefetch queue switched providers live (log lines flip fromusing provider claudetousing provider hostler, 16 mock sessions created);drafts:rerun-agentreturnedproviderIds: ["hostler"]and the run streamed 19 hostler-tagged events, executed the localread_emailtool, completed, and rendered in the Agent tab. The mock's canned script doesn't callgenerateDraft, so no draft body is expected from it — real hostler.dev would produce one.🤖 Generated with Claude Code
Pre-PR verdict: PASS
fullafb4751