-
Notifications
You must be signed in to change notification settings - Fork 614
refactor: split the Setting Store into multiple single-responsibility stores. #1101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughRefactors configuration management: moves provider/model/system-prompt/UI settings logic into new main-process helpers and replaces the monolithic renderer settings store with focused Pinia stores; updates components to use new stores; removes inline Content-Security-Policy meta tags from multiple renderer HTML files. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Renderer App
participant Init as initAppStores()
participant UI as UiSettingsStore
participant Prov as ProviderStore
participant Model as ModelStore
participant Presenter as ConfigPresenter
participant Helper as Helper classes (Provider/Model/SystemPrompt/Status)
participant IPC as Main<->Renderer IPC
App->>Init: initAppStores()
Init->>UI: loadSettings()
Init->>Prov: initialize providers
Prov->>Presenter: request providers
Presenter->>Helper: ProviderHelper.getProviders()
Helper-->>Presenter: providers
Presenter-->>Prov: providers (via IPC)
Init->>Model: initialize models
Model->>Presenter: request provider models
Presenter->>Helper: ProviderModelHelper.getProviderModels()
Helper-->>Presenter: provider models
Presenter-->>Model: provider models (via IPC)
rect rgb(220,240,200)
Note over Model,Prov: Event-driven updates
Helper->>IPC: emit CONFIG_EVENTS / MODEL_STATUS_CHANGED
IPC-->>Model: MODEL_LIST_CHANGED
IPC-->>Prov: PROVIDER_CHANGED
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas to focus during review:
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (42)
src/main/presenter/configPresenter/providerModelHelper.ts (2)
15-26: AlignModelConfigResolvertyping with actual usage and sync expectation
ModelConfigResolveris typed to always return aModelConfig, butgetProviderModelstreatsconfigas possibly absent:const config = this.getModelConfig(model.id, providerId) if (config) { // ... } else { // ... }and the helper assumes a synchronous resolver (it immediately reads
config.maxTokens, etc.).To avoid subtle bugs and type gaps:
- Either change the type to
type ModelConfigResolver = (modelId: string, providerId?: string) => ModelConfig | undefined, or- If a config must always exist, drop the
if (config)branch and enforce that invariant at the resolver.Also ensure that the concrete resolver you pass here is synchronous and does not return a Promise; otherwise this code will start reading properties off a
Promiseat runtime.Also applies to: 57-63
57-82: Consider avoiding in-place mutation of ElectronStore model objects
getProviderModelsandgetCustomModelsmutate theMODEL_METAobjects returned fromstore.get(...)before returning them. This works, but it ties callers to ElectronStore’s internal object identity.For clearer separation and safer future changes, consider returning new objects instead:
return models.map((model) => { const config = this.getModelConfig(model.id, providerId) const base = { ...model } // apply overrides to `base` instead of mutating `model` return base })Similar treatment can be applied in
getCustomModels.Also applies to: 89-99
src/renderer/settings/components/OllamaProviderSettingsDetail.vue (3)
288-305: Verify that Ollama model names align withRENDERER_MODEL_META.id
providerModelMetasis keyed bymeta.id, but you later look up metadata using the Ollama model’sname:const metaMap = new Map( providerModelMetas.value.map((meta) => [meta.id, meta]) ) const meta = metaMap.get(model.name)This assumes that, for the Ollama provider,
RENDERER_MODEL_META.idequals the Ollama modelname. If IDs are prefixed (e.g.ollama/gemma3:4b) whilemodel.nameis justgemma3:4b,metawill always beundefinedand you’ll silently lose persisted flags likeenabled,vision, etc.Please double-check that:
modelStore.allProviderModelsexposes IDs that matchlocalModels’namevalues for Ollama, or- Otherwise, adjust the keying to use a shared canonical ID (e.g.
meta.ollamaModel?.nameor a normalized key function).Also applies to: 315-325, 759-793
135-152: Ensure model status updates use the same ID convention as model metadataHere you pass
model.nameinto the status API:<ModelConfigItem :model-id="model.meta?.id ?? model.name" ... @enabled-change="handleModelEnabledChange(model.name, $event)" />const handleModelEnabledChange = async (modelName: string, enabled: boolean) => { await modelStore.updateModelStatus(props.provider.id, modelName, enabled) }If
modelStore.updateModelStatusand the underlying status helper use the canonical model ID (oftenmeta.id), but you pass only the displaymodel.name, status entries may not align with the metadata used elsewhere (especially if IDs are prefixed or normalized differently).It would be safer to:
- Pass
model.meta?.id ?? model.nameintohandleModelEnabledChange, and- Use that same identifier consistently for status/config operations.
Please verify that the expected identifier for
updateModelStatusmatches what you pass here.Also applies to: 767-793, 870-873
314-316: Update outdated comment; provider/ollama store usage looks goodThe logic now correctly routes:
- Model refresh and locality checks through
ollamaStore,- Status updates through
modelStore, and- API updates, key validation, and provider deletion through
providerStore.The inline comment “使用 settings store” above
refreshModelsis now misleading, since the settings store is no longer used—consider updating or removing it. Otherwise, the refactor here looks coherent.Also applies to: 850-853, 903-904, 908-909, 916-917, 920-923, 945-948
src/main/presenter/configPresenter/modelStatusHelper.ts (1)
5-12: Clarify contract betweenstoreandsetSettingfor status persistence
ModelStatusHelperreads fromstore:const status = this.store.get(statusKey) as boolean | undefinedbut writes via the injected
setSettingfunction:this.setSetting(statusKey, enabled)while
deleteModelStatususesthis.store.delete(statusKey).This works if and only if
setSettingultimately writes into the same ElectronStore instance passed asstore(e.g.,setSetting = store.set.bind(store)). IfsetSettingis wired to a different store or abstraction, reads and writes could diverge.It would be good to:
- Document that
setSettingmust target the same storage backend asstore, or- Simplify by using
this.store.set(...)directly insidesetModelStatusfor symmetry withgetModelStatus/deleteModelStatus.Also applies to: 68-72, 108-112
src/renderer/src/stores/providerStore.ts (1)
199-223:optimizeProviderOrderignores theenableflag; simplify or use it meaningfullyIn
optimizeProviderOrder, both branches of the ternary build the samenewOrder:const newOrder = enable ? [...enabledOrder, providerId, ...disabledOrder] : [...enabledOrder, providerId, ...disabledOrder]So the
enableparameter currently has no effect on ordering. Either:
- This is intentional and
enablecan be dropped from the function signature, or- You meant to position the provider differently when disabling (e.g. append to the disabled section).
Clarifying this and removing the redundant ternary (or implementing distinct behavior per branch) would make the intent clearer and avoid confusion for future readers.
src/main/presenter/configPresenter/providerHelper.ts (2)
118-127: Consider makingproviderIdoptional or using a sentinel value for reorder operations.The reorder operation sets
providerId: '', which is semantically unclear since reordering affects the entire provider list rather than a single provider. Consumers of thePROVIDER_ATOMIC_UPDATEevent might be confused by an empty string.Consider either:
- Making
providerIdoptional in theProviderChangeinterface for operations likereorderthat don't target a single provider- Using a well-documented sentinel value (e.g.,
'__ALL__'or'*')- Creating a separate event type for reorder operations
51-60: Improve error handling insetProviderById.When a provider is not found, the method only logs an error to the console but doesn't throw or return an error indication. This makes it difficult for callers to detect and handle failures.
Consider one of these approaches:
setProviderById(id: string, provider: LLM_PROVIDER): void { const providers = this.getProviders() const index = providers.findIndex((p) => p.id === id) if (index !== -1) { providers[index] = provider this.setProviders(providers) } else { - console.error(`[Config] Provider ${id} not found`) + throw new Error(`[Config] Provider ${id} not found`) } }Or return a boolean:
-setProviderById(id: string, provider: LLM_PROVIDER): void { +setProviderById(id: string, provider: LLM_PROVIDER): boolean { const providers = this.getProviders() const index = providers.findIndex((p) => p.id === id) if (index !== -1) { providers[index] = provider this.setProviders(providers) + return true } else { console.error(`[Config] Provider ${id} not found`) + return false } }src/renderer/settings/components/ProviderModelList.vue (1)
142-143: Add basic error handling around async modelStore operationsThe switch to
modelStorelooks correct, but the async calls aren’t guarded:
handleModelEnabledChangeawaitsmodelStore.updateModelStatuswithout atry/catch, so any rejection becomes an unhandled promise and there’s no user feedback.- If
enableAllModels/disableAllModelsare async in the store, they’ll also silently fail on rejection.Consider wrapping these in
try/catchand at least logging, to match the pattern inconfirmAdd/handleDeleteCustomModel:-const handleModelEnabledChange = async (model: RENDERER_MODEL_META, enabled: boolean) => { - emit('enabledChange', model, enabled) - await modelStore.updateModelStatus(model.providerId, model.id, enabled) -} +const handleModelEnabledChange = async (model: RENDERER_MODEL_META, enabled: boolean) => { + emit('enabledChange', model, enabled) + try { + await modelStore.updateModelStatus(model.providerId, model.id, enabled) + } catch (error) { + console.error('Failed to update model status:', error) + } +}And, if
enableAllModels/disableAllModelsare async, you might mirror that pattern there as well.Also applies to: 155-156, 233-249, 251-272
src/renderer/src/stores/systemPromptStore.ts (1)
18-27: Ensure store state stays in sync after default/clear/save operations
addSystemPrompt,updateSystemPrompt,deleteSystemPrompt, andsetDefaultSystemPromptIdall reload vialoadPrompts, but:
setDefaultSystemPromptresetToDefaultPromptclearSystemPrompt- (and arguably
savePrompts)don’t update
prompts/defaultPromptIdlocally after calling the presenter. That can leave the store’s reactive state out of sync with persisted state until someone callsloadPrompts()elsewhere.To keep consumers consistent, consider either:
- Calling
await loadPrompts()at the end of these methods, or- Updating
prompts/defaultPromptIddirectly in these methods in parallel with the presenter calls.Example:
const setDefaultSystemPrompt = async (content: string) => { await configP.setDefaultSystemPrompt(content) + await loadPrompts() } const resetToDefaultPrompt = async () => { await configP.resetToDefaultPrompt() + await loadPrompts() } const clearSystemPrompt = async () => { await configP.clearSystemPrompt() + await loadPrompts() }This keeps the store as the single source of truth for the UI.
Also applies to: 28-39, 40-59
src/renderer/src/views/WelcomeView.vue (1)
5-7: Provider/model store migration looks correct; consider reusing computed models and guarding final navigationThe migration here generally looks good:
providerStore.providersis now the single source for provider metadata, and step‑1 logic correctly derivesisOllamaApifrom it.- Models are pulled from
modelStore.allProviderModels, andhandleModelEnabledChangenow updates model status throughmodelStore.updateModelStatus, which is consistent with the new store architecture.initAppStores()inonMountedgives you providers/models populated before the user proceeds through the steps.Two small follow‑ups you might consider:
- Reuse the
providerModelscomputed in the templateYou already have:
const providerModels = computed(() => { return ( modelStore.allProviderModels.find((p) => p.providerId === selectedProvider.value)?.models ?? [] ) })But the template’s v‑for recomputes the same find:
<ModelConfigItem v-for="model in modelStore.allProviderModels.find( (p) => p.providerId === selectedProvider )?.models" ... />For clarity and to avoid duplication, you can just loop over
providerModels:- <ModelConfigItem - v-for="model in modelStore.allProviderModels.find( - (p) => p.providerId === selectedProvider - )?.models" + <ModelConfigItem + v-for="model in providerModels" :key="model.id" ... />
- Defensively handle missing models on final navigation
In the final step, you access
providerModels.value[0].idwithout a length check. If model loading fails or returns an empty list, this will throw:router.push({ name: 'chat', query: { modelId: providerModels.value[0].id, providerId: selectedProvider.value } })A simple guard (fallback to a default model or showing an error dialog) would make the flow more robust.
Overall, the store refactor in this view looks solid.
Also applies to: 80-82, 121-130, 141-158, 183-209, 211-214, 216-223, 271-331, 363-375
src/renderer/src/lib/storeInitializer.ts (1)
34-69: Guard Electron IPC access in MCP deeplink handler for non‑Electron contexts
useMcpInstallDeeplinkHandlerassumeswindow.electron.ipcRendereris always available:window.electron.ipcRenderer.on(DEEPLINK_EVENTS.MCP_INSTALL, handleMcpInstall) ... window.electron.ipcRenderer.removeAllListeners(DEEPLINK_EVENTS.MCP_INSTALL)This will throw in tests, Storybook, or any non‑Electron/web builds where
window.electronis absent, unlike other code paths that use optional chaining.To make it safer and consistent with patterns used elsewhere:
const setup = () => { - window.electron.ipcRenderer.on(DEEPLINK_EVENTS.MCP_INSTALL, handleMcpInstall) + if (!window?.electron?.ipcRenderer) return + window.electron.ipcRenderer.on(DEEPLINK_EVENTS.MCP_INSTALL, handleMcpInstall) } const cleanup = () => { - window.electron.ipcRenderer.removeAllListeners(DEEPLINK_EVENTS.MCP_INSTALL) + if (!window?.electron?.ipcRenderer) return + window.electron.ipcRenderer.removeAllListeners(DEEPLINK_EVENTS.MCP_INSTALL) }This keeps the deeplink behavior intact in Electron while avoiding crashes in other environments.
src/renderer/settings/components/AnthropicProviderSettingsDetail.vue (1)
367-457: Store migration looks correct; consider tightening provider auth updates and error surfacingThe migration from the old settings store to
useProviderStore/useModelStoreis consistent:
- Computeds (
enabledModels,totalModelsCount,providerModels,customModels) correctly guard against missing provider entries with|| []/|| 0.- Auth-mode transitions use
providerStore.updateProviderAuthindetectAuthMethod,switchAuthMethod,submitOAuthCode, anddisconnectOAuth, which keeps the persisted state in sync.- Model actions (
disableAllModels,updateModelStatus) and provider removal (removeProvider) use the new stores as intended.Two small follow-ups you may want to consider:
detectAuthMethodcallsupdateProviderAuththree times in the fallback path withundefinedfor the credential; if the helper already defaults the third argument, you could omit it or centralize that logic to avoid redundant writes.- All provider/model mutations are wrapped in
try/catchwithconsole.erroronly; if you want better UX, wiring these into the existing toast/error pipeline would give users feedback when provider operations fail.If you’d like, I can draft a small helper to encapsulate the “detect auth + persist” flow, or hook these errors into your toast system. Please also double‑check that
updateProviderAuthandupdateProviderApiare defined with optional parameters as used here inproviderStore. Based on learnings.Also applies to: 471-487, 551-594, 610-618, 620-632, 648-652, 686-698, 709-712
src/renderer/settings/App.vue (1)
60-61: Settings window store initialization and UI store migration look soundThe changes here are coherent:
initializeSettingsStoressequencesproviderStore.initialize(),modelStore.initialize(), optionalollamaStore.initialize?.(), and the search assistant/engine setup, with a catch‑allconsole.errorto avoid breaking the mount flow.- Using
void initializeSettingsStores()inonMountedis appropriate since errors are handled inside.- UI font size now consistently comes from
uiSettingsStore.fontSizeClassand is applied via the watcher, whileuiSettingsStore.loadSettings()is kicked off on mount.No correctness issues stand out. The only optional tweak would be to document that
initializeSettingsStoresis intentionally fire‑and‑forget and safe to call once at settings window startup.Please confirm that each of the called store methods (
initialize,initOrUpdateSearchAssistantModel,refreshSearchEngines,setupSearchEnginesListener,loadSettings) are idempotent or guarded against multiple invocations, since this function may be reused from other entry points in the future.Also applies to: 72-92, 117-145, 147-158, 195-199, 256-267
src/renderer/src/App.vue (1)
12-13: App‑level UI settings and store/deeplink initialization are wired correctly; watch body/html class usageKey points in this refactor:
- Replacing the legacy settings store with
useUiSettingsStorefor font sizing and usinguiSettingsStore.fontSizeLevel+updateFontSizeLevelin the zoom handlers is consistent with the new UI‑settings abstraction.- The watcher on
[themeStore.themeMode, uiSettingsStore.fontSizeClass]correctly keepsdocument.documentElementin sync with theme and font size, while initial classes are applied inonMounted.initAppStores()anduseMcpInstallDeeplinkHandler()are integrated cleanly: init is invoked once on mount, andcleanupMcpDeeplink()is called during unmount alongside the IPC listener cleanup.Two small things to keep an eye on:
- The initial theme/font-size classes are added to
document.body, but the watcher maintains them ondocument.documentElement. If your CSS expects them consistently on<html>, you might want to normalize that for the initial state too.- Zoom handlers don’t clamp the font size locally; this relies on
uiSettingsStore.updateFontSizeLevelto validate the bounds. If that’s not already enforced, adding clamping there would prevent extreme values.Please confirm that
updateFontSizeLevelenforces valid bounds and that your global styles read theme/font-size from the same element (htmlvsbody) for both initial and updated states.Also applies to: 22-23, 30-31, 45-65, 147-163, 195-207, 313-332
src/renderer/src/stores/searchAssistantStore.ts (1)
1-141: New search assistant store is well‑structured; consider tightening validation and error handlingThe
useSearchAssistantStoredesign looks solid:
- State and computeds (
modelRef/providerRef,searchAssistantModel,searchAssistantProvider) are straightforward.findPriorityModelcorrectly prioritizes models based on theprioritieslist and falls back to the first enabled Chat/ImageGeneration model with a provider.setSearchAssistantModelkeeps Pinia state, config (configP.setSetting), andthreadPresenterin sync.initOrUpdateSearchAssistantModelandcheckAndUpdateSearchAssistantModelensure there’s always a valid assistant model, reacting to changes inenabledModelsvia the deepwatch.A couple of robustness tweaks you might want to add:
Config presenter error handling
BothsetSearchAssistantModelandinitOrUpdateSearchAssistantModelassumeconfigP.getSetting/setSettingsucceed. If these can throw or reject (e.g., disk issues), wrapping them intry/catchwill prevent store consumers from seeing unhandled rejections.Model existence check could also include providerId
IncheckAndUpdateSearchAssistantModel, you only checkmodel.id. If there’s any chance of id collisions across providers or a model moving providers, you could tighten this to also matchproviderId:const stillAvailable = enabledModels.value.some((provider) => provider.providerId === providerRef.value && provider.models.some((model) => model.id === currentModel.id) )This keeps the selection aligned with both model and provider.
Optional: avoid re‑normalizing when using saved config
For the saved case, you directly trust the stored payload, which is fine. If you later add schema changes, you may want a small normalization step similar to the one you use for the priority‑selected path.Overall, this store is a good fit for the new single‑responsibility architecture; the above are just to harden it for edge cases.
Please confirm how
configPresenter.getSetting/setSettingbehave on failure, and whether model ids are guaranteed unique across providers; that will determine how necessary the stricter checks are here.src/renderer/src/components/NewThread.vue (1)
149-149: Consider keepingartifactsin sync with UI settings
artifactsis initialized fromuiSettingsStore.artifactsEffectEnabledbut never updated if that setting changes while this component is mounted. If users can toggle this globally from elsewhere, adding a smallwatchonuiSettingsStore.artifactsEffectEnabledto updateartifacts.valuewould avoid subtle desyncs.src/renderer/src/stores/modelConfigStore.ts (1)
1-59: Model config store is well‑factored; consider cache invalidation hooksThe store cleanly wraps
configPresenterwith a simple per‑(provider,model) cache and covers the common operations (get/set/reset, provider listing, import/export). Implementation is correct and idiomatic for Pinia.Two optional improvements you might consider:
- Cache invalidation:
cacheis only updated bygetModelConfig/setModelConfig/resetModelConfig. If other parts of the app still callconfigPresenter.setModelConfig/resetModelConfig/importModelConfigsdirectly, this store could serve stale configs. Subscribing toCONFIG_EVENTS.MODEL_CONFIG_CHANGED/MODEL_CONFIG_RESET/MODEL_CONFIGS_IMPORTEDin this store and clearing or updating relevant cache entries would make it more robust.- Default provider cache key: Using
'default'ingetCacheKeyfor missingproviderIdis fine, but if a real provider with id'default'ever appears, keys could collide. A slightly more defensive prefix like'__default__'would avoid that edge case.src/renderer/settings/components/prompt/SystemPromptSettingsSection.vue (1)
241-274: Reset/create/update/delete flows are consistent with centralized system prompt managementThe reset, delete, and create/update handlers now all call the appropriate
systemPromptStoremethods (updateSystemPrompt,deleteSystemPrompt,addSystemPrompt,setDefaultSystemPromptId) and then refresh vialoadSystemPrompts(). That keeps this UI thin and avoids duplicating persistence logic.As a minor improvement, you could consider reusing the shared
SystemPrompttype from@shared/presenterinstead ofSystemPromptItem, to avoid drift if shape changes, but that’s cosmetic.Also applies to: 276-291, 305-335
src/renderer/settings/components/BedrockProviderSettingsDetail.vue (1)
250-259: Model status operations correctly usemodelStore; watcher is a possible optimization
- Validation now calls
providerStore.checkProviderand thenmodelStore.refreshProviderModels, which fits the new split of concerns.- Enabling/disabling a single model and disabling all models now use
modelStore.updateModelStatusandmodelStore.disableAllModels, matching store responsibilities and keeping status mutations centralized.As a minor optimization, the deep watcher on
modelStore.allProviderModelscould potentially be relaxed (e.g., watching a shallow version or an explicit version key) if this array becomes large, but the current approach is functionally correct.Also applies to: 271-274, 276-283, 293-303, 309-316
src/renderer/src/stores/mcp.ts (2)
415-427: setMcpEnabled flow is robust but has some duplicated refresh responsibilitiesThe overall sequence—optimistic local toggle, main-process mutation, forced config refetch, server startup/status sync, and then forced tool/client/prompt reloads—is sensible and avoids the UI lag that existed before. Two points to consider:
- Some responsibilities overlap with
syncConfigFromQuery(both can clear state and refresh queries on enable/disable), which makes the flow a bit harder to reason about when changes are driven from other windows vs this one.- The extra delayed
setTimeoutrefresh after enabling is defensively correct but adds complexity; if backends guarantee tool registration by the timeupdateAllServerStatusescompletes, this could be simplified.Neither is a correctness bug, but factoring the shared “after enable/disable” behavior into a single helper (used by both
setMcpEnabledandsyncConfigFromQuery) would make the flow easier to maintain.Also applies to: 429-481
485-497: Server status updates now respect global mcpEnabled flagGating
updateAllServerStatuses/updateServerStatusonconfig.value.mcpEnabledand clearing statuses when disabled aligns the UI with the global toggle and avoids pointless IPC when MCP is off. The logic that merges server-specific tools intoenabledMcpToolswhen a server is running, and prunes invalid tools when it stops, reads correctly and should keep chat config in sync.Only minor note: with global MCP disabled it’s still technically possible to start/stop servers via
toggleServer, butupdateServerStatuswill treat them as not running. If that’s intentional (global off hides server state), this is fine; otherwise you may want to guardtoggleServeronmcpEnabledin the UI.Also applies to: 500-505, 518-524
src/main/presenter/configPresenter/uiSettingsHelper.ts (1)
1-69: UiSettingsHelper encapsulation and event emission look correct; consider harmonizing getter shapesThe helper cleanly wraps
getSetting/setSettingwith strongly named methods and emits the appropriateCONFIG_EVENTS.*to all windows on mutation, which matches the intended architecture. Two minor design notes:
getSearchPreviewEnabledis asynchronous (Promise<boolean>) while the other getters are synchronous. If this isn’t constrained by existing callers, you might consider aligning them to a single style (all sync or all async) for consistency.- Default behaviors (
falsefor content protection / copy-with-COT,truefor notifications when unset) seem reasonable, but worth double-checking against previous defaults to avoid surprising users on upgrade.Functionally, though, the implementation is sound.
src/renderer/settings/components/common/SearchEngineSettingsSection.vue (2)
207-212:currentEnginecomputed works, but could be simplifiedThe
currentEnginecomputed correctly resolves the selected engine fromsearchEngineStore.searchEnginesand falls back tonull. You could slightly simplify it by omitting the explicit|| nullsincefindalready returnsSearchEngineTemplate | undefined, but that’s purely cosmetic.
229-262: Custom engine add/delete flows correctly delegate to store but duplicatesetSearchEnginecallsThe refactor from mutating
settingsStore.searchEnginesin-place to:
- updating custom engines via
configPresenter.setCustomSearchEngines(...), thenawait searchEngineStore.refreshSearchEngines(), and- finally
await searchEngineStore.setSearchEngine(...)is a solid separation of concerns.
However, because you also assign
selectedSearchEngine.valueand have awatch(selectedSearchEngine, async ...)that callssearchEngineStore.setSearchEngine(newValue), each path ends up invokingsetSearchEnginetwice for the same id (once via the watcher, once explicitly). It’s harmless but unnecessary work.You could rely on the watcher alone by removing the explicit
setSearchEnginecalls inaddCustomSearchEngineanddeleteCustomSearchEngineonceselectedSearchEngineis updated.Also applies to: 278-303
src/renderer/settings/components/ModelProviderSettings.vue (2)
217-219: Drag-and-drop reordering now delegates to providerStore appropriatelyUsing
providerStore.sortedProvidersas the base list forallEnabledProviders/allDisabledProviders, and callingproviderStore.updateProvidersOrder(allProviders)in the computed setters, preserves the previous reordering behavior with the new store. The filtered-vs-unfiltered handling to maintain correct indices during search is careful and reads correctly.If
updateProvidersOrderis asynchronous, you might eventually want to make these setters async andawaitthe call, but given likely synchronous store behavior this is not a blocking issue.Also applies to: 221-239, 241-259
294-296: Active provider resolution now based on providerStore.providersUsing
providerStore.providers.find((p) => p.id === route.params.providerId)is the right source of truth post-refactor. If you ever see inconsistent behavior whenproviderIdcan be an array, you may want to normalize it toString(route.params.providerId), but for normal routing this should be fine.src/main/presenter/configPresenter/systemPromptHelper.ts (5)
8-11: DEFAULT_SYSTEM_PROMPT string has a minor typoThe last sentence is missing a space:
"languages.Always"→"languages. Always". Consider fixing to avoid shipping this typo in the default text.
6-7: Confirm getSetting/setSetting contracts align with presenter implementations
GetSetting/SetSettingare typed as synchronous functions returning values directly. IfconfigPresenter.getSetting/setSettingarePromise‑based (as in the renderer stores), passing them here would violate the contract and could lead to subtle bugs.Either:
- Keep these types strictly sync and ensure callers pass sync wrappers, or
- Widen the types to accept async functions and
awaitthem in this helper.Please double‑check the actual presenter signatures and adjust if needed.
Also applies to: 13-19
32-39: Consider falling back to DEFAULT_SYSTEM_PROMPT when no default is set
getDefaultSystemPromptreturns''when there is noisDefaultprompt anddefault_system_promptisn’t set. Given you have aDEFAULT_SYSTEM_PROMPTconstant and aresetToDefaultPrompthelper, it may be more user‑friendly to fall back to that constant instead of an empty string for a fresh install or missing setting.Example:
- return this.getSetting<string>('default_system_prompt') || '' + return this.getSetting<string>('default_system_prompt') || DEFAULT_SYSTEM_PROMPTIf an empty default is intentional (to explicitly mean “no system prompt”), keeping the current behavior is fine, but it’s worth validating.
41-51: Align default prompt mutations with DEFAULT_SYSTEM_PROMPT_CHANGED eventingRight now, only
setDefaultSystemPromptIdemitsCONFIG_EVENTS.DEFAULT_SYSTEM_PROMPT_CHANGED. Direct calls tosetDefaultSystemPrompt,resetToDefaultPrompt, orclearSystemPromptwill update settings but won’t notify renderer windows.If any callers mutate the default prompt via these methods without going through
setDefaultSystemPromptId, UIs depending on the event may not update.Consider either:
- Emitting the event from these methods as well, or
- Making
setDefaultSystemPromptIdthe single public entry point for default changes and keeping the others private/internal.Please verify how these methods are used in
configPresenterand the renderer before deciding.Also applies to: 87-113
115-128: Edge case when default prompt id is missing in stored promptsWhen there is a non‑empty
default_system_promptbut no prompt markedisDefault,getDefaultSystemPromptIdfalls back to'default'even if no prompt with id'default'actually exists.If the intention is to represent a “legacy inline default” that isn’t part of the saved prompt list, this is fine. Otherwise, you may want to:
- Create/ensure a corresponding
'default'prompt in the list, or- Return
'empty'(ornull) to explicitly indicate “no matching prompt in the list”.Worth double‑checking what the renderer expects when given
'default'but no such prompt exists.src/renderer/src/stores/modelStore.ts (6)
93-127: Leverage modelConfigStore consistently (and ensure getModelConfig contract)
applyUserDefinedModelConfigcallsmodelConfigStore.getModelConfig(model.id, providerId)and assumes it may returnnulland anisUserDefinedflag. That aligns with the snippet ofuseModelConfigStore, but make sure:
- The underlying presenter really can return configs that are not user‑defined (so
isUserDefinedis meaningful).getModelConfigis typed to allow thenullcase; otherwise TS will complain.Functionally this is fine; this comment is mainly about keeping type signatures consistent with how you’re using them here.
129-161: Guard updateLocalModelStatus when the model isn’t yet in local state
updateLocalModelStatusassumes the model exists inallProviderModelsorcustomModels. If an IPCMODEL_STATUS_CHANGEDevent arrives before models are hydrated for that provider,providerModel/customModelwill be undefined and there may be no existing enabled group. In that case:
- You create an
enabledProviderwith an emptymodelsarray,- But
sourceModelisundefined, so nothing is pushed,enabledModelsends up containing a provider entry with an emptymodelslist.This doesn’t break, but it’s a slightly inconsistent state and may confuse consumers.
Consider early‑returning when you cannot find the model in any local collection, or triggering a
refreshProviderModels(providerId)in that case instead of trying to mutate local state.
197-304: refreshStandardModels logic looks solid; just confirm Ollama interactionThe merge between DB models, stored models, and remote
llmP.getModelListis careful and preserves overrides;getBatchModelStatus+applyUserDefinedModelConfigare used consistently.The only thing to double‑check is how this behaves for providers with
apiType === 'ollama', given the newollamaStorealso manipulatesallProviderModels/enabledModels. Make sure there isn’t a tug‑of‑war betweenrefreshStandardModelsandsyncOllamaModelsToGlobalover the same provider’s model list (whichever writes last “wins”).
417-435: updateModelStatus: optimistic update and Ollama special caseThe optimistic update + rollback on error pattern is good. Note that for
apiType === 'ollama'you mutate local state viaupdateLocalModelStatusbut immediatelyreturnand never callllmP.updateModelStatusor refresh.If Ollama statuses are intended to be managed solely by
ollamaStore(and not persisted viallmP.updateModelStatus), this is fine; otherwise you might need symmetric handling here as well.Worth quickly validating against how Ollama models are supposed to be enabled/disabled end‑to‑end.
437-481: Custom model helpers vs IPC mutations: ensure state stays in syncYou expose both:
- Imperative helpers (
addCustomModel,removeCustomModel,updateCustomModel) that callllmP/configPand thenrefreshCustomModels, and- IPC mutations (
addCustomModelMutation,removeCustomModelMutation,updateCustomModelMutation) that only invalidate Colada queries.Because
customModels/allProviderModels/enabledModelsare only updated inrefreshCustomModels/refreshStandardModels, invalidating queries alone won’t update these refs unless some other path triggers a refresh (e.g., a CONFIG_EVENTS.MODEL_LIST_CHANGED handler).Please confirm that:
- UI code uses one approach consistently (helpers or mutations), and
- Using the mutation path will always result in a subsequent refresh of the modelStore state (via events or explicit calls), so the internal refs never drift from the query data.
533-564: findModelByIdOrName only searches enabled models and has redundant id search
findModelByIdOrNamecurrently:
- Searches
enabledModelsfor an exact id match,- Flattens
enabledModelsand searches by id again (redundant with step 1),- Falls back to a substring match on id/name, still only within enabled models.
If callers expect to be able to resolve disabled models too, this should probably also look into
allProviderModels. If the intent is explicitly “search only among enabled models”, the behavior is OK, but you can drop the second exact‑id search to simplify the function.Example simplification:
- for (const providerModels of enabledModels.value) { - const model = providerModels.models.find((m) => m.id === modelId) - if (model) { - return { model, providerId: providerModels.providerId } - } - } - - const enabledModel = enabledModels.value - .flatMap((provider) => - provider.models.map((m) => ({ ...m, providerId: provider.providerId })) - ) - .find((m) => m.id === modelId) - if (enabledModel) { - return { model: enabledModel, providerId: enabledModel.providerId! } - } + for (const providerModels of enabledModels.value) { + const model = providerModels.models.find((m) => m.id === modelId) + if (model) { + return { model, providerId: providerModels.providerId } + } + }Optionally extend it to
allProviderModelsif needed.src/renderer/src/stores/ollamaStore.ts (3)
62-71: Ollama model normalization is solid; consider reusing shared config logicThe way you derive
contextLength,maxTokens,type, and capabilities from bothmodelConfigandollamaModel.capabilitiesis thorough and matches how other providers are handled.Given
modelStore.applyUserDefinedModelConfigalready encapsulates similar logic, consider extracting shared normalization into a helper (or reuse the modelConfigStore there) to avoid subtle divergence over time between Ollama and non‑Ollama models. Not urgent, but worth thinking about.Also applies to: 85-139
72-175: Syncing Ollama models overrides global models; ensure it doesn’t fight modelStore
syncOllamaModelsToGlobalreplacesallProviderModels[providerId].modelswithollamaModelsAsGlobaland recalculatesenabledModelsfor that provider. At the same time,modelStore.refreshStandardModelsandrefreshCustomModelsalso manage those same refs.For
apiType === 'ollama'providers this might be intended (Ollama is the source of truth), but it does mean:
- Whichever runs last (modelStore vs ollamaStore) wins the model list for that provider.
- Any future changes to modelStore’s invariants around
allProviderModels/enabledModelswon’t automatically apply here.Please confirm that:
refreshStandardModelsis either not used for Ollama providers, or its effects are intentionally overwritten, and- There are no components relying on non‑Ollama metadata for Ollama providers that would be lost here.
270-278: initialize assumes providerStore.providers is already hydrated
initializefiltersproviderStore.providersfor enabled Ollama providers and immediately loops torefreshOllamaModels. This is fine as long as the app’s store initialization sequence ensuresproviderStore.initialize()(or an equivalent) has completed beforeollamaStore.initialize()is called.Please confirm the call order in your
initAppStoreswiring so thatprovidersisn’t empty here due to pending async initialization.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (54)
src/main/presenter/configPresenter/index.ts(17 hunks)src/main/presenter/configPresenter/modelStatusHelper.ts(1 hunks)src/main/presenter/configPresenter/providerHelper.ts(1 hunks)src/main/presenter/configPresenter/providerModelHelper.ts(1 hunks)src/main/presenter/configPresenter/systemPromptHelper.ts(1 hunks)src/main/presenter/configPresenter/uiSettingsHelper.ts(1 hunks)src/renderer/floating/index.html(0 hunks)src/renderer/settings/App.vue(6 hunks)src/renderer/settings/components/AddCustomProviderDialog.vue(2 hunks)src/renderer/settings/components/AnthropicProviderSettingsDetail.vue(11 hunks)src/renderer/settings/components/BedrockProviderSettingsDetail.vue(10 hunks)src/renderer/settings/components/BuiltinKnowledgeSettings.vue(2 hunks)src/renderer/settings/components/CommonSettings.vue(2 hunks)src/renderer/settings/components/DisplaySettings.vue(4 hunks)src/renderer/settings/components/GitHubCopilotOAuth.vue(3 hunks)src/renderer/settings/components/ModelProviderSettings.vue(9 hunks)src/renderer/settings/components/ModelProviderSettingsDetail.vue(14 hunks)src/renderer/settings/components/OllamaProviderSettingsDetail.vue(7 hunks)src/renderer/settings/components/ProviderModelList.vue(4 hunks)src/renderer/settings/components/common/LoggingSettingsSection.vue(2 hunks)src/renderer/settings/components/common/SearchAssistantModelSection.vue(1 hunks)src/renderer/settings/components/common/SearchEngineSettingsSection.vue(8 hunks)src/renderer/settings/components/prompt/SystemPromptSettingsSection.vue(9 hunks)src/renderer/settings/index.html(0 hunks)src/renderer/shell/index.html(0 hunks)src/renderer/splash/index.html(0 hunks)src/renderer/src/App.vue(6 hunks)src/renderer/src/components/ChatView.vue(4 hunks)src/renderer/src/components/ModelChooser.vue(3 hunks)src/renderer/src/components/ModelSelect.vue(2 hunks)src/renderer/src/components/NewThread.vue(7 hunks)src/renderer/src/components/chat-input/composables/usePromptInputConfig.ts(3 hunks)src/renderer/src/components/chat-input/composables/useRateLimitStatus.ts(3 hunks)src/renderer/src/components/icons/ModelIcon.vue(2 hunks)src/renderer/src/components/mcp-config/components/McpServers.vue(3 hunks)src/renderer/src/components/mcp-config/mcpServerForm.vue(3 hunks)src/renderer/src/components/message/MessageItemAssistant.vue(4 hunks)src/renderer/src/components/message/MessageToolbar.vue(1 hunks)src/renderer/src/components/settings/ModelCheckDialog.vue(3 hunks)src/renderer/src/components/settings/ModelConfigDialog.vue(5 hunks)src/renderer/src/composables/useModelTypeDetection.ts(3 hunks)src/renderer/src/lib/storeInitializer.ts(1 hunks)src/renderer/src/stores/mcp.ts(8 hunks)src/renderer/src/stores/mcpSampling.ts(7 hunks)src/renderer/src/stores/modelConfigStore.ts(1 hunks)src/renderer/src/stores/modelStore.ts(1 hunks)src/renderer/src/stores/ollamaStore.ts(1 hunks)src/renderer/src/stores/providerStore.ts(1 hunks)src/renderer/src/stores/searchAssistantStore.ts(1 hunks)src/renderer/src/stores/settings.ts(0 hunks)src/renderer/src/stores/systemPromptStore.ts(1 hunks)src/renderer/src/stores/uiSettingsStore.ts(1 hunks)src/renderer/src/views/WelcomeView.vue(10 hunks)test/renderer/composables/useModelTypeDetection.test.ts(1 hunks)
💤 Files with no reviewable changes (5)
- src/renderer/settings/index.html
- src/renderer/splash/index.html
- src/renderer/src/stores/settings.ts
- src/renderer/floating/index.html
- src/renderer/shell/index.html
🧰 Additional context used
🧬 Code graph analysis (19)
src/renderer/src/composables/useModelTypeDetection.ts (1)
src/renderer/src/stores/modelConfigStore.ts (1)
useModelConfigStore(6-59)
src/renderer/src/components/chat-input/composables/useRateLimitStatus.ts (1)
src/renderer/src/stores/providerStore.ts (1)
useProviderStore(11-363)
src/main/presenter/configPresenter/providerModelHelper.ts (3)
src/shared/types/presenters/legacy.presenters.d.ts (1)
ModelConfig(132-150)scripts/fetch-provider-db.mjs (1)
models(37-37)src/main/eventbus.ts (1)
eventBus(151-151)
src/renderer/src/components/chat-input/composables/usePromptInputConfig.ts (1)
src/renderer/src/stores/modelStore.ts (1)
useModelStore(17-662)
src/renderer/src/lib/storeInitializer.ts (1)
src/main/presenter/deeplinkPresenter/index.ts (1)
handleMcpInstall(291-422)
src/main/presenter/configPresenter/uiSettingsHelper.ts (1)
src/main/eventbus.ts (1)
eventBus(151-151)
src/renderer/src/stores/systemPromptStore.ts (2)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)src/shared/types/presenters/legacy.presenters.d.ts (1)
SystemPrompt(76-83)
src/renderer/src/stores/mcpSampling.ts (2)
src/renderer/src/stores/modelStore.ts (1)
useModelStore(17-662)src/renderer/src/stores/providerStore.ts (1)
useProviderStore(11-363)
src/renderer/src/stores/providerStore.ts (2)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)src/renderer/src/composables/useIpcQuery.ts (1)
useIpcQuery(31-47)
src/main/presenter/configPresenter/modelStatusHelper.ts (1)
src/main/eventbus.ts (1)
eventBus(151-151)
src/renderer/src/stores/searchAssistantStore.ts (3)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)src/renderer/src/stores/modelStore.ts (1)
useModelStore(17-662)src/main/presenter/threadPresenter/index.ts (1)
setSearchAssistantModel(139-142)
src/renderer/src/stores/uiSettingsStore.ts (1)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)
src/renderer/src/stores/mcp.ts (5)
src/main/presenter/configPresenter/index.ts (1)
setMcpEnabled(890-892)src/main/presenter/mcpPresenter/index.ts (1)
setMcpEnabled(1140-1142)src/main/presenter/configPresenter/mcpConfHelper.ts (1)
setMcpEnabled(539-546)src/main/events.ts (1)
MCP_EVENTS(111-122)src/renderer/src/events.ts (1)
MCP_EVENTS(72-81)
src/renderer/src/stores/modelConfigStore.ts (4)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)src/shared/types/presenters/legacy.presenters.d.ts (2)
ModelConfig(132-150)IModelConfig(152-157)src/main/presenter/configPresenter/index.ts (1)
hasUserModelConfig(983-985)src/main/presenter/configPresenter/modelConfig.ts (2)
importConfigs(494-538)exportConfigs(544-546)
src/renderer/src/stores/modelStore.ts (4)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)src/renderer/src/stores/providerStore.ts (1)
useProviderStore(11-363)src/renderer/src/stores/modelConfigStore.ts (1)
useModelConfigStore(6-59)src/renderer/src/composables/useIpcMutation.ts (1)
useIpcMutation(40-89)
src/main/presenter/configPresenter/index.ts (8)
src/main/presenter/configPresenter/providerHelper.ts (1)
ProviderHelper(21-136)src/main/presenter/configPresenter/modelStatusHelper.ts (1)
ModelStatusHelper(14-113)src/main/presenter/configPresenter/providerModelHelper.ts (1)
ProviderModelHelper(28-142)src/main/presenter/configPresenter/systemPromptHelper.ts (1)
SystemPromptHelper(21-129)src/main/presenter/configPresenter/uiSettingsHelper.ts (1)
UiSettingsHelper(12-69)src/shared/types/presenters/llmprovider.presenter.d.ts (2)
LLM_PROVIDER(44-59)MODEL_META(27-42)src/shared/types/presenters/legacy.presenters.d.ts (3)
LLM_PROVIDER(582-602)MODEL_META(567-581)SystemPrompt(76-83)src/shared/provider-operations.ts (1)
ProviderBatchUpdate(62-67)
src/main/presenter/configPresenter/providerHelper.ts (2)
src/main/eventbus.ts (1)
eventBus(151-151)src/shared/provider-operations.ts (3)
checkRequiresRebuild(55-57)ProviderChange(23-34)ProviderBatchUpdate(62-67)
src/main/presenter/configPresenter/systemPromptHelper.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (1)
SystemPrompt(76-83)src/main/eventbus.ts (1)
eventBus(151-151)
src/renderer/src/stores/ollamaStore.ts (1)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)
| const pullOllamaModel = async (providerId: string, modelName: string) => { | ||
| try { | ||
| updatePullingProgress(providerId, modelName, 0) | ||
| const success = await llmP.pullOllamaModels(providerId, modelName) | ||
| if (!success) { | ||
| updatePullingProgress(providerId, modelName) | ||
| } | ||
| return success | ||
| } catch (error) { | ||
| console.error('Failed to pull Ollama model', modelName, providerId, error) | ||
| updatePullingProgress(providerId, modelName) | ||
| return false | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Newly pulled Ollama models may not immediately update local/global state
After pullOllamaModel succeeds, you rely on handleOllamaModelPullEvent to process progress events. On "success" / "completed" status, you:
- Clear pulling progress after a
setTimeout, - Call
modelStore.getProviderModelsQuery(providerId).refetch().
But you never call refreshOllamaModels(providerId) here, so:
runningModelsandlocalModelsin ollamaStore will stay stale until something else explicitly callsrefreshOllamaModels,syncOllamaModelsToGlobalis not invoked, soallProviderModels/enabledModelswon’t reflect the new Ollama model either.
Consider triggering a full refresh on success, e.g.:
- if (status === 'success' || status === 'completed') {
- setTimeout(() => {
- updatePullingProgress(providerId, modelName)
- modelStore.getProviderModelsQuery(providerId).refetch()
- }, 600)
- }
+ if (status === 'success' || status === 'completed') {
+ setTimeout(() => {
+ updatePullingProgress(providerId, modelName)
+ void refreshOllamaModels(providerId)
+ }, 600)
+ }This keeps Ollama state and the global modelStore in sync right after a successful pull.
Also applies to: 221-226
🤖 Prompt for AI Agents
In src/renderer/src/stores/ollamaStore.ts around lines 191-205 (and similarly
221-226), after a successful pull you currently rely only on
handleOllamaModelPullEvent and refetching the provider models query, but you
never call refreshOllamaModels so runningModels/localModels and the global
modelStore stay stale; fix by invoking refreshOllamaModels(providerId) (and/or
ensure syncOllamaModelsToGlobal runs) immediately when pull returns success or
when the progress event status is "success"/"completed", so the Ollama store
updates runningModels/localModels and triggers syncOllamaModelsToGlobal to
update allProviderModels/enabledModels right after a successful pull.
| const setupOllamaEventListeners = () => { | ||
| window.electron?.ipcRenderer?.on( | ||
| OLLAMA_EVENTS.PULL_MODEL_PROGRESS, | ||
| (_event: unknown, data: Record<string, unknown>) => handleOllamaModelPullEvent(data) | ||
| ) | ||
| } | ||
|
|
||
| const removeOllamaEventListeners = () => { | ||
| window.electron?.ipcRenderer?.removeAllListeners(OLLAMA_EVENTS.PULL_MODEL_PROGRESS) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Event listener lifecycle can easily double‑register and remove listeners too often
You currently call setupOllamaEventListeners in two places:
- Inside
onMounted, and - Inside
initialize.
And you call removeOllamaEventListeners in onBeforeUnmount.
Without a listenersRegistered guard:
- Calling
initializeplus the automaticonMountedwill register duplicate IPC listeners, so each progress event is processed multiple times. - If multiple components use this store, the first one to unmount will call
removeAllListeners, removing listeners even while other components still rely on them.
This can lead to inconsistent behavior and unnecessary re‑work.
I’d strongly recommend either:
- Removing the
onMounted/onBeforeUnmounthooks and controlling event wiring explicitly viainitialize()and a dedicatedcleanup()call (similar tomodelStore), or - Introducing a
listenersRegisteredflag to makesetupOllamaEventListenersidempotent and only callingremoveOllamaEventListenerswhen you know the store is truly being torn down.
Example guard:
- const setupOllamaEventListeners = () => {
- window.electron?.ipcRenderer?.on(
+ const listenersRegistered = ref(false)
+
+ const setupOllamaEventListeners = () => {
+ if (listenersRegistered.value) return
+ listenersRegistered.value = true
+ window.electron?.ipcRenderer?.on(
OLLAMA_EVENTS.PULL_MODEL_PROGRESS,
(_event: unknown, data: Record<string, unknown>) => handleOllamaModelPullEvent(data)
)
}
- const removeOllamaEventListeners = () => {
- window.electron?.ipcRenderer?.removeAllListeners(OLLAMA_EVENTS.PULL_MODEL_PROGRESS)
- }
+ const removeOllamaEventListeners = () => {
+ window.electron?.ipcRenderer?.removeAllListeners(OLLAMA_EVENTS.PULL_MODEL_PROGRESS)
+ listenersRegistered.value = false
+ }…and then decide on a single place (either initialize or a top‑level lifecycle) to call these.
Also applies to: 266-282
I'll verify the review comment by examining the codebase to confirm the actual call patterns and lifecycle issues.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the ollamaStore.ts file and examine setupOllamaEventListeners call sites
fd -name "ollamaStore.ts" -type f
Length of output: 230
🏁 Script executed:
#!/bin/bash
# Search for setupOllamaEventListeners calls with context
rg -n "setupOllamaEventListeners" --type=ts -B2 -A2Length of output: 1338
🏁 Script executed:
#!/bin/bash
# Search for removeOllamaEventListeners calls with context
rg -n "removeOllamaEventListeners" --type=ts -B2 -A2Length of output: 1008
🏁 Script executed:
#!/bin/bash
# Examine lines 220-290 of ollamaStore.ts to see the full context
fd -name "ollamaStore.ts" -type f -exec head -n 290 {} \; -exec tail -n +220 {} \;Length of output: 230
🏁 Script executed:
#!/bin/bash
# Check for any listenersRegistered flag or similar guard in ollamaStore.ts
rg -n "listenersRegistered" --type=tsLength of output: 578
---
🏁 Script executed:
```shell
#!/bin/bash
# Get exact content of ollamaStore.ts lines 266-282
cat src/renderer/src/stores/ollamaStore.ts | sed -n '266,282p'
Length of output: 404
🏁 Script executed:
#!/bin/bash
# Get broader context around the lifecycle hooks to understand the store setup
cat src/renderer/src/stores/ollamaStore.ts | sed -n '260,290p'Length of output: 712
Event listener double-registration and premature removal confirmed
The issue is real. setupOllamaEventListeners is called both in onMounted() (line 267) and initialize() (line 271), causing duplicate listener registration when both execute. Additionally, removeOllamaEventListeners is called in onBeforeUnmount() (line 281), which removes all listeners globally—if multiple components use this store, the first to unmount removes listeners while others still need them.
The codebase provides a proven pattern: modelStore.ts and providerStore.ts both implement the listenersRegistered guard (e.g., lines 26, 567-568, 601 in modelStore.ts). Apply the same pattern here:
- Add a
listenersRegisteredref to track guard state - Guard
setupOllamaEventListenersto return early if already registered - Reset the flag in
removeOllamaEventListeners - Choose a single lifecycle ownership: either centralize via explicit
initialize()calls or keep lifecycle hooks but ensure idempotency
🤖 Prompt for AI Agents
In src/renderer/src/stores/ollamaStore.ts around lines 229 to 238, the IPC
listener is being double-registered (setupOllamaEventListeners called in both
onMounted and initialize) and removeOllamaEventListeners uses removeAllListeners
which can prematurely remove listeners used by other components; add a
listenersRegistered ref to guard registration (return early if already true) and
set it true after registering, change removeOllamaEventListeners to only remove
the specific handler and reset listenersRegistered to false, and consolidate
lifecycle ownership by making initialization idempotent (remove one of the
duplicate lifecycle calls — prefer a single explicit initialize call or keep
onMounted but ensure idempotency) so listeners are not duplicated or globally
removed by one component unmount.
| import { ref, computed, onMounted, onBeforeUnmount } from 'vue' | ||
| import { defineStore } from 'pinia' | ||
| import { usePresenter } from '@/composables/usePresenter' | ||
| import { CONFIG_EVENTS } from '@/events' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid lifecycle hooks inside Pinia stores; ensure IPC listeners are actually registered
This store calls onMounted / onBeforeUnmount inside defineStore, but Pinia store setup does not run in a component instance, so Vue lifecycle hooks here won’t fire as expected. The practical effect:
setupListeners()is never called, so the IPC listeners forCONFIG_EVENTS.*are not attached.loadSettings()is only executed becauseinitAppStorescalls it explicitly, not because ofonMounted.
This means UI state will not react to config changes coming from the main process.
A more robust pattern is to remove lifecycle hooks from the store and manage initialization directly in store code (or via the existing initAppStores). For example:
-import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
+import { ref, computed } from 'vue'
@@
- const setupListeners = () => {
+ let listenersInitialized = false
+
+ const setupListeners = () => {
+ if (listenersInitialized) return
+ listenersInitialized = true
if (!window?.electron?.ipcRenderer) return
@@
- onMounted(() => {
- loadSettings()
- setupListeners()
- })
-
- onBeforeUnmount(() => {
- if (!window?.electron?.ipcRenderer) return
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.FONT_SIZE_CHANGED)
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.SEARCH_PREVIEW_CHANGED)
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.CONTENT_PROTECTION_CHANGED)
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.COPY_WITH_COT_CHANGED)
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.TRACE_DEBUG_CHANGED)
- window.electron.ipcRenderer.removeAllListeners(CONFIG_EVENTS.NOTIFICATIONS_CHANGED)
- })
+ // Option 1: ensure listeners on first load
+ const loadSettings = async () => {
+ // ...existing body...
+ setupListeners()
+ }With this, initAppStores’ existing await uiSettingsStore.loadSettings() will both load settings and register listeners, and you avoid lifecycle hooks inside the store altogether. If you still need cleanup, consider exposing an explicit teardown() method instead of relying on component unmounts.
Also applies to: 25-39, 82-117, 119-138
I'll verify this review comment by examining the actual implementation of the store and understanding how Vue lifecycle hooks work within Pinia stores.
🏁 Script executed:
#!/bin/bash
# Find and examine the uiSettingsStore.ts file
find . -type f -name "uiSettingsStore.ts" -exec cat {} \;Length of output: 5242
I need to verify whether Vue lifecycle hooks actually work inside Pinia store setup functions.
🌐 Web query:
Vue 3 Pinia store setup lifecycle hooks onMounted onBeforeUnmount
💡 Result:
Short answer: you generally should not call onMounted/onBeforeUnmount directly when the store is defined; those hooks are tied to the active component instance and must be registered during a component's setup. If you call them while creating a singleton store they run (or attempt to register) at store creation, not per-component usage, so they won't behave as you expect. [1][2]
Recommended patterns
- Register lifecycle hooks in the component's setup (or in a composable that the component calls). From that hook call store actions (e.g. store.init(), store.cleanup()). This registers the hook on the component instance. [1][3]
- Or expose a store method that itself calls onMounted/onBeforeUnmount — but only if that method is invoked from a component's setup; then the hook registers on that component. Many community examples and discussions show this approach when you want store behavior to follow component mount/unmount. [2][3]
- If you need per-consumer tracking inside the store (detect how many components are using it), implement explicit attach/detach calls from components (e.g. store.useCount++ / store.useCount-- in onMounted/onBeforeUnmount), or call a composable wrapper that handles the lifecycle. [2]
Minimal example (pattern)
- Component:
- onMounted(() => store.load())
- onBeforeUnmount(() => store.stop())
Useful refs
- Vue Composition API lifecycle hooks (onMounted/onBeforeUnmount). [1]
- Pinia discussion showing hooks won't fire as expected when store is created once; suggestions to call hook-registrations from components. [2]
- StackOverflow threads recommending keeping lifecycle hooks in components (or calling store actions from component hooks). [3]
References
[1] Vue Composition API — onMounted / onBeforeUnmount docs.
[2] Pinia discussion: lifecycle / store usage tracking (GitHub).
[3] StackOverflow: avoid using onMounted inside Pinia store; call actions from component onMounted.
Now let me verify how initAppStores initializes the store and whether setupListeners is called anywhere.
🏁 Script executed:
#!/bin/bash
# Search for initAppStores and verify setupListeners is not called elsewhere
rg -n "initAppStores" --type=ts
echo "---"
rg -n "setupListeners" --type=ts -C2Length of output: 798
Let me examine the initAppStores implementation to understand the full initialization flow.
🏁 Script executed:
#!/bin/bash
# Examine the storeInitializer.ts file
cat src/renderer/src/lib/storeInitializer.tsLength of output: 2097
Remove lifecycle hooks from store and ensure IPC listeners are registered on initialization
Verification confirms the core issue: Vue lifecycle hooks (onMounted/onBeforeUnmount) inside Pinia store setup do not fire as expected because they are tied to component instances, not singletons. When hooks are registered during store creation, they run at store creation time, not per-component usage, so they won't behave as expected.
Practical impact: setupListeners() (line 82) is never called, so IPC listeners for CONFIG_EVENTS are never attached. Only loadSettings() executes because initAppStores calls it explicitly (line 18 of storeInitializer.ts). This means UI state will not react to config changes from the main process.
The suggested refactor is sound: remove lifecycle hook imports and calls (lines 1, 119–138), consolidate listener setup into loadSettings() to ensure both load and listen happen together when initAppStores runs.
🤖 Prompt for AI Agents
In src/renderer/src/stores/uiSettingsStore.ts lines 1 and 119-138, the Pinia
store incorrectly uses Vue lifecycle hooks (onMounted/onBeforeUnmount) so
setupListeners() is never executed when the store is initialized; remove the
onMounted/onBeforeUnmount imports and any hook usage, then move the IPC listener
registration logic into loadSettings() so that loadSettings() both loads
settings and registers CONFIG_EVENTS listeners when initAppStores calls it;
ensure any teardown logic is either unnecessary for this singleton store or
exposed as an explicit cleanup function (not lifecycle hooks).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
package.json (1)
106-106: Optional: Reconsider exact version pinning for devDependencies.Changing
@lingual/i18n-checkfrom^0.8.12(flexible) to0.8.12(exact) prevents automatic receipt of security patches and bug fixes. While the enriched summary mentions this ensures consistency during refactoring, exact pins can add maintenance overhead.Unless there's a specific incompatibility or test fragility issue with newer patch versions, consider reverting to
^0.8.12to balance consistency with flexibility.
Summary by CodeRabbit