-
Notifications
You must be signed in to change notification settings - Fork 614
feat: add Agent Client Protocol (ACP) #1102
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
|
Important Review skippedReview was skipped as selected files did not have any reviewable changes. 💤 Files selected but had no reviewable changes (1)
You can disable this status message by setting the WalkthroughAdds full ACP (Agent Client Protocol) support: new ACP SDK dependency, ACP config persistence, provider/process/session managers, SQLite session table, content mapping/formatting, renderer settings/UI, store integrations, permission/workdir flows, and public type/interface extensions for ACP. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as Renderer (UI)
participant Thread as ThreadPresenter
participant LLM as LLMProviderPresenter
participant ACP as AcpProvider
participant SM as AcpSessionManager
participant PM as AcpProcessManager
participant Agent as Agent Process
UI->>Thread: startStreamCompletion(..., conversationId)
Thread->>LLM: startStreamCompletion(..., conversationId)
LLM->>ACP: coreStream(messages, modelConfig)
rect rgb(230,240,255)
ACP->>SM: getOrCreateSession(conversationId, agentId)
SM->>PM: getConnection(agentId)
PM->>Agent: spawn & establish IPC
Agent-->>PM: ready
PM-->>SM: connection
SM-->>ACP: session ready
end
rect rgb(240,255,230)
Agent-->>PM: SessionNotification (message/tool_call/permission)
PM->>SM: route notification
SM->>ACP: deliver notification
ACP->>ACP: AcpContentMapper -> LLMCoreStreamEvent(s)
ACP-->>LLM: stream events (including permission)
LLM-->>Thread: yield events
Thread-->>UI: render / show permission prompt
end
rect rgb(255,245,230)
UI->>Thread: user approves/denies
Thread->>LLM: resolveAgentPermission(requestId, granted)
LLM->>ACP: resolvePermissionRequest(requestId, granted)
ACP->>PM: send IPC response
PM->>Agent: notify decision
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas needing extra attention:
Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
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: 14
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/main/presenter/llmProviderPresenter/baseProvider.ts (1)
162-191:fetchModelstry/catch doesn't catch async rejections; rewrite withawaitThe current implementation:
try { return this.fetchProviderModels().then((models) => { ... }) } catch (e) { ... }will not catch asynchronous rejections from
fetchProviderModels(). A try/catch around a Promise chain only catches synchronous exceptions thrown before the Promise is created — it will NOT catch asynchronous rejections from the Promise. This means network failures and other async errors won't be handled, and callers won't reliably get the intended[]fallback.Using
awaitinside an async function with try/catch will catch rejection. Rewrite as suggested:public async fetchModels(): Promise<MODEL_META[]> { try { - return this.fetchProviderModels().then((models) => { - console.log( - `[Provider] fetchModels: fetched ${models?.length || 0} models for provider "${this.provider.id}"` - ) - // Validate that all models have correct providerId - const validatedModels = models.map((model) => { - if (model.providerId !== this.provider.id) { - console.warn( - `[Provider] fetchModels: Model ${model.id} has incorrect providerId: expected "${this.provider.id}", got "${model.providerId}". Fixing it.` - ) - model.providerId = this.provider.id - } - return model - }) - this.models = validatedModels - this.configPresenter.setProviderModels(this.provider.id, validatedModels) - return validatedModels - }) + const models = await this.fetchProviderModels() + console.log( + `[Provider] fetchModels: fetched ${models?.length || 0} models for provider "${this.provider.id}"` + ) + const validatedModels = models.map((model) => { + if (model.providerId !== this.provider.id) { + console.warn( + `[Provider] fetchModels: Model ${model.id} has incorrect providerId: expected "${this.provider.id}", got "${model.providerId}". Fixing it.` + ) + model.providerId = this.provider.id + } + return model + }) + this.models = validatedModels + this.configPresenter.setProviderModels(this.provider.id, validatedModels) + return validatedModels } catch (e) { console.error( `[Provider] fetchModels: Failed to fetch models for provider "${this.provider.id}":`, e ) if (!this.models) { this.models = [] } return [] } }Apply the same fix to the other location (lines 199–207).
src/renderer/src/stores/chat.ts (1)
717-743: Bug:getActiveThreadId() === getActiveThreadId()condition is always trueIn
handleStreamEnd, both branches that update messages check:if (getActiveThreadId() === getActiveThreadId()) { // update messages }This condition is tautologically true and ignores
cached.threadId, so messages will be treated as if they belong to the active thread even when the event is for a background conversation.This was pre-existing but is worth fixing. The intent seems to be comparing against
cached.threadId:- if (getActiveThreadId() === getActiveThreadId()) { + if (getActiveThreadId() === cached.threadId) { const mainMsgIndex = getMessages().findIndex((m) => m.id === mainMessage.id) if (mainMsgIndex !== -1) { getMessages()[mainMsgIndex] = enrichedMainMessage as AssistantMessage | UserMessage } } ... - if (getActiveThreadId() === getActiveThreadId()) { + if (getActiveThreadId() === cached.threadId) { const msgIndex = getMessages().findIndex((m) => m.id === msg.eventId) if (msgIndex !== -1) { getMessages()[msgIndex] = enrichedMessage as AssistantMessage | UserMessage } }src/shared/types/presenters/legacy.presenters.d.ts (1)
1260-1263: Fix typo in type definition.Line 1262:
unknowshould beunknown.Apply this diff:
command: string args: string[] - env: Record<string, unknow> + env: Record<string, unknown> descriptions: string
🧹 Nitpick comments (42)
src/renderer/src/components/message/MessageBlockToolCall.vue (1)
4-6: Verify layout behavior with visual testing.The changes to
inline-flex, removal offlex-wrap, and addition ofoverflow-hiddenalter the container's layout behavior. The switch toinline-flexmeans the container will only be as wide as its content (up to max-width) rather than filling available space, and removingflex-wrapprevents content from wrapping to multiple lines.Please verify:
- The tool call block displays correctly with various content lengths
- Long server names and function names truncate properly without breaking layout
- The component flows correctly with surrounding elements in different screen sizes
src/renderer/src/components/message/MessageItemAssistant.vue (1)
7-19: Consider addingaltfor ACP ModelIcon for consistency/accessibilityThe ACP-specific branch correctly switches to
currentMessage.model_id, but it drops thealtprop that the non-ACP branch provides. To keep accessibility and behavior consistent, you can also pass:alt="currentMessage.role"(or a more specific label) on the ACP branch.- <ModelIcon - v-if="currentMessage.model_provider === 'acp'" - :model-id="currentMessage.model_id" - :is-dark="themeStore.isDark" - custom-class="w-[18px] h-[18px]" - /> + <ModelIcon + v-if="currentMessage.model_provider === 'acp'" + :model-id="currentMessage.model_id" + :is-dark="themeStore.isDark" + custom-class="w-[18px] h-[18px]" + :alt="currentMessage.role" + />src/main/presenter/llmProviderPresenter/managers/modelManager.ts (1)
14-21: Model/providerId normalization and logging are reasonable, but somewhat redundantThe added logging and
providerIdfix-up ingetModelListhelp harden against malformed model metadata and provide good diagnostics. Note thatBaseLLMProvider.fetchModels()now also normalizesproviderId, so this layer is mostly a safety net rather than strictly necessary. Keeping both is fine; if logs become noisy in production, you could later centralize the normalization in one place.Also applies to: 23-34, 56-66
src/main/presenter/llmProviderPresenter/agent/acpMessageFormatter.ts (2)
34-41: Consider improving tool call text formatting for clarity.The tool call text concatenation on line 38 directly concatenates the function name and arguments without a delimiter:
text: `${prefix} TOOL CALL ${toolCall.id || ''}: ${toolCall.function?.name || 'unknown'} ${toolCall.function?.arguments || ''}`This could make it difficult to parse or read when arguments are present. Consider adding a clearer delimiter or structure, such as:
text: `${prefix} TOOL CALL ${toolCall.id || ''}: ${toolCall.function?.name || 'unknown'} ARGS: ${toolCall.function?.arguments || ''}`
60-92: Content normalization is functional but lacks defensive error handling.The
normalizeContentmethod handles various content formats (string, array with text/image parts) but doesn't validate or handle malformed input gracefully. Ifcontenthas an unexpected structure, the method silently returns an empty array or skips invalid parts.For production resilience, consider:
- Logging warnings when content doesn't match expected structure
- Validating that
contentis the expected type before processingThis is optional as silent failure may be acceptable for a formatter, but explicit logging would aid debugging.
src/renderer/src/stores/agentModelStore.ts (1)
29-75: Document hardcoded ACP model defaults and consider validation.The
refreshAgentModelsmethod uses hardcoded default values for all ACP agent models:
contextLength: 8192maxTokens: 4096vision: false,functionCall: true,reasoning: false,enableSearch: falseConsider:
- Documentation: Add a comment explaining why these specific defaults are appropriate for all ACP agents
- Uniqueness validation: No check that
agent.idvalues are unique. Duplicate IDs could cause conflicts in model selection- Error handling: No try-catch for
getAcpAgents()- failures will bubble up unhandledExample validation:
const agents = await configPresenter.getAcpAgents() const seenIds = new Set<string>() for (const agent of agents) { if (seenIds.has(agent.id)) { console.warn(`Duplicate ACP agent ID detected: ${agent.id}`) } seenIds.add(agent.id) }src/main/presenter/llmProviderPresenter/agent/types.ts (1)
15-41: Clarify session keying and process descriptor vs agentId symmetryThe abstractions look clean, but there are two design points worth double‑checking:
AgentSessionManager.getSession(conversationId: string)only keys byconversationId, while your SQLite table (AcpSessionsTable) uses(conversation_id, agent_id)as the uniqueness constraint. If a conversation can have multiple agents, this signature may be ambiguous. Consider either:
- Making
getSessiontake the agent descriptor as well, or- Clearly documenting that an
AgentSessionManageris scoped to a single agent soconversationIdis sufficient.In
AgentProcessManager,getConnection(agent: TDescriptor)accepts a generic descriptor, butgetProcess/releasetakeagentId: string. If descriptors can be richer than a string, you may want these methods to also acceptTDescriptorfor symmetry, or alternatively renameTDescriptortoTAgentIdand keep everything string‑based.Both are design/API questions, but locking this down now will make later providers/sessions easier to reason about.
src/renderer/src/i18n/fr-FR/routes.json (1)
15-16: French ACP label is fine; consider plural if the page lists multiple agents
"settings-acp": "Agent ACP"is valid and clear. If this settings page manages multiple agents (as the English “ACP Agents” suggests), you might optionally prefer"Agents ACP"for number agreement, but it’s not strictly required.src/renderer/src/i18n/zh-TW/routes.json (1)
15-16: zh-TW ACP label is clear; optional localization tweakThe new entry
"settings-acp": "ACP Agent"is valid and understandable, and the JSON remains well‑formed.If you want to align more closely with
"MCP市場", you could optionally localize “Agent” (e.g.,"ACP 代理"), but this is stylistic.src/renderer/src/components/ModelChooser.vue (1)
33-44: Consider consolidating the conditional ModelIcon rendering.The current implementation duplicates the ModelIcon component with only the
:model-idprop varying. You could simplify this using a computed property or ternary expression:Apply this diff to reduce duplication:
<div class="flex h-7 w-7 items-center justify-center rounded-md border border-border bg-muted/40 text-[11px] font-semibold uppercase text-muted-foreground transition group-data-[selected=true]:border-primary group-data-[selected=true]:bg-primary/20 group-data-[selected=true]:text-primary" > - <ModelIcon - v-if="provider.id === 'acp'" - class="h-4 w-4 shrink-0 opacity-80 transition group-hover:opacity-100 group-data-[selected=true]:opacity-100" - :model-id="model.id" - :is-dark="themeStore.isDark" - /> - <ModelIcon - v-else + <ModelIcon class="h-4 w-4 shrink-0 opacity-80 transition group-hover:opacity-100 group-data-[selected=true]:opacity-100" - :model-id="provider.id" + :model-id="provider.id === 'acp' ? model.id : provider.id" :is-dark="themeStore.isDark" /> </div>src/renderer/src/i18n/ko-KR/chat.json (1)
19-24: ACP workdir & toolCall keys look structurally correct; consider localizing new labelsThe new ACP workdir strings and
toolCallblock mirror the en-US structure and placeholders, so they should integrate cleanly. If full localization is a goal forko-KR, you may want to translate the ACP workdir labels instead of leaving them in English for consistency with the rest of this file.Also applies to: 72-82
src/renderer/settings/components/ModelProviderSettings.vue (1)
217-223: Avoid navigation side-effects insideactiveProvidercomputed; consider usingvisibleProvidersfor default selectionFiltering
visibleProvidersto exclude theacpprovider and deriving enabled/disabled lists from it makes sense. Theacpspecial-case inactiveProvider, however, performs a route redirect from inside a computed, which tightly couples this computed to router behavior and can be brittle if route params change.Two follow-ups you might consider:
- Move the
acpredirect intosetActiveProvider(or awatchonroute.params.providerId) so computed logic stays side-effect free.- In
onMounted, pick the initial provider fromvisibleProvidersinstead ofproviderStore.sortedProvidersto avoid briefly navigating tosettings-providerwithacpand then immediately redirecting tosettings-acp.Also applies to: 298-305
src/main/presenter/configPresenter/providerModelHelper.ts (1)
44-61: Provider model store isolation and providerId normalization look good; consider sanitizingstoreNameThe per-provider store isolation (
name: models_${providerId},cwd: userDataPath/provider_models) and providerId normalization in bothgetProviderModelsandsetProviderModelsshould make the model cache much more robust against mismatched or missingproviderIdvalues.Two minor follow-ups you might consider:
- If there is any chance that
providerIdcan contain path- or filename-unfriendly characters (/,\,:, etc.), normalize or hash it when buildingstoreNameto avoid filesystem issues.- The post-validation
incorrectProviderIdschecks will essentially always be empty because you correctmodel.providerIdearlier in the same map; you can keep them for extra safety or drop them later as dead diagnostics.Functionally, the changes are solid as-is.
Also applies to: 68-116, 119-153
test/main/presenter/llmProviderPresenter/acpContentMapper.test.ts (1)
1-114: Good coverage of ACP tool call mapping; core behaviors are well testedThese tests exercise the critical
AcpContentMapperbehaviors: emitting start/chunk/end events with correctly assembled arguments and maintaining independent tool-call state per session even when ids collide. That gives solid confidence in the streaming tool-call path.If you expand tests later, you might add cases for
failedstatus or non-JSON argument buffers, but the current suite is already very useful.src/main/presenter/configPresenter/providerHelper.ts (2)
36-103: Provider store self-heal works, butlistChangedmay miss some repair casesThe
getProvidersflow does a nice job of:
- Skipping MODEL_META-like entries (with
providerIdbut noapiType).- Merging partial provider entries with defaults when ids match.
- Restoring full defaults if no valid providers are found.
- Ensuring all default providers are present in the final list.
One nuance:
listChangedonly checks for a length change or remaining entries withoutapiType. If you repair entries by merging them with templates (i.e., fill in missing fields but preserve length and addapiType),listChangedstays false and the healed data is never written back, so the underlying store remains in its pre-repair shape. Callers still getrepairedProviders, so behavior is correct, but you lose the “self-healing” persistence.If you want the store to be truly normalized after any repair, consider broadening
listChanged(e.g., deep-compare ids/order, or track whether you merged/skipped anything during the loop) and persisting when repairs actually occurred.
115-134: Centralize provider validation to avoid future inconsistencies
setProvidersnow filters out invalid entries (missingidorapiType) before persisting, which is good. However, several code paths still write provider lists directly viasetSetting:
updateProvidersBatchusesbatchUpdate.providersas-is.updateProviderAtomic,addProviderAtomic, andreorderProvidersAtomicalso callsetSettingdirectly.As long as those callers only ever pass well-formed
LLM_PROVIDERs this is fine, but over time it’s easy for a new caller to bypass the validation unintentionally. To keep the invariants enforced uniformly, consider routing all writes of the full providers array through a single validation path (e.g., reusesetProvidersfrom these methods, or extract the validation/filtering into a shared helper).Also applies to: 175-178, 180-183, 207-208
src/main/presenter/sqlitePresenter/index.ts (1)
256-259: Consider wrapping cascade deletion in a transaction.The deletion of a conversation and its associated ACP sessions is performed sequentially without transaction protection. If
deleteByConversationfails after the conversation is deleted, the ACP sessions will remain orphaned.Apply this diff to ensure atomic deletion:
public async deleteConversation(conversationId: string): Promise<void> { - await this.conversationsTable.delete(conversationId) - await this.acpSessionsTable.deleteByConversation(conversationId) + await this.runTransaction(() => { + this.conversationsTable.delete(conversationId) + this.acpSessionsTable.deleteByConversation(conversationId) + }) }Note: Verify that the table deletion methods are synchronous (not returning promises) before applying this change, as
runTransactionexpects synchronous operations.test/main/presenter/threadPresenter/permissionHandler.test.ts (1)
112-126: Consider adding edge case coverage for ACP permission handling.The tests correctly verify the happy path for granted/denied permissions. Consider adding test cases for error scenarios:
it('handles missing permissionRequestId gracefully', async () => { const blockWithoutRequestId = { ...block, extra: { ...block.extra, permissionRequestId: undefined } } const { handler, messageId, toolCallId } = createHandler({ block: blockWithoutRequestId }) // Should not crash and possibly log error await expect( handler.handlePermissionResponse(messageId, toolCallId, true, 'write', false) ).resolves.not.toThrow() }) it('handles resolveAgentPermission rejection', async () => { const mockReject = vi.fn().mockRejectedValue(new Error('Network error')) const { handler, messageId, toolCallId } = createHandler({ resolveAgentPermission: mockReject }) await expect( handler.handlePermissionResponse(messageId, toolCallId, true, 'write', false) ).rejects.toThrow('Network error') })src/shared/types/presenters/agent-provider.d.ts (1)
1-32: Agent provider/session types look good; avoid duplicate definitionsThe new
AgentSessionState/AgentProcessHandleshapes are sensible and cover expected metadata. However, the same symbols also exist insrc/main/presenter/llmProviderPresenter/agent/types.ts, which risks the two diverging over time.Consider centralizing these type definitions (e.g., only in
@shared/presenterand importing them where needed) rather than redefining similar interfaces in multiple places.src/renderer/src/i18n/ru-RU/chat.json (1)
19-24: Consider localizing new ACP workdir stringsThe new
acpWorkdir*strings ("ACP Workdir","Set ACP working directory", etc.) are still in English while the rest of this file is Russian. Functionally it’s fine, but for consistency and UX, it would be better to translate these into Russian (similar to howtoolCall.*was localized).Also applies to: 73-82
test/main/presenter/llmProviderPresenter.test.ts (1)
4-9: SQLite presenter mock correctly matches new constructor requirementsThe tests have been updated to:
- Import
ISQLitePresenterand build amockSqlitePresenterthat includes the new ACP session/workdir methods.- Pass this mock into
LLMProviderPresenterwherever it’s instantiated, matching the new(configPresenter, sqlitePresenter)signature.For these integration-style tests (which target non-ACP flows), this level of mocking is sufficient and keeps the existing behavior intact. If
ISQLitePresenterkeeps evolving, you might eventually want a shared factory/helper for this mock to avoid manual drift across test files, but that can be deferred.Also applies to: 79-110, 177-178, 582-583
src/renderer/src/components/chat-input/ChatInput.vue (2)
102-150: ACP workdir integration and state wiring look solidThe ACP workdir button/tooltip wiring matches the composable API:
isAcpModelandhasWorkdircorrectly gate visibility/state, andconversationIdplusactiveModelSourcegive the composable enough context for both chat and new-thread flows. The pending-workdir path should behave correctly withmodelInfo-driven new-thread creation and the chat store’sacpWorkdirMap.Minor nit: in the template you can drop the
.valueaccess onacpWorkdir.*since Vue automatically unwraps refs in templates; this would slightly simplify the markup.Also applies to: 369-397, 533-545
205-216: Guard ACP ModelIcon usage against missingidThe ACP branch renders:
<ModelIcon v-if="config.activeModel.value.providerId === 'acp'" :model-id="config.activeModel.value.id" ... />If there’s any transient state where
providerId === 'acp'butidis unset,model-idwill beundefined. ModelIcon will likely degrade gracefully, but you may want to either:
- Also check for
config.activeModel.value.idin thev-if, or- Provide a sensible fallback (e.g. a generic ACP icon) when
idis missing.This would make the UI a bit more robust to incomplete activeModel state.
src/renderer/src/stores/providerStore.ts (1)
227-248:optimizeProviderOrdercurrently ignores theenableflagIn
optimizeProviderOrder, the computednewOrderis the same regardless of theenableboolean:const newOrder = enable ? [...enabledOrder, providerId, ...disabledOrder] : [...enabledOrder, providerId, ...disabledOrder]So the
enableargument is effectively unused. Behavior is still reasonable (the provider is placed after all currently enabled providers and before all disabled ones), but this is either:
- Dead parameter that can be removed, or
- A missed opportunity to differentiate where disabled providers should land.
If the intent is just “move provider to the enabled/disabled boundary”, consider dropping
enablefrom the signature; otherwise, adjust thefalsebranch to place the provider in the disabled section explicitly.src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts (1)
1-176: ACP workdir composable design matches chat store and thread presenter usageThe composable cleanly encapsulates ACP workdir behavior:
isAcpModelgating ensures all logic is scoped to ACP agents with a valid id.loadWorkdironly callsthreadPresenter.getAcpWorkdirwhen bothconversationIdandagentIdare present, and synchronizes the resulting path back intochatStore.setAcpWorkdirPreference, which lines up with howcreateThreadandloadChatConfiguseacpWorkdirMap(per the provided chat store snippets).- The
pendingWorkdirpath for “chosen before conversation exists” is handled byselectWorkdir+syncPendingWhenReady, so the workdir is applied once a conversation id appears, and the tooltip/button state stays in sync.clearWorkdircorrectly differentiates between conversation‑bound and pre‑conversation state, resetting both backend and preference.If you want a bit more robustness, you could wrap
devicePresenter.selectDirectory()in a try/catch to avoid bubbling any unexpected IPC errors out ofselectWorkdir, but functionally this composable looks solid.src/renderer/src/stores/chat.ts (1)
167-184: ACP workdir injection on thread creation looks correct but is intentionally conservativeCloning
settingsintonormalizedSettingsand only attachingacpWorkdirMapwhen:
- no map is present/empty,
providerId === 'acp',- and
modelIdis a non-empty string,is a good way to avoid mutating callers and only apply pending workdirs to ACP threads. You also correctly scope the injected map to the active
modelId.If in the future you want to support multiple ACP agents per conversation, you may consider copying over the entire
acpWorkdirMapinstead of just the selectedmodelIdentry, but the current behavior is coherent with a single-agent-per-thread model.src/renderer/settings/components/AcpSettings.vue (3)
467-505: Profile save handler covers all four modes; minor suggestion on explicit fallthrough handlingThe
switch (profileDialogState.mode)correctly handlesbuiltin-add,builtin-edit,custom-add, andcustom-edit, followed by a singlerefreshAfterMutationand success toast, withsavingProfileguarded via finally.You might optionally add a
defaultbranch that logs an error if an unknown mode appears, to make the code more future-proof against refactors:default: console.error('[ACP] Unknown profile dialog mode:', profileDialogState.mode) return
532-559: Manager delete flow is safe and user-friendly
handleManagerDelete:
- prompts via
window.confirm,- respects a boolean
removedresult from the presenter and shows a specific error toast if the profile cannot be deleted,- refreshes and shows a success toast on success.
If you later add a shared confirmation dialog component, this would be a good candidate to migrate away from
window.confirmfor a more consistent look, but functionally this is fine.
94-105: Consider localizingCMD,ARGS, andENVlabelsMinor i18n nit: the labels
"CMD:","ARGS:", and"ENV:"in both the builtin and custom sections are currently hard-coded. Everything else in this component goes throught(...), so you might consider adding translation keys for these to keep the UI fully localized.Also applies to: 170-180
src/renderer/settings/components/AcpProfileManagerDialog.vue (1)
88-123: Typing and env formatting helper look good; optional small i18n enhancementThe props and emits typing around
AcpBuiltinAgent,AcpAgentProfile, andAcpBuiltinAgentIdare precise and make the component easy to consume from TypeScript.
formatEnvsafely handles empty env maps and falls back tosettings.acp.none, returning a compactKEY=VALUElist.Tiny optional suggestion: similar to AcpSettings, if you want full i18n consistency, you could move the literal
"ENV:"label in the template into a translation key instead of keeping it hard-coded.src/renderer/src/stores/modelStore.ts (2)
115-116: Model type normalization is now consistent across code pathsNormalizing
model.typewith aModelType.Chatfallback innormalizeRendererModel,applyUserDefinedModelConfig,normalizeStoredModel, and thegetModelListmapping makes type handling more robust when upstream metadata omitstype. If you find this pattern spreading further, consider centralizing it in a small helper (e.g.resolveModelType(model, fallback?)) to avoid future drift, but what’s here is already safe and clear.Also applies to: 175-191, 322-323, 376-377
218-235: Enabled state update logic is sound; consider taming log noise
updateEnabledStatecorrectly maintainsenabledModelsper provider and reassigns the ref to trigger reactivity. The addedconsole.logis useful during development but may be noisy in production ifrefreshProviderModelsis called frequently. If logs become too verbose in real usage, consider gating them behind a debug flag or environment check.src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (1)
39-87: Session/workdir handling looks solid; consider tightening status typing and fallback path.The lazy creation in
updateWorkdirand theclearSessionsemantics (resettingsessionId/status but preserving workdir) are coherent and should behave well with the ACP provider.Two small optional refinements:
- The literal
'idle'is used as anAgentSessionLifecycleStatusvalue in bothupdateWorkdirandclearSession. If the status union ever changes, this will drift silently. Consider centralizing it (e.g., a typed constant or enum member) to keep compiler help.- In
getDefaultWorkdir, falling back toprocess.env.HOME || process.cwd()is fine for tests and most environments, butprocess.cwd()can be an unexpected location (e.g., install dir) ifHOMEis missing. You might prefer a more explicit OS‑aware fallback, or at least a comment documenting why cwd is acceptable here.src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts (3)
53-99: Content mapping is sensible; consider future‑proofing non‑text content handling.
pushContent/emitAsTextgive a clear mapping for text vs reasoning and image events/blocks, while safely stringifying audio/resources. That should work fine with current consumers.If you later surface richer audio/resource UIs, it might be worth:
- Emitting dedicated stream events for audio/resources (similar to
image_data), and/or- Truncating
JSON.stringifyoutput for largeresourcepayloads to avoid oversized reasoning/content blocks.For now this is an acceptable trade‑off.
111-159: Tool call lifecycle handling is well‑structured; watch for never‑completed calls.
handleToolCallUpdateplusgetOrCreateToolCallState/emitToolCallStartIfNeeded/emitToolCallChunk/emitToolCallEndprovide a clear lifecycle:
- Start event once per
(sessionId, toolCallId).- Reasoning updates on initial call or status change.
- Buffered args via
argumentsBuffer+ chunk events and blocks.- Cleanup in
emitToolCallEndby deleting fromtoolCallStates.One possible edge case: if a tool call never emits a
completed/failedstatus (e.g., session aborted mid‑call), its entry will remain intoolCallStatesuntil process exit. If ACP sessions can be long‑lived or heavily multiplexed, you might eventually want an explicit cleanup hook (e.g., on session close/timeout) to clear stale tool call state by sessionId.
215-236: JSON argument parsing strikes a good balance; optionally distinguish “invalid” vs “incomplete”.
tryParseJsonArguments:
- Trims and early‑returns undefined for empty buffers.
- Only attempts
JSON.parsewhen the buffer looks JSON‑like.- Logs a warning but still returns the raw string on parse failure, so you don’t lose the payload.
That’s a reasonable compromise. If you want slightly clearer downstream semantics, you could consider returning a tuple like
{ raw: string; validJson: boolean }or tagging the warning case, so UI or tooling can render “possibly incomplete JSON” differently from plain arguments. Not required, just an option.src/main/presenter/configPresenter/acpConfHelper.ts (1)
301-352: Legacy agent replacement API is robust; consider sharing more logic withmigrateLegacyAgents.
replaceWithLegacyAgents:
- Sanitizes incoming legacy agents via
normalizeLegacyAgent.- Rebuilds builtins/customs from scratch, using
BUILTIN_ORDERand default templates.- Persists normalized arrays and bumps the store version, returning sanitized configs.
This mirrors what
migrateLegacyAgentsdoes for the oldagentskey. If you find yourself evolving the legacy format again, you might want a shared internal helper for the “sanitize + partition into builtins/customs” pattern to avoid drift between these two flows. Behavior is fine as‑is.src/main/presenter/configPresenter/index.ts (1)
1066-1093: Agent mutation refresh path is pragmatic; consider guarding against noisy refreshes if needed.
refreshAcpProviderAgents:
- Locates the existing
acpprovider instance through the globalpresenter.- Casts to
AcpProviderand callsrefreshAgents(agentIds)when available.- Wraps the whole operation in a try/catch with a warn‑level log on failure.
This is a practical way to keep long‑running ACP processes in sync with config changes. If you ever see excessive refresh churn from rapid UI edits, you might later introduce simple debounce or coalescing around
agentIds, but that’s an optimization rather than a correctness issue.src/main/presenter/llmProviderPresenter/providers/acpProvider.ts (3)
109-134: Redundant providerId validation.Line 114 explicitly sets
providerId: this.provider.id, then lines 125-131 validate and re-fix the same field. Since no mutation occurs between these lines, the validation will never trigger. Consider removing the redundant check unless there's evidence of past issues.Apply this diff to remove the redundant validation:
const model: MODEL_META = { id: agent.id, name: agent.name, group: 'ACP', providerId: this.provider.id, // Ensure providerId is explicitly set isCustom: true, contextLength: 8192, maxTokens: 4096, description: agent.command, functionCall: true, reasoning: false, enableSearch: false, type: ModelType.Chat } - - // Validate that providerId is correctly set - if (model.providerId !== this.provider.id) { - console.error( - `[ACP] fetchProviderModels: Model ${model.id} has incorrect providerId: expected "${this.provider.id}", got "${model.providerId}"` - ) - model.providerId = this.provider.id // Fix it - } - return model
523-540: Clarify fallback when grant has no matching option.Lines 534-536: if
granted=truebut no matching permission option exists, the code defaults tocancelledwith a warning. This could confuse callers expecting the grant to succeed. Consider throwing an error instead, or document this edge case clearly.const option = this.pickPermissionOption(state.params.options, granted ? 'allow' : 'deny') if (option) { state.resolve({ outcome: { outcome: 'selected', optionId: option.optionId } }) } else if (granted) { - console.warn('[ACP] No matching permission option for grant, defaulting to cancel') - state.resolve({ outcome: { outcome: 'cancelled' } }) + const error = new Error(`[ACP] No matching allow option for grant request ${requestId}`) + console.error(error.message) + state.reject(error) } else { state.resolve({ outcome: { outcome: 'cancelled' } }) }
551-582: Verbose logging may impact performance.Line 574 logs every chunk from the stream, which could generate excessive logs in production. Consider removing this line or gating it behind a debug/trace flag.
- for await (const chunk of this.coreStream( + for await (const chunk of this.coreStream( messages, modelId, mergedConfig, temperature, maxTokens, [] )) { - console.log('[ACP] collectFromStream: chunk:', chunk) if (chunk.type === 'text' && chunk.content) {src/shared/types/presenters/legacy.presenters.d.ts (1)
675-740: Consider consolidating overlapping types.
AcpAgentConfig(lines 709-715) andAcpCustomAgent(lines 693-700) have nearly identical structure, withAcpCustomAgentadding only anenabledfield. This duplication may cause confusion. Consider makingAcpAgentConfiga base type or merging them with optionalenabledfield.Example consolidation:
export interface AcpAgentConfig { id: string name: string command: string args?: string[] env?: Record<string, string> enabled?: boolean // Optional, present for custom agents } export type AcpCustomAgent = Required<Pick<AcpAgentConfig, 'enabled'>> & AcpAgentConfig
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (88)
package.json(1 hunks)src/main/presenter/configPresenter/acpConfHelper.ts(1 hunks)src/main/presenter/configPresenter/index.ts(6 hunks)src/main/presenter/configPresenter/providerHelper.ts(1 hunks)src/main/presenter/configPresenter/providerModelHelper.ts(2 hunks)src/main/presenter/configPresenter/providers.ts(1 hunks)src/main/presenter/index.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/acpMessageFormatter.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/acpSessionManager.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts(1 hunks)src/main/presenter/llmProviderPresenter/agent/types.ts(1 hunks)src/main/presenter/llmProviderPresenter/baseAgentProvider.ts(1 hunks)src/main/presenter/llmProviderPresenter/baseProvider.ts(2 hunks)src/main/presenter/llmProviderPresenter/index.ts(7 hunks)src/main/presenter/llmProviderPresenter/managers/agentLoopHandler.ts(3 hunks)src/main/presenter/llmProviderPresenter/managers/modelManager.ts(2 hunks)src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts(9 hunks)src/main/presenter/llmProviderPresenter/providers/acpProvider.ts(1 hunks)src/main/presenter/sqlitePresenter/index.ts(6 hunks)src/main/presenter/sqlitePresenter/tables/acpSessions.ts(1 hunks)src/main/presenter/threadPresenter/const.ts(1 hunks)src/main/presenter/threadPresenter/handlers/permissionHandler.ts(6 hunks)src/main/presenter/threadPresenter/handlers/streamGenerationHandler.ts(2 hunks)src/main/presenter/threadPresenter/handlers/toolCallHandler.ts(3 hunks)src/main/presenter/threadPresenter/index.ts(3 hunks)src/renderer/settings/components/AcpProfileDialog.vue(1 hunks)src/renderer/settings/components/AcpProfileManagerDialog.vue(1 hunks)src/renderer/settings/components/AcpSettings.vue(1 hunks)src/renderer/settings/components/ModelProviderSettings.vue(2 hunks)src/renderer/settings/main.ts(5 hunks)src/renderer/src/components/ModelChooser.vue(1 hunks)src/renderer/src/components/ModelSelect.vue(1 hunks)src/renderer/src/components/NewThread.vue(4 hunks)src/renderer/src/components/chat-input/ChatInput.vue(5 hunks)src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts(1 hunks)src/renderer/src/components/icons/ModelIcon.vue(1 hunks)src/renderer/src/components/message/MessageBlockPermissionRequest.vue(3 hunks)src/renderer/src/components/message/MessageBlockToolCall.vue(2 hunks)src/renderer/src/components/message/MessageItemAssistant.vue(1 hunks)src/renderer/src/components/ui/MessageDialog.vue(2 hunks)src/renderer/src/i18n/en-US/chat.json(2 hunks)src/renderer/src/i18n/en-US/routes.json(1 hunks)src/renderer/src/i18n/en-US/settings.json(1 hunks)src/renderer/src/i18n/fa-IR/chat.json(2 hunks)src/renderer/src/i18n/fa-IR/routes.json(1 hunks)src/renderer/src/i18n/fa-IR/settings.json(1 hunks)src/renderer/src/i18n/fr-FR/chat.json(2 hunks)src/renderer/src/i18n/fr-FR/routes.json(1 hunks)src/renderer/src/i18n/fr-FR/settings.json(1 hunks)src/renderer/src/i18n/ja-JP/chat.json(2 hunks)src/renderer/src/i18n/ja-JP/routes.json(1 hunks)src/renderer/src/i18n/ja-JP/settings.json(1 hunks)src/renderer/src/i18n/ko-KR/chat.json(2 hunks)src/renderer/src/i18n/ko-KR/routes.json(1 hunks)src/renderer/src/i18n/ko-KR/settings.json(1 hunks)src/renderer/src/i18n/pt-BR/chat.json(2 hunks)src/renderer/src/i18n/pt-BR/routes.json(1 hunks)src/renderer/src/i18n/pt-BR/settings.json(1 hunks)src/renderer/src/i18n/ru-RU/chat.json(2 hunks)src/renderer/src/i18n/ru-RU/routes.json(1 hunks)src/renderer/src/i18n/ru-RU/settings.json(1 hunks)src/renderer/src/i18n/zh-CN/chat.json(2 hunks)src/renderer/src/i18n/zh-CN/routes.json(1 hunks)src/renderer/src/i18n/zh-CN/settings.json(1 hunks)src/renderer/src/i18n/zh-HK/chat.json(2 hunks)src/renderer/src/i18n/zh-HK/routes.json(1 hunks)src/renderer/src/i18n/zh-HK/settings.json(1 hunks)src/renderer/src/i18n/zh-TW/chat.json(2 hunks)src/renderer/src/i18n/zh-TW/routes.json(1 hunks)src/renderer/src/i18n/zh-TW/settings.json(1 hunks)src/renderer/src/stores/agentModelStore.ts(1 hunks)src/renderer/src/stores/chat.ts(5 hunks)src/renderer/src/stores/mcp.ts(2 hunks)src/renderer/src/stores/modelStore.ts(10 hunks)src/renderer/src/stores/providerStore.ts(4 hunks)src/shared/types/core/agent-events.ts(2 hunks)src/shared/types/core/llm-events.ts(5 hunks)src/shared/types/index.d.ts(1 hunks)src/shared/types/presenters/agent-provider.d.ts(1 hunks)src/shared/types/presenters/index.d.ts(1 hunks)src/shared/types/presenters/legacy.presenters.d.ts(10 hunks)src/shared/types/presenters/llmprovider.presenter.d.ts(4 hunks)src/shared/types/presenters/thread.presenter.d.ts(3 hunks)test/main/presenter/llmProviderPresenter.test.ts(4 hunks)test/main/presenter/llmProviderPresenter/acpContentMapper.test.ts(1 hunks)test/main/presenter/threadPresenter/permissionHandler.test.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-28T05:55:31.482Z
Learnt from: zerob13
Repo: ThinkInAIXYZ/deepchat PR: 804
File: src/main/presenter/llmProviderPresenter/providers/tokenfluxProvider.ts:153-156
Timestamp: 2025-08-28T05:55:31.482Z
Learning: TokenFlux models generally support function calling by default, so it's reasonable to assume hasFunctionCalling = true for TokenFlux provider implementations in src/main/presenter/llmProviderPresenter/providers/tokenfluxProvider.ts
Applied to files:
src/renderer/src/stores/modelStore.ts
📚 Learning: 2025-06-21T15:49:17.044Z
Learnt from: neoragex2002
Repo: ThinkInAIXYZ/deepchat PR: 550
File: src/renderer/src/stores/chat.ts:1011-1035
Timestamp: 2025-06-21T15:49:17.044Z
Learning: In src/renderer/src/stores/chat.ts, the user prefers to keep both `text` and `content` properties in the `handleMeetingInstruction` function's `sendMessage` call, even though they are redundant, rather than removing the `content` property.
Applied to files:
src/renderer/src/stores/chat.ts
🧬 Code graph analysis (32)
src/shared/types/presenters/agent-provider.d.ts (1)
src/main/presenter/llmProviderPresenter/agent/types.ts (4)
AgentSessionLifecycleStatus(11-11)AgentSessionState(12-12)AgentProcessStatus(10-10)AgentProcessHandle(9-9)
src/main/presenter/threadPresenter/handlers/toolCallHandler.ts (1)
src/shared/types/core/llm-events.ts (1)
PermissionRequestPayload(167-185)
src/main/presenter/llmProviderPresenter/managers/modelManager.ts (1)
scripts/fetch-provider-db.mjs (1)
models(37-37)
src/main/presenter/index.ts (1)
src/main/presenter/llmProviderPresenter/index.ts (1)
LLMProviderPresenter(34-499)
src/main/presenter/llmProviderPresenter/baseAgentProvider.ts (1)
src/main/presenter/llmProviderPresenter/agent/types.ts (4)
AgentSessionManager(15-30)AgentProcessManager(32-41)AgentPermissionRequest(50-57)AgentPermissionResult(59-61)
src/main/presenter/llmProviderPresenter/agent/acpSessionManager.ts (2)
src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (4)
AcpProcessManager(41-276)SessionNotificationHandler(25-25)PermissionResolver(27-29)AcpProcessHandle(14-19)src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (1)
AcpSessionPersistence(8-88)
src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (3)
src/main/presenter/llmProviderPresenter/agent/types.ts (2)
AgentProcessHandle(9-9)AgentProcessManager(32-41)src/shared/types/presenters/legacy.presenters.d.ts (1)
AcpAgentConfig(709-715)test/mocks/electron.ts (1)
app(2-10)
src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts (1)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)
test/main/presenter/threadPresenter/permissionHandler.test.ts (6)
src/shared/chat.d.ts (1)
AssistantMessage(39-42)src/main/presenter/threadPresenter/types.ts (1)
GeneratingMessageState(4-36)src/main/presenter/threadPresenter/managers/messageManager.ts (1)
MessageManager(21-379)src/shared/types/presenters/legacy.presenters.d.ts (1)
ILlmProviderPresenter(764-855)src/shared/types/presenters/llmprovider.presenter.d.ts (1)
ILlmProviderPresenter(130-222)src/main/presenter/threadPresenter/handlers/baseHandler.ts (1)
ThreadHandlerContext(5-11)
src/shared/types/presenters/thread.presenter.d.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (1)
AcpWorkdirInfo(736-739)
src/main/presenter/configPresenter/providerModelHelper.ts (1)
scripts/fetch-provider-db.mjs (1)
models(37-37)
src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (2)
ISQLitePresenter(279-351)AcpSessionEntity(717-727)test/mocks/electron.ts (1)
app(2-10)
src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts (1)
src/shared/types/core/llm-events.ts (1)
createStreamEvent(100-154)
test/main/presenter/llmProviderPresenter.test.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (1)
ISQLitePresenter(279-351)src/main/presenter/llmProviderPresenter/index.ts (1)
LLMProviderPresenter(34-499)
src/main/presenter/llmProviderPresenter/baseProvider.ts (1)
scripts/fetch-provider-db.mjs (1)
models(37-37)
src/main/presenter/llmProviderPresenter/agent/acpMessageFormatter.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (2)
ChatMessage(1519-1519)ModelConfig(133-152)
test/main/presenter/llmProviderPresenter/acpContentMapper.test.ts (1)
src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts (1)
AcpContentMapper(21-328)
src/main/presenter/configPresenter/providerHelper.ts (1)
src/main/eventbus.ts (1)
eventBus(151-151)
src/renderer/src/stores/agentModelStore.ts (1)
src/renderer/src/composables/usePresenter.ts (1)
usePresenter(103-105)
src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (2)
LLM_PROVIDER(638-658)IConfigPresenter(402-607)src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (1)
AcpSessionPersistence(8-88)
src/renderer/src/stores/modelStore.ts (2)
src/renderer/src/stores/agentModelStore.ts (1)
useAgentModelStore(22-141)src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.ts (1)
isAgentProvider(243-259)
src/main/presenter/threadPresenter/index.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (1)
AcpWorkdirInfo(736-739)
src/main/presenter/llmProviderPresenter/index.ts (3)
src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (1)
AcpSessionPersistence(8-88)src/shared/types/presenters/legacy.presenters.d.ts (2)
ISQLitePresenter(279-351)AcpWorkdirInfo(736-739)src/main/presenter/llmProviderPresenter/providers/acpProvider.ts (1)
AcpProvider(51-645)
src/main/presenter/llmProviderPresenter/agent/types.ts (1)
src/shared/types/presenters/agent-provider.d.ts (2)
AgentSessionState(3-12)AgentProcessHandle(16-24)
src/renderer/src/stores/chat.ts (2)
src/shared/types/presenters/legacy.presenters.d.ts (1)
CONVERSATION_SETTINGS(857-874)src/shared/types/presenters/thread.presenter.d.ts (1)
CONVERSATION_SETTINGS(15-31)
src/main/presenter/sqlitePresenter/index.ts (2)
src/main/presenter/sqlitePresenter/tables/acpSessions.ts (2)
AcpSessionsTable(36-210)AcpSessionUpsertData(29-34)src/shared/types/presenters/legacy.presenters.d.ts (1)
AcpSessionEntity(717-727)
src/main/presenter/configPresenter/acpConfHelper.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (6)
AcpBuiltinAgentId(675-675)AcpAgentProfile(677-683)AcpStoreData(702-707)AcpAgentConfig(709-715)AcpBuiltinAgent(685-691)AcpCustomAgent(693-700)
src/shared/types/presenters/llmprovider.presenter.d.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (1)
AcpWorkdirInfo(736-739)
src/main/presenter/configPresenter/index.ts (6)
src/main/presenter/configPresenter/acpConfHelper.ts (1)
AcpConfHelper(64-718)src/shared/types/presenters/legacy.presenters.d.ts (5)
AcpAgentConfig(709-715)AcpBuiltinAgent(685-691)AcpCustomAgent(693-700)AcpBuiltinAgentId(675-675)AcpAgentProfile(677-683)src/main/presenter/index.ts (1)
presenter(223-223)src/main/presenter/llmProviderPresenter/providers/acpProvider.ts (1)
AcpProvider(51-645)src/main/eventbus.ts (1)
eventBus(151-151)src/main/events.ts (1)
CONFIG_EVENTS(12-43)
src/main/presenter/llmProviderPresenter/providers/acpProvider.ts (8)
src/shared/types/core/llm-events.ts (4)
LLMCoreStreamEvent(87-98)createStreamEvent(100-154)PermissionRequestPayload(167-185)PermissionRequestOption(161-165)src/shared/types/presenters/legacy.presenters.d.ts (7)
LLMCoreStreamEvent(1516-1516)AcpAgentConfig(709-715)LLM_PROVIDER(638-658)IConfigPresenter(402-607)MODEL_META(623-637)ChatMessage(1519-1519)ModelConfig(133-152)src/main/presenter/llmProviderPresenter/agent/acpSessionManager.ts (2)
AcpSessionManager(30-213)AcpSessionRecord(24-28)src/main/presenter/llmProviderPresenter/agent/acpProcessManager.ts (1)
AcpProcessManager(41-276)src/main/presenter/llmProviderPresenter/agent/acpSessionPersistence.ts (1)
AcpSessionPersistence(8-88)src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts (1)
AcpContentMapper(21-328)src/main/presenter/llmProviderPresenter/agent/acpMessageFormatter.ts (1)
AcpMessageFormatter(9-93)src/main/eventbus.ts (1)
eventBus(151-151)
src/shared/types/core/agent-events.ts (1)
src/shared/types/core/llm-events.ts (1)
PermissionRequestOption(161-165)
src/shared/types/presenters/legacy.presenters.d.ts (1)
src/shared/types/presenters/agent-provider.d.ts (1)
AgentSessionLifecycleStatus(1-1)
| constructor(options: AcpSessionManagerOptions) { | ||
| this.providerId = options.providerId | ||
| this.processManager = options.processManager | ||
| this.sessionPersistence = options.sessionPersistence | ||
|
|
||
| app.on('before-quit', () => { | ||
| void this.clearAllSessions() | ||
| }) | ||
| } |
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.
Concurrent getOrCreateSession calls ignore agent/workdir differences
pendingSessions is keyed only by conversationId. If, in an edge case, two concurrent getOrCreateSession calls are made for the same conversationId but with different agent or workdir arguments, the second call will just await the first in‑flight promise and receive a session bound to the first agent/workdir.
This may never happen in normal UI flow (one agent per conversation at a time), but if you expect more complex orchestration it might be safer to incorporate agent.id and/or resolvedWorkdir into the pending key, e.g.:
- private readonly pendingSessions = new Map<string, Promise<AcpSessionRecord>>()
+ private readonly pendingSessions = new Map<string, Promise<AcpSessionRecord>>()
async getOrCreateSession(
conversationId: string,
agent: AcpAgentConfig,
hooks: SessionHooks,
workdir?: string | null
): Promise<AcpSessionRecord> {
const resolvedWorkdir = this.sessionPersistence.resolveWorkdir(workdir)
+ const pendingKey = `${conversationId}:${agent.id}:${resolvedWorkdir}`
const existing = this.sessionsByConversation.get(conversationId)
// ...
- const inflight = this.pendingSessions.get(conversationId)
+ const inflight = this.pendingSessions.get(pendingKey)
if (inflight) {
return inflight
}
- const createPromise = this.createSession(conversationId, agent, hooks, resolvedWorkdir)
- this.pendingSessions.set(conversationId, createPromise)
+ const createPromise = this.createSession(conversationId, agent, hooks, resolvedWorkdir)
+ this.pendingSessions.set(pendingKey, createPromise)
try {
const session = await createPromise
// ...
return session
} finally {
- this.pendingSessions.delete(conversationId)
+ this.pendingSessions.delete(pendingKey)
}
}Not strictly necessary if the higher layers guarantee serialized access, but it would make this component more robust to future changes.
Also applies to: 48-90
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/agent/acpSessionManager.ts around
lines 38 to 46 (also apply same change for the logic between lines 48-90),
pendingSessions is currently keyed only by conversationId so concurrent
getOrCreateSession calls with the same conversationId but different agent or
workdir can return the wrong session; change the pendingSessions key to include
agent.id (or a unique agent identifier) and the resolvedWorkdir (e.g.,
conversationId + '|' + agent.id + '|' + resolvedWorkdir) so each distinct
agent/workdir combination gets its own in-flight promise, and ensure you
normalize null/undefined agent or workdir into a stable string when building the
key and use the same composite key for storing and awaiting pendingSessions.
| public cleanup(): void { | ||
| void this.getSessionManager() | ||
| .clearAllSessions() | ||
| .catch((error) => { | ||
| console.warn( | ||
| `[AgentProvider] Failed to clear sessions for provider "${this.provider.id}":`, | ||
| error | ||
| ) | ||
| }) | ||
|
|
||
| void this.getProcessManager() | ||
| .shutdown() | ||
| .catch((error) => { | ||
| console.warn( | ||
| `[AgentProvider] Failed to shutdown process manager for provider "${this.provider.id}":`, | ||
| error | ||
| ) | ||
| }) | ||
| } |
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
cleanup fire-and-forget pattern is reasonable; consider guarding manager getters
Using void ... .clearAllSessions().catch(...) and void ... .shutdown().catch(...) is a pragmatic way to keep cleanup() sync in the public API while still handling async teardown and logging failures.
One small hardening option: wrap getSessionManager() / getProcessManager() in try/catch in case a concrete implementation throws synchronously, which would currently bypass the .catch handlers and prevent the second teardown from running:
public cleanup(): void {
try {
void this.getSessionManager().clearAllSessions().catch(/* log */)
} catch (error) {
console.warn('[AgentProvider] Failed to obtain session manager:', error)
}
try {
void this.getProcessManager().shutdown().catch(/* log */)
} catch (error) {
console.warn('[AgentProvider] Failed to obtain process manager:', error)
}
}🌐 Web query:
Is it idiomatic in TypeScript/Node to keep provider cleanup methods synchronous while performing async teardown in a fire-and-forget manner, or is returning a Promise preferred for lifecycle hooks?
💡 Result:
Short answer: prefer returning a Promise / making the teardown async and awaited. Most Node frameworks and graceful-shutdown helpers expect/allow async hooks and will await them; fire‑and‑forget teardown is error‑prone (unhandled rejections, lost cleanup if the process exits) unless you build robust orchestration around it.
Why (brief):
- Frameworks (Fastify, Nest, etc.) support async lifecycle hooks and will wait for returned Promises (or provide explicit shutdown hooks you can await). Use those so the framework coordinates shutdown. [1][2]
- Fire‑and‑forget teardown can produce unhandled rejections and resources not being closed before the process exits; Node best‑practice guides and graceful‑shutdown libraries prefer awaiting async cleanup. [3][4]
- Practical rule: if the caller cares about the resource being cleanly closed (server shutdown, DB clients, job processors), expose/return a Promise and let the supervisor await it; use fire‑and‑forget only for noncritical background tasks that truly don’t need deterministic completion. [1][3][4]
If you want, tell me which framework or lifecycle hook you’re using and I’ll give an exact pattern (sync vs async) to implement.
Sources:
[1] Fastify hooks docs (onClose/onReady support async or callback forms).
[2] Fastify server.close semantics (returns Promise/awaits onClose).
[3] Node.js best-practices / community guides on async error handling and awaiting shutdown.
[4] node-graceful npm README (examples that await async exit handlers).
🏁 Script executed:
#!/bin/bash
# Find where cleanup() is called in the codebase
rg -n "\.cleanup\(\)" --type ts --type js -C 3Length of output: 1198
🏁 Script executed:
#!/bin/bash
# Check the baseAgentProvider file for imports and class definition
head -50 src/main/presenter/llmProviderPresenter/baseAgentProvider.tsLength of output: 1557
🏁 Script executed:
#!/bin/bash
# Search for framework setup or lifecycle hook patterns
rg -n "onClose|onShutdown|lifecycle|destroy" --type ts --type js -g "!**/node_modules/**" -C 2 | head -100Length of output: 6127
🏁 Script executed:
#!/bin/bash
# Get full context around cleanup() call in providerInstanceManager
sed -n '365,380p' src/main/presenter/llmProviderPresenter/managers/providerInstanceManager.tsLength of output: 538
🏁 Script executed:
#!/bin/bash
# Find all cleanup() method definitions, not just calls
rg -n "cleanup\(\)" --type ts --type js -B 2 | grep -E "(public|private|protected).*cleanup|async.*cleanup"Length of output: 167
🏁 Script executed:
#!/bin/bash
# Check if scrollCapture cleanup returns a Promise
rg -n "cleanup" src/main/lib/scrollCapture.ts -B 5 -A 5 | head -40Length of output: 434
Change cleanup() to return Promise<void> and await it at call sites
The fire-and-forget pattern is not idiomatic for provider cleanup. Most Node frameworks and graceful-shutdown helpers expect/allow async hooks and will await them; fire‑and‑forget teardown is error‑prone (unhandled rejections, lost cleanup if the process exits) unless you build robust orchestration around it. If the caller cares about the resource being cleanly closed, expose/return a Promise and let the supervisor await it.
Change baseAgentProvider.ts line 28 from public cleanup(): void to async cleanup(): Promise<void> and remove the void keywords wrapping the promise chains. Then update providerInstanceManager.ts line 373 to await (instance as any).cleanup() so the resource teardown completes before the instance is removed.
The try/catch suggestion for the getters is optional, but the return type change is necessary for proper async lifecycle management.
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/baseAgentProvider.ts around lines 28
to 46, change the synchronous fire-and-forget cleanup to an async, awaitable
method by updating the signature from public cleanup(): void to public async
cleanup(): Promise<void>, remove the void prefixes before
getSessionManager().clearAllSessions() and getProcessManager().shutdown() so
their returned promises are awaited inside the method, and propagate errors
normally (or optionally wrap each await in try/catch if you want per-operation
handling); then update the call site in providerInstanceManager.ts at line 373
to await (instance as any).cleanup() so teardown completes before the instance
is removed.
| constructor( | ||
| provider: LLM_PROVIDER, | ||
| configPresenter: IConfigPresenter, | ||
| sessionPersistence: AcpSessionPersistence | ||
| ) { | ||
| super(provider, configPresenter) | ||
| this.sessionPersistence = sessionPersistence | ||
| this.processManager = new AcpProcessManager({ providerId: provider.id }) | ||
| this.sessionManager = new AcpSessionManager({ | ||
| providerId: provider.id, | ||
| processManager: this.processManager, | ||
| sessionPersistence: this.sessionPersistence | ||
| }) | ||
|
|
||
| void this.initWhenEnabled() | ||
| } |
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.
Async initialization in constructor risks unhandled rejections.
Line 78 starts initWhenEnabled() without awaiting. If initialization fails, the promise rejection may go unhandled. Consider moving async initialization to an explicit init() call by the factory/caller, or document that errors are handled internally.
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/providers/acpProvider.ts around lines
64 to 79, the constructor starts initWhenEnabled() with "void" which risks
unhandled promise rejections; remove the fire-and-forget call from the
constructor, add a public async init() method that calls initWhenEnabled() and
awaits it (or calls it inside a try/catch and logs/handles errors), and update
the factory/caller to call await acpProvider.init() after construction;
alternatively, if you must keep internal startup, ensure initWhenEnabled() is
invoked inside the constructor only via a self-contained try/catch that handles
all errors to prevent unhandled rejections.
| public async updateAcpWorkdir( | ||
| conversationId: string, | ||
| agentId: string, | ||
| workdir: string | null | ||
| ): Promise<void> { | ||
| const trimmed = workdir?.trim() ? workdir : null | ||
| const existing = await this.sessionPersistence.getSessionData(conversationId, agentId) | ||
| const previous = existing?.workdir ?? null | ||
| await this.sessionPersistence.updateWorkdir(conversationId, agentId, trimmed) | ||
| const previousResolved = this.sessionPersistence.resolveWorkdir(previous) | ||
| const nextResolved = this.sessionPersistence.resolveWorkdir(trimmed) | ||
| if (previousResolved !== nextResolved) { | ||
| try { | ||
| await this.sessionManager.clearSession(conversationId) | ||
| } catch (error) { | ||
| console.warn('[ACP] Failed to clear session after workdir update:', error) | ||
| } | ||
| } | ||
| } |
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.
Logic error: trimmed value not used.
Line 367 checks workdir?.trim() but assigns the original workdir to trimmed if truthy. This means trimmed may contain leading/trailing whitespace. The trimmed value is never actually captured.
Apply this diff to fix the logic:
- const trimmed = workdir?.trim() ? workdir : null
+ const trimmed = workdir?.trim() || nullOr more explicitly:
- const trimmed = workdir?.trim() ? workdir : null
+ const temp = workdir?.trim()
+ const trimmed = temp || null📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public async updateAcpWorkdir( | |
| conversationId: string, | |
| agentId: string, | |
| workdir: string | null | |
| ): Promise<void> { | |
| const trimmed = workdir?.trim() ? workdir : null | |
| const existing = await this.sessionPersistence.getSessionData(conversationId, agentId) | |
| const previous = existing?.workdir ?? null | |
| await this.sessionPersistence.updateWorkdir(conversationId, agentId, trimmed) | |
| const previousResolved = this.sessionPersistence.resolveWorkdir(previous) | |
| const nextResolved = this.sessionPersistence.resolveWorkdir(trimmed) | |
| if (previousResolved !== nextResolved) { | |
| try { | |
| await this.sessionManager.clearSession(conversationId) | |
| } catch (error) { | |
| console.warn('[ACP] Failed to clear session after workdir update:', error) | |
| } | |
| } | |
| } | |
| public async updateAcpWorkdir( | |
| conversationId: string, | |
| agentId: string, | |
| workdir: string | null | |
| ): Promise<void> { | |
| const trimmed = workdir?.trim() || null | |
| const existing = await this.sessionPersistence.getSessionData(conversationId, agentId) | |
| const previous = existing?.workdir ?? null | |
| await this.sessionPersistence.updateWorkdir(conversationId, agentId, trimmed) | |
| const previousResolved = this.sessionPersistence.resolveWorkdir(previous) | |
| const nextResolved = this.sessionPersistence.resolveWorkdir(trimmed) | |
| if (previousResolved !== nextResolved) { | |
| try { | |
| await this.sessionManager.clearSession(conversationId) | |
| } catch (error) { | |
| console.warn('[ACP] Failed to clear session after workdir update:', error) | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/providers/acpProvider.ts around lines
362 to 380, the variable `trimmed` is set to the original `workdir` when
`workdir?.trim()` is truthy, so leading/trailing whitespace is not removed;
change the assignment to use the actual trimmed string (e.g., assign `trimmed =
workdir?.trim() ? workdir.trim() : null` or `trimmed = workdir?.trim() ?? null`)
so subsequent calls to updateWorkdir and resolveWorkdir use the trimmed value;
keep the rest of the flow (fetch existing, compare resolved paths, clear session
on change) unchanged.
| async upsert(conversationId: string, agentId: string, data: AcpSessionUpsertData): Promise<void> { | ||
| const now = Date.now() | ||
| const payload = { | ||
| conversationId, | ||
| agentId, | ||
| sessionId: data.sessionId ?? null, | ||
| workdir: data.workdir ?? null, | ||
| status: data.status ?? 'idle', | ||
| metadata: data.metadata ? JSON.stringify(data.metadata) : null, | ||
| createdAt: now, | ||
| updatedAt: now | ||
| } | ||
|
|
||
| this.db | ||
| .prepare( | ||
| ` | ||
| INSERT INTO acp_sessions ( | ||
| conversation_id, | ||
| agent_id, | ||
| session_id, | ||
| workdir, | ||
| status, | ||
| metadata, | ||
| created_at, | ||
| updated_at | ||
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) | ||
| ON CONFLICT(conversation_id, agent_id) DO UPDATE SET | ||
| session_id = excluded.session_id, | ||
| workdir = excluded.workdir, | ||
| status = excluded.status, | ||
| metadata = excluded.metadata, | ||
| updated_at = excluded.updated_at | ||
| ` | ||
| ) | ||
| .run( | ||
| payload.conversationId, | ||
| payload.agentId, | ||
| payload.sessionId, | ||
| payload.workdir, | ||
| payload.status, | ||
| payload.metadata, | ||
| payload.createdAt, | ||
| payload.updatedAt | ||
| ) | ||
| } |
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
upsert silently resets status to 'idle' when data.status is omitted
Current logic:
const payload = {
// ...
status: data.status ?? 'idle',
// ...
}
...
ON CONFLICT(conversation_id, agent_id) DO UPDATE SET
session_id = excluded.session_id,
workdir = excluded.workdir,
status = excluded.status,
metadata = excluded.metadata,
updated_at = excluded.updated_atBecause AcpSessionUpsertData.status is optional, any call to upsert(...) that omits status will:
- Use
'idle'in the insert payload, and - On conflict, overwrite the existing row’s
statusto'idle'.
That makes status effectively “required” for updates, despite being optional in the type, and risks unintentionally downgrading active/error sessions back to idle when you only mean to update sessionId, workdir, or metadata.
If that’s not intentional, consider one of these approaches:
- Treat
statusas required inAcpSessionUpsertDataso the type matches current behavior; or - Make the SQL conditional, e.g.:
- ON CONFLICT(conversation_id, agent_id) DO UPDATE SET
- session_id = excluded.session_id,
- workdir = excluded.workdir,
- status = excluded.status,
- metadata = excluded.metadata,
- updated_at = excluded.updated_at
+ ON CONFLICT(conversation_id, agent_id) DO UPDATE SET
+ session_id = excluded.session_id,
+ workdir = excluded.workdir,
+ status = CASE
+ WHEN excluded.status IS NOT NULL THEN excluded.status
+ ELSE acp_sessions.status
+ END,
+ metadata = excluded.metadata,
+ updated_at = excluded.updated_atpaired with setting status to NULL in the payload when data.status is omitted and loosening the NOT NULL constraint, or by generating the SET clause dynamically and only including status when present in data.
Right now this is a silent data‑semantics change risk and should be clarified.
I'll verify this review comment by examining the type definition and call sites of the upsert method.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find AcpSessionUpsertData type definition
rg -n "AcpSessionUpsertData" --type=ts -A 10 -B 2
Length of output: 4275
🏁 Script executed:
#!/bin/bash
# Search for calls to the upsert method on ACP sessions
rg -n "\.upsert\(" --type=ts -B 3 -A 3 | head -100Length of output: 518
🏁 Script executed:
#!/bin/bash
# Look for the ACP sessions table class/file to understand the structure
fd "acpSessions" --type=tsLength of output: 169
🏁 Script executed:
#!/bin/bash
# Search for any tests related to upsert to understand expected behavior
rg -n "upsert" --type=ts -g "**test**" -g "**spec**" -B 2 -A 5Length of output: 886
🏁 Script executed:
#!/bin/bash
# Check the full file to understand context around the upsert method
cat src/main/presenter/sqlitePresenter/tables/acpSessions.tsLength of output: 5594
Now let me verify actual usage patterns to confirm the bug manifests:
🏁 Script executed:
#!/bin/bash
# Find all calls to upsertAcpSession to see how it's used
rg -n "upsertAcpSession" --type=ts -B 2 -A 2 -g "!test"Length of output: 1383
🏁 Script executed:
#!/bin/bash
# Check if there are calls where status is intentionally omitted
rg -n "upsertAcpSession\(" --type=ts -A 5 -g "!test" | grep -v "test"Length of output: 1558
Fix type-implementation mismatch in upsert to prevent silent status resets
The issue is valid. AcpSessionUpsertData.status is optional (allowing partial updates), but the upsert implementation unconditionally defaults it to 'idle' and applies it on conflict. This creates a trap: any future call omitting status (e.g., upsert(convId, agentId, { sessionId, workdir })) will silently reset existing sessions from 'active'/'error' back to 'idle'.
The dedicated updateStatus() method exists with a targeted SQL UPDATE, signaling the API design intent: upsert should not unconditionally mutate status.
Choose one approach:
- Make
statusrequired inAcpSessionUpsertDataif it must always be provided, or - Conditionally update
statusonly when present indata(suggested SQL fix in the review is sound; pair withstatus: nullin payload when omitted and loosen the NOT NULL constraint, or dynamically generate the SET clause).
src/main/presenter/sqlitePresenter/tables/acpSessions.ts, lines 86–130
🤖 Prompt for AI Agents
src/main/presenter/sqlitePresenter/tables/acpSessions.ts around lines 86–130:
the upsert unconditionally defaults and applies status = 'idle', which causes
omitted status in callers to reset existing sessions; change upsert to not
default status and to only include status in the INSERT/ON CONFLICT DO UPDATE
SET when data.status is explicitly provided (i.e., build the SQL and parameter
list dynamically: omit status from VALUES and from the DO UPDATE SET clause when
data.status is undefined, otherwise include it), ensuring parameter ordering
matches the constructed SQL so existing status is not overwritten on partial
upserts.
| "rateLimitWaitingTooltip": "あと{seconds}秒待機、間隔{interval}秒", | ||
| "acpWorkdir": "ACP Workdir", | ||
| "acpWorkdirTooltip": "Set ACP working directory", | ||
| "acpWorkdirSelect": "Select a folder to use as the ACP workdir", | ||
| "acpWorkdirChange": "Reset to default workdir", | ||
| "acpWorkdirCurrent": "Current workdir: {path}" |
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.
ACP workdir strings are in English, not Japanese.
Lines 20-24 contain English text instead of Japanese translations:
- "ACP Workdir"
- "Set ACP working directory"
- "Select a folder to use as the ACP workdir"
- etc.
This is inconsistent with other locale files (e.g., zh-CN/chat.json has Chinese translations for these keys).
Replace with Japanese translations:
- "acpWorkdir": "ACP Workdir",
- "acpWorkdirTooltip": "Set ACP working directory",
- "acpWorkdirSelect": "Select a folder to use as the ACP workdir",
- "acpWorkdirChange": "Reset to default workdir",
- "acpWorkdirCurrent": "Current workdir: {path}"
+ "acpWorkdir": "作業ディレクトリ",
+ "acpWorkdirTooltip": "ACP作業ディレクトリを設定",
+ "acpWorkdirSelect": "作業ディレクトリとして使用するフォルダを選択",
+ "acpWorkdirChange": "デフォルトの作業ディレクトリに戻す",
+ "acpWorkdirCurrent": "現在の作業ディレクトリ:{path}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "rateLimitWaitingTooltip": "あと{seconds}秒待機、間隔{interval}秒", | |
| "acpWorkdir": "ACP Workdir", | |
| "acpWorkdirTooltip": "Set ACP working directory", | |
| "acpWorkdirSelect": "Select a folder to use as the ACP workdir", | |
| "acpWorkdirChange": "Reset to default workdir", | |
| "acpWorkdirCurrent": "Current workdir: {path}" | |
| "rateLimitWaitingTooltip": "あと{seconds}秒待機、間隔{interval}秒", | |
| "acpWorkdir": "作業ディレクトリ", | |
| "acpWorkdirTooltip": "ACP作業ディレクトリを設定", | |
| "acpWorkdirSelect": "作業ディレクトリとして使用するフォルダを選択", | |
| "acpWorkdirChange": "デフォルトの作業ディレクトリに戻す", | |
| "acpWorkdirCurrent": "現在の作業ディレクトリ:{path}" |
🤖 Prompt for AI Agents
In src/renderer/src/i18n/ja-JP/chat.json around lines 19 to 24, the ACP-related
values are still in English; replace them with Japanese translations: set
"acpWorkdir" to "ACP 作業ディレクトリ", "acpWorkdirTooltip" to "ACP の作業ディレクトリを設定",
"acpWorkdirSelect" to "ACP の作業ディレクトリとして使用するフォルダを選択", "acpWorkdirChange" to
"デフォルトの作業ディレクトリにリセット", and "acpWorkdirCurrent" to "現在の作業ディレクトリ: {path}" so the
locale file is consistent with other translations.
| "acpWorkdir": "ACP Workdir", | ||
| "acpWorkdirTooltip": "Set ACP working directory", | ||
| "acpWorkdirSelect": "Select a folder to use as the ACP workdir", | ||
| "acpWorkdirChange": "Reset to default workdir", | ||
| "acpWorkdirCurrent": "Current workdir: {path}" |
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.
Missing Portuguese translations for ACP workdir strings.
The ACP workdir strings on lines 22-24 are in English instead of Portuguese. This breaks the i18n experience for Portuguese-speaking users.
Apply this diff with proper Portuguese translations:
"acpWorkdir": "ACP Workdir",
- "acpWorkdirTooltip": "Set ACP working directory",
- "acpWorkdirSelect": "Select a folder to use as the ACP workdir",
- "acpWorkdirChange": "Reset to default workdir",
- "acpWorkdirCurrent": "Current workdir: {path}"
+ "acpWorkdirTooltip": "Definir diretório de trabalho ACP",
+ "acpWorkdirSelect": "Selecione uma pasta para usar como diretório de trabalho ACP",
+ "acpWorkdirChange": "Redefinir para o diretório de trabalho padrão",
+ "acpWorkdirCurrent": "Diretório de trabalho atual: {path}"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "acpWorkdir": "ACP Workdir", | |
| "acpWorkdirTooltip": "Set ACP working directory", | |
| "acpWorkdirSelect": "Select a folder to use as the ACP workdir", | |
| "acpWorkdirChange": "Reset to default workdir", | |
| "acpWorkdirCurrent": "Current workdir: {path}" | |
| "acpWorkdir": "ACP Workdir", | |
| "acpWorkdirTooltip": "Definir diretório de trabalho ACP", | |
| "acpWorkdirSelect": "Selecione uma pasta para usar como diretório de trabalho ACP", | |
| "acpWorkdirChange": "Redefinir para o diretório de trabalho padrão", | |
| "acpWorkdirCurrent": "Diretório de trabalho atual: {path}" |
🤖 Prompt for AI Agents
In src/renderer/src/i18n/pt-BR/chat.json around lines 20 to 24, the ACP workdir
entries are still in English; replace them with Portuguese equivalents: set
"acpWorkdir" to "Diretório de Trabalho ACP", "acpWorkdirTooltip" to "Definir
diretório de trabalho do ACP", "acpWorkdirSelect" to "Selecione uma pasta para
usar como diretório de trabalho do ACP", "acpWorkdirChange" to "Redefinir para o
diretório de trabalho padrão" and "acpWorkdirCurrent" to "Diretório de trabalho
atual: {path}" so the Portuguese localization is complete.
| const setAcpWorkdirPreference = (agentId: string, workdir: string | null) => { | ||
| if (!agentId) return | ||
| const currentMap = chatConfig.value.acpWorkdirMap ?? {} | ||
| const nextMap = { ...currentMap } | ||
| if (workdir && workdir.trim().length > 0) { | ||
| nextMap[agentId] = workdir | ||
| } else { | ||
| delete nextMap[agentId] | ||
| } | ||
| chatConfig.value = { ...chatConfig.value, acpWorkdirMap: nextMap } | ||
| } |
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
Clarify persistence expectations of setAcpWorkdirPreference
setAcpWorkdirPreference updates chatConfig.value.acpWorkdirMap in-memory only and doesn’t call saveChatConfig / updateChatConfig or any presenter. That’s fine if callers are responsible for persisting both to the backend (e.g., via a separate presenter API) and to conversation settings, but if the UI uses only this method, workdir choices might be lost on reload when loadChatConfig overwrites chatConfig from conversation.settings.
I’d recommend either:
- documenting that callers must also invoke
updateChatConfig({ acpWorkdirMap: … })/ persistence, or - optionally adding a small helper here that both updates the local map and persists it.
Also, minor naming nit: the parameter is called agentId, but this map is later read with modelId keys in createThread. If these are logically the same identifier for ACP agents, a unified name (agentId or modelId) across both places would reduce cognitive overhead.
🏁 Script executed:
#!/bin/bash
# Check where setAcpWorkdirPreference is used and whether persistence is handled alongside it.
rg -n "setAcpWorkdirPreference" -C3Length of output: 1659
---
🏁 Script executed:
```shell
#!/bin/bash
# Check if updateChatConfig is called after or within syncPreference context
rg -n "syncPreference|updateChatConfig" src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts -C5
Length of output: 1949
🏁 Script executed:
#!/bin/bash
# Search for createThread and how acpWorkdirMap is accessed with modelId
rg -n "createThread|acpWorkdirMap" src/renderer/src/stores/chat.ts -C3Length of output: 2030
🏁 Script executed:
#!/bin/bash
# Check if there's a naming inconsistency between agentId and modelId
rg -n "activeModel.*\.id|modelId.*acpWorkdir" src/renderer/src/components/chat-input/composables/useAcpWorkdir.ts -C2Length of output: 307
🏁 Script executed:
#!/bin/bash
# Look for all references to acpWorkdirMap to understand how it's used
rg -n "acpWorkdirMap" -C2Length of output: 5318
🏁 Script executed:
#!/bin/bash
# Search for any save/persist operations that might follow syncPreference calls
rg -n "\.setAcpWorkdirPreference" -A10Length of output: 1032
---
🏁 Script executed:
```shell
#!/bin/bash
# Check how NewThread.vue calls createThread and if settings with acpWorkdirMap get persisted
rg -n "createThread\(" src/renderer/src/components/NewThread.vue -B5 -A15
Length of output: 833
🏁 Script executed:
#!/bin/bash
# Search for how conversation.settings are saved/persisted to backend
rg -n "conversation\.settings|updateChatConfig|saveChatConfig" src/renderer/src/stores/chat.ts -C2Length of output: 999
🏁 Script executed:
#!/bin/bash
# Check threadPresenter to see if it handles acpWorkdirMap persistence
rg -n "acpWorkdirMap" src/main/presenter/threadPresenter/index.ts -B2 -A5Length of output: 386
Clarify persistence expectations and fix naming inconsistency in setAcpWorkdirPreference
setAcpWorkdirPreference updates chatConfig.value.acpWorkdirMap in-memory only. Persistence is indirect and implicit: the map is persisted to the backend only when createThread is called and passes the current acpWorkdirMap to the presenter. If the user changes the workdir preference but doesn't create a new thread before switching conversations or reloading, those changes are lost. This design is not transparent from reading the function alone.
Additionally, there's a naming inconsistency: setAcpWorkdirPreference uses the parameter name agentId, but createThread reads the map with modelId keys (chat.ts line 176), and useAcpWorkdir derives the identifier as agentId = activeModel.value?.id. These all refer to the same identifier; unifying the name (modelId preferred, as used in createThread and CONVERSATION_SETTINGS) would reduce confusion.
Recommended fixes:
- Rename
setAcpWorkdirPreferenceparameter fromagentIdtomodelIdfor consistency - Add a comment or JSDoc to
setAcpWorkdirPreferencedocumenting that it updates in-memory only and persists indirectly whencreateThreadis invoked - Optionally add an explicit
updateChatConfig({ acpWorkdirMap: nextMap })call if immediate backend persistence is desired
🤖 Prompt for AI Agents
In src/renderer/src/stores/chat.ts around lines 212 to 222, rename the function
parameter from agentId to modelId to match createThread and
CONVERSATION_SETTINGS, add a short JSDoc/comment above setAcpWorkdirPreference
stating that it only updates chatConfig.value.acpWorkdirMap in-memory and that
persistence to the backend happens indirectly when createThread sends the
current map (or call updateChatConfig explicitly if immediate persistence is
desired), and if immediate persistence is required add a call to
updateChatConfig({ acpWorkdirMap: nextMap }) after assigning chatConfig.value so
changes are saved to the backend.
| reasoningEffort?: 'minimal' | 'low' | 'medium' | 'high' | ||
| verbosity?: 'low' | 'medium' | 'high' | ||
| maxCompletionTokens?: number // GPT-5 series uses this parameter to replace maxTokens | ||
| conversationId?: string |
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.
🛠️ Refactor suggestion | 🟠 Major
Design concern: conversationId in ModelConfig.
Adding conversationId to ModelConfig mixes runtime context with model configuration. ModelConfig should describe model settings (temperature, tokens, etc.), not which conversation is active. Consider passing conversationId separately in method parameters instead.
🤖 Prompt for AI Agents
In src/shared/types/presenters/legacy.presenters.d.ts around line 151,
ModelConfig currently includes a conversationId property which mixes runtime
conversational context with static model configuration; remove conversationId
from ModelConfig and update call sites to accept conversationId as a separate
method/handler parameter (or in a separate Context/request object) so
ModelConfig only contains model settings (temperature, tokens, etc.), refactor
interfaces and function signatures that relied on ModelConfig.conversationId to
read the id from the new parameter and update type definitions and usages
accordingly.
#1100
Summary by CodeRabbit
New Features
Localization
Improvements
✏️ Tip: You can customize this high-level summary in your review settings.