Skip to content

refactor(model): derive selectable model source - #1575

Merged
zerob13 merged 7 commits into
devfrom
codex/p1-selectable-model-source
Apr 30, 2026
Merged

refactor(model): derive selectable model source#1575
zerob13 merged 7 commits into
devfrom
codex/p1-selectable-model-source

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Apr 30, 2026

Copy link
Copy Markdown
Collaborator

Summary\n- derive chat-selectable model groups from the current provider catalog inside modelStore\n- keep model state aligned when providers are enabled, disabled, or removed\n- switch NewThreadPage, MCP sampling, and ChatStatusBar to the shared selectable model source\n\n## Testing\n- commit hook ran oxfmt and typecheck automatically during git commit\n

Summary by CodeRabbit

  • Refactor

    • Consolidated chat model selection and grouping for consistent model choice resolution
    • Centralized ACP config UI/state handling used by the chat status bar
  • New Features

    • Exposed chat model selection helpers for resolving preferred/sampling/query-based models
    • Added API to list agents
  • Improvements

    • More precise agent sync with scoped agent updates
    • Tighter provider enablement handling to reflect provider state
  • Bug Fixes

    • Provider removal now cleans provider-scoped model state to prevent stale data
  • Tests

    • Expanded unit tests for model selection, ACP config, provider cleanup, and agent sync

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab3989f3-fee9-4b0c-8b66-5e5b491d280e

📥 Commits

Reviewing files that changed from the base of the PR and between fd38274 and 61a52d3.

📒 Files selected for processing (8)
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/configPresenter/modelStatusHelper.ts
  • src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts
  • src/shared/contracts/routes/config.routes.ts
  • test/main/presenter/configPresenter/modelStatusHelper.test.ts
  • test/main/routes/contracts.test.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/stores/modelStore.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/renderer/stores/modelStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/main/presenter/configPresenter/modelStatusHelper.test.ts

📝 Walkthrough

Walkthrough

Centralizes chat model selection into new chat-selectable model groups and resolver utilities, extracts ACP chat-status UI/state into a composable, updates stores/components to consume the new APIs, and adds provider cleanup and agent-scoped agent-change routing/notifications.

Changes

Cohort / File(s) Summary
Model store & selection core
src/renderer/src/stores/modelStore.ts, src/renderer/src/lib/chatModelSelection.ts
Adds chatSelectableModelGroups, selection helpers (findChatSelectableModel, pickFirstChatSelectableModel) and new chat-model selection utilities/types (preferred/sampling/query resolvers).
Chat UI & ACP composable
src/renderer/src/components/chat/ChatStatusBar.vue, src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts
Moves ACP loading/caching/update logic into useChatStatusBarAcpConfig; ChatStatusBar now consumes composable and modelStore.chatSelectableModelGroups (removed local ACP state and filtering logic).
Pages & sampling
src/renderer/src/pages/NewThreadPage.vue, src/renderer/src/stores/mcpSampling.ts
Refactors model resolution to use new resolvers and chatSelectableModelGroups; sampling API signature changed to accept modelGroups and ChatModelSelection inputs.
Agent/UI store & events
src/renderer/src/stores/ui/agent.ts, src/shared/contracts/events/config.events.ts
Switches ACP sync to agent-driven updates (configClient.onAgentsChanged) and adds optional agentIds to config.agents.changed payload.
Presenter provider cleanup
src/main/presenter/configPresenter/index.ts, .../modelStatusHelper.ts, .../providerHelper.ts, .../providerModelHelper.ts
Adds provider cleanup hooks and implementations: deleteProviderModelStatuses, clearProviderModelStore, setCleanupHooks, and wiring on provider removal.
Routes / API / contracts
src/main/routes/config/configRouteHandler.ts, src/renderer/api/ConfigClient.ts, src/shared/contracts/routes/config.routes.ts, src/shared/contracts/routes.ts
Adds config.listAgents route and ConfigClient.listAgents, registers route, and updates route/catalog wiring.
Tests & mocks
test/renderer/**, test/main/presenter/**, test/renderer/lib/chatModelSelection.test.ts, test/main/routes/contracts.test.ts
Updates/adds many tests and test-local mocked stores to expose chatSelectableModelGroups, selection helpers, provider cleanup behavior, and new route/contract validations.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ChatStatusBar
  participant Composable as useChatStatusBarAcpConfig
  participant ModelStore
  participant ConfigClient
  Note over User,ChatStatusBar: User opens ACP options or selects a model
  User->>ChatStatusBar: open / select option
  ChatStatusBar->>Composable: request options / update option
  Composable->>ConfigClient: listAgents / setAcpSessionConfigOption
  ConfigClient-->>Composable: agents / update ack
  Composable->>ModelStore: query chatSelectableModelGroups / resolve model
  ModelStore-->>Composable: model groups / resolved model
  Composable-->>ChatStatusBar: updated ACP state / resolved {providerId,modelId}
  ChatStatusBar-->>User: reflect new selection/state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • zerob13

Poem

🐰 I hop through stores and tidy rows,
I group the models that the chatbox knows.
Composable hums, presenters prune,
Agents and models find a tune. 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main refactoring work: centralizing chat-selectable model sources within modelStore, which is the primary objective and foundational change enabling the downstream refactors in NewThreadPage, MCP sampling, and ChatStatusBar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/p1-selectable-model-source

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/stores/modelStore.ts (1)

350-366: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent removed providers from being re-materialized by late refresh writes.

pruneModelState (Line 385) removes deleted providers, but an in-flight refresh can still finish later and re-add them via updateCustomModelState / updateAllProviderState (Line 350 / Line 359), because those writers don’t validate provider existence.

Suggested guard to keep state aligned with current provider catalog
 const updateCustomModelState = (providerId: string, models: RENDERER_MODEL_META[]) => {
+  if (!getProviderState(providerId)) {
+    customModels.value = customModels.value.filter((item) => item.providerId !== providerId)
+    return
+  }
   const customIndex = customModels.value.findIndex((item) => item.providerId === providerId)
   if (customIndex !== -1) {
     customModels.value[customIndex].models = models
   } else {
     customModels.value.push({ providerId, models })
   }
 }

 const updateAllProviderState = (providerId: string, models: RENDERER_MODEL_META[]) => {
+  if (!getProviderState(providerId)) {
+    allProviderModels.value = allProviderModels.value.filter((item) => item.providerId !== providerId)
+    return
+  }
   const idx = allProviderModels.value.findIndex((item) => item.providerId === providerId)
   if (idx !== -1) {
     allProviderModels.value[idx].models = models
   } else {
     allProviderModels.value.push({ providerId, models })
   }
 }

Also applies to: 385-393, 1178-1213

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/stores/modelStore.ts` around lines 350 - 366, The
updateCustomModelState and updateAllProviderState functions can re-add providers
removed by pruneModelState; before updating or pushing into customModels or
allProviderModels, verify the provider still exists in the current provider
catalog (e.g., check providers.value for an entry with matching providerId) and
bail out if not found; apply the same guard pattern to any other writers that
mutate provider lists (locations referenced by pruneModelState and the block
around 1178-1213) so late refreshes cannot re-materialize deleted providers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/renderer/components/ChatStatusBar.test.ts`:
- Around line 305-311: The current findChatSelectableModel implementation only
searches the first group returned by getChatSelectableModelGroups() for a
matching providerId, so if a providerId appears in multiple groups it can miss
valid models; update findChatSelectableModel to search all groups with
providerId (e.g., iterate/filter all entries from getChatSelectableModelGroups()
where entry.providerId === providerId and then find the model with id ===
modelId) and return the providerName from the specific group that contained the
model, returning null only if no group contains that model.

In `@test/renderer/stores/modelStore.test.ts`:
- Around line 63-66: The test setup currently forces every provider to apiType:
'openai' by unconditionally overriding apiType in the providerRecords.map used
to build sortedProviders; change that map so it preserves an existing
provider.apiType and only defaults to 'openai' when apiType is missing (i.e.,
spread provider then set apiType to provider.apiType ?? 'openai' or equivalent),
so providerRecords and the derived sortedProviders retain provider-specific
apiType values for tests that rely on them.

---

Outside diff comments:
In `@src/renderer/src/stores/modelStore.ts`:
- Around line 350-366: The updateCustomModelState and updateAllProviderState
functions can re-add providers removed by pruneModelState; before updating or
pushing into customModels or allProviderModels, verify the provider still exists
in the current provider catalog (e.g., check providers.value for an entry with
matching providerId) and bail out if not found; apply the same guard pattern to
any other writers that mutate provider lists (locations referenced by
pruneModelState and the block around 1178-1213) so late refreshes cannot
re-materialize deleted providers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 282e8939-1117-45ed-be9c-5a60bac0d49a

📥 Commits

Reviewing files that changed from the base of the PR and between 41d3f6d and 3d497e5.

📒 Files selected for processing (9)
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/mcpSampling.ts
  • src/renderer/src/stores/modelStore.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/components/NewThreadPage.test.ts
  • test/renderer/pages/NewThreadPage.test.ts
  • test/renderer/stores/mcpSampling.test.ts
  • test/renderer/stores/modelStore.test.ts

Comment thread test/renderer/components/ChatStatusBar.test.ts Outdated
Comment thread test/renderer/stores/modelStore.test.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/presenter/configPresenter/index.ts`:
- Around line 1990-1992: The success path that sets agent install state
(getAgentRepositoryOrThrow().setAgentInstallState(registryAgent.id,
installedState)) only calls notifyAcpAgentsChanged([registryAgent.id]) and never
updates the live ACP provider; after install you should call
refreshAcpProviderAgents to ensure the enabled agent appears immediately. Modify
the success branch in the same block (after setAgentInstallState and
notifyAcpAgentsChanged) to also invoke refreshAcpProviderAgents() (or
refreshAcpProviderAgents([registryAgent.id]) if a targeted refresh overload
exists) so the ACP provider is refreshed right after installation completes.

In `@src/main/presenter/configPresenter/modelStatusHelper.ts`:
- Around line 235-250: deleteProviderModelStatuses currently skips removing
persisted keys if candidate.store (raw snapshot) is unavailable; change it to
fall back to another snapshot source and still remove persisted keys: inside
deleteProviderModelStatuses, after computing prefix and candidate, if rawStore
is falsy obtain a key list from the ElectronStore instance (e.g., use
Object.keys((this.store as any).store) or call a safe accessor on this.store to
get all keys), then iterate those keys and call this.store.delete for keys that
startWith(prefix), and finally call clearProviderModelStatusCache(providerId);
reference deleteProviderModelStatuses, MODEL_STATUS_KEY_PREFIX, candidate.store
and clearProviderModelStatusCache.

In `@src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts`:
- Around line 73-75: hasAcpConfigStateData currently returns false for valid
AcpConfigState objects with an empty options array; update it to treat an
AcpConfigState with options: [] as loaded by checking that state is
non-null/undefined and that state.options is an array (e.g.,
Array.isArray(state.options)) rather than checking truthiness of options.length;
apply the same fix to the other similar checks referenced (the isAcpConfigState
usage and the checks around the process fetch path) so empty option lists clear
loading state instead of falling back to cached data.
- Around line 91-92: The ACP config cache is currently keyed only by agent
(acpConfigCacheByAgent) but acpConfigRequestKey includes workspace
(process:${agentId}::${workdir}), so change the cache to use the same scoped
request key string for both reads and writes: derive a single key (reuse
acpConfigRequestKey) and use it when setting/getting acpConfigCacheByAgent and
when returning the cached fallback in syncAcpConfigOptions; update all places
that read/write the cache (including inside syncAcpConfigOptions and any other
spots that reference acpConfigCacheByAgent or seed from it) so the cache is
workspace-scoped and not collapsed across workdirs, leaving acpConfigSyncToken
logic unchanged.
- Around line 199-208: The status label helper getAcpOptionDisplayValue
currently returns option.currentValue for select options before
getAcpOptionCurrentLabel can provide a human-readable label; update
getAcpOptionDisplayValue to check option.type === 'select' and return
getAcpOptionCurrentLabel(option) ?? '' before falling back to returning a raw
string currentValue, keep the boolean branch as-is, and only use the raw
currentValue for non-select/string options so the status bar displays the select
option's label instead of internal/provider value.

In `@src/renderer/src/stores/ui/agent.ts`:
- Around line 138-139: Replace the hardcoded English error messages assigned to
error.value in the refresh logic (the lines setting error.value = `Failed to
refresh ${agentType} agents: ${e}` and the other similar assignment around the
refresh function) with vue-i18n keys and parameters: import or use the existing
i18n translator (e.g., useI18n() or i18n.t) and set error.value =
t('agents.refresh_failed', { type: agentType, error: e?.toString() }) (or the
project's chosen key name), ensuring you add the corresponding key to
src/renderer/src/i18n and pass agentType and error as params; update both
occurrences that reference error.value so all user-facing strings use i18n keys
instead of inline English text.

In `@src/shared/contracts/routes/config.routes.ts`:
- Line 18: AgentSchema currently uses z.custom<Agent>() which performs no
runtime validation; replace it with a proper Zod object schema by either
importing and reusing the existing AgentBootstrapItemSchema from common.ts or by
constructing a z.object(...) schema that matches the Agent shape, then use that
schema where AgentSchema is referenced (e.g., in the config.listAgents route) so
payloads are validated at runtime. Ensure the new AgentSchema has the same
required fields/types as AgentBootstrapItemSchema and update any imports/exports
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 68a11379-7d54-4848-8285-a04cbf40cd55

📥 Commits

Reviewing files that changed from the base of the PR and between 3d497e5 and d2b96a9.

📒 Files selected for processing (21)
  • src/main/presenter/configPresenter/index.ts
  • src/main/presenter/configPresenter/modelStatusHelper.ts
  • src/main/presenter/configPresenter/providerHelper.ts
  • src/main/presenter/configPresenter/providerModelHelper.ts
  • src/main/routes/config/configRouteHandler.ts
  • src/main/routes/legacyTypedEventBridge.ts
  • src/renderer/api/ConfigClient.ts
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts
  • src/renderer/src/lib/chatModelSelection.ts
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/mcpSampling.ts
  • src/renderer/src/stores/ui/agent.ts
  • src/shared/contracts/events/config.events.ts
  • src/shared/contracts/routes.ts
  • src/shared/contracts/routes/config.routes.ts
  • test/main/presenter/configPresenter/modelStatusHelper.test.ts
  • test/main/presenter/configPresenter/providerHelper.test.ts
  • test/main/presenter/configPresenter/providerModelHelper.test.ts
  • test/renderer/lib/chatModelSelection.test.ts
  • test/renderer/stores/agentStore.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/components/chat/ChatStatusBar.vue

Comment thread src/main/presenter/configPresenter/index.ts
Comment thread src/main/presenter/configPresenter/modelStatusHelper.ts
Comment thread src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts Outdated
Comment thread src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts Outdated
Comment thread src/renderer/src/components/chat/composables/useChatStatusBarAcpConfig.ts Outdated
Comment thread src/renderer/src/stores/ui/agent.ts
Comment thread src/shared/contracts/routes/config.routes.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/stores/modelStore.ts (1)

138-154: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Invalidate in-flight refreshes when a provider is removed.

purgeRemovedProviderState() clears the materialized groups, but an already-running refreshStandardModels / refreshCustomModels can still resume afterwards and write that provider straight back into allProviderModels, customModels, or enabledModels. Since the removal watch only fires on the catalog change itself, that resurrected state can stick until the next provider update. Please add a provider-generation/tombstone check so refresh code bails before writing state for a removed provider.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/src/stores/modelStore.ts` around lines 138 - 154,
purgeRemovedProviderState currently clears stored groups but doesn't prevent
concurrently-running refreshStandardModels/refreshCustomModels from re-writing a
removed provider; add a tombstone/generation mechanism: update
purgeRemovedProviderState to record a removal marker (e.g., increment a
providerGeneration entry or add providerId to a removedProviders set), and
modify refreshStandardModels and refreshCustomModels to check that
marker/generation before committing any writes to allProviderModels,
customModels, or enabledModels (and abort the write if the provider is marked
removed or generation mismatches). Use the existing providerId keys and the
functions providerModelQueries/customModelQueries/enabledModelQueries to locate
where writes occur so you can gate them with the tombstone/generation check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@src/renderer/src/stores/modelStore.ts`:
- Around line 138-154: purgeRemovedProviderState currently clears stored groups
but doesn't prevent concurrently-running
refreshStandardModels/refreshCustomModels from re-writing a removed provider;
add a tombstone/generation mechanism: update purgeRemovedProviderState to record
a removal marker (e.g., increment a providerGeneration entry or add providerId
to a removedProviders set), and modify refreshStandardModels and
refreshCustomModels to check that marker/generation before committing any writes
to allProviderModels, customModels, or enabledModels (and abort the write if the
provider is marked removed or generation mismatches). Use the existing
providerId keys and the functions
providerModelQueries/customModelQueries/enabledModelQueries to locate where
writes occur so you can gate them with the tombstone/generation check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1ae5431d-fc49-4d7f-96dc-f0985c84dd72

📥 Commits

Reviewing files that changed from the base of the PR and between d2b96a9 and fd38274.

📒 Files selected for processing (6)
  • src/main/presenter/configPresenter/index.ts
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/pages/NewThreadPage.vue
  • src/renderer/src/stores/modelStore.ts
  • test/renderer/components/ChatStatusBar.test.ts
  • test/renderer/stores/modelStore.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/main/presenter/configPresenter/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/renderer/stores/modelStore.test.ts

@zerob13
zerob13 merged commit f516e16 into dev Apr 30, 2026
3 checks passed
@zhangmo8
zhangmo8 deleted the codex/p1-selectable-model-source branch May 12, 2026 07:51
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.

2 participants