Skip to content

Conversation

@zerob13
Copy link
Collaborator

@zerob13 zerob13 commented Nov 20, 2025

#1100

Summary by CodeRabbit

  • New Features

    • Full ACP (Agent Client Protocol) support: enable toggle, agent list, per-agent profiles, workspace (workdir) per conversation, permission requests, and ACP models in the UI.
    • New ACP settings panel and dialogs for adding/editing agent profiles; chat input now exposes ACP workdir controls and model-aware icons.
  • Localization

    • Added ACP and tool-call translations across all supported locales.
  • Improvements

    • Better provider/model validation, synchronization, and renderer-side agent/session status handling.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • resources/model-db/providers.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

Adds 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

Cohort / File(s) Summary
Dependencies
package.json
Bump version to 0.4.7 and add dependency @agentclientprotocol/sdk ^0.5.1
ACP config & provider registration
src/main/presenter/configPresenter/acpConfHelper.ts, src/main/presenter/configPresenter/providers.ts
New AcpConfHelper for ACP agent/profile storage and migration; ACP entry added to default providers
ConfigPresenter integration
src/main/presenter/configPresenter/index.ts, .../providerHelper.ts, .../providerModelHelper.ts
ConfigPresenter extended with ACP APIs (enable, CRUD for agents/profiles); provider store validation/self-healing and model-store logging/validation added
LLM/provider wiring
src/main/presenter/index.ts, src/main/presenter/llmProviderPresenter/index.ts, .../providerInstanceManager.ts, .../baseProvider.ts
LLMProviderPresenter now accepts sqlite presenter; AcpSessionPersistence/Provider wiring; ProviderInstanceManager supports ACP instances and isAgentProvider check; base provider lifecycle for agent providers
ACP core: provider, managers, mapping, formatter
src/main/presenter/llmProviderPresenter/providers/acpProvider.ts, .../agent/acpProcessManager.ts, .../agent/acpSessionManager.ts, .../agent/acpSessionPersistence.ts, .../agent/acpContentMapper.ts, .../agent/acpMessageFormatter.ts, .../agent/types.ts, .../baseAgentProvider.ts
New AcpProvider, process/session managers, session persistence, content mapper, message formatter, and shared agent abstractions implementing streaming, permission flow, session/process lifecycle, and IPC
SQLite additions
src/main/presenter/sqlitePresenter/index.ts, .../tables/acpSessions.ts
New acp_sessions table/table wrapper and presenter methods (get/upsert/update/delete) for ACP sessions
Thread & permission handlers
src/main/presenter/threadPresenter/*
Add acpWorkdirMap default; thread presenter methods for ACP workdirs; permission and stream handlers extended to propagate conversationId and handle ACP permission flows; tool-call payload extended
Renderer: settings UI & routing
src/renderer/settings/components/AcpSettings.vue, AcpProfileDialog.vue, AcpProfileManagerDialog.vue, src/renderer/settings/main.ts
New ACP settings UI components, route /acp added, route positions adjusted
Renderer: model/UI components & workdir UX
src/renderer/src/components/*, chat-input/*, useAcpWorkdir.ts
ModelIcon mappings for ACP models; conditional icon rendering when provider is acp; ChatInput receives modelInfo and integrates useAcpWorkdir composable; New ACP workdir selection/clear UX
Renderer stores
src/renderer/src/stores/agentModelStore.ts, chat.ts, modelStore.ts, mcp.ts, providerStore.ts
New agentModelStore; chat store persists acpWorkdirMap and exposes setter; modelStore routes agent providers to agentModelStore and adds cache logic; mcp/provider stores hardened and debounced
I18n (many locales)
src/renderer/src/i18n/*/{chat,routes,settings}.json
Added ACP-related translations: acpWorkdir/toolCall sections and full settings entries plus route labels across locales
Shared types & presenter d.ts
src/shared/types/core/*, src/shared/types/presenters/*, src/shared/types/index.d.ts
New stream permission event and payload types; ACP domain types (AcpAgentProfile, AcpBuiltinAgent, AcpCustomAgent, AcpSessionEntity, AcpWorkdirInfo); presenter interfaces extended (Config/Thread/LLM/SQLite) with ACP methods and conversationId parameter on startStreamCompletion
Renderer tests & unit tests
test/**/*
Tests updated for LLMProviderPresenter constructor; new tests covering AcpContentMapper and permission handler flows

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas needing extra attention:

  • AcpProvider core streaming, permission lifecycle, and event mapping
  • AcpProcessManager / AcpSessionManager (child process IPC, concurrency)
  • SQLite acp_sessions table SQL and upsert semantics
  • ProviderInstanceManager ACP instantiation path and backward compatibility
  • Renderer store integrations (modelStore, agentModelStore) and cache invalidation
  • Presenter interface/type consistency across main/renderer/shared boundaries

Possibly related issues

Possibly related PRs

  • #1064 — related changes to ThreadPresenter permission/workflow logic that this PR extends
  • #1097 — overlapping llmProviderPresenter and manager refactors used by ACP integration
  • #1051 — overlaps in provider/model refresh and merge logic in renderer stores

Suggested labels

codex

Suggested reviewers

  • deepinfect

Poem

🐰
I hopped through code, a twitchy guide,
Brought agents, sessions, and routes beside.
Workdirs set, permissions asked—no fear,
ACP arrived with a cheerful cheer.
Hop on, let agents dance far and wide!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Agent Client Protocol (ACP)' clearly and concisely describes the main change: adding ACP support to the codebase. It directly relates to the substantial new functionality introduced across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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: fetchModels try/catch doesn't catch async rejections; rewrite with await

The 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 await inside 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 true

In 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: unknow should be unknown.

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 of flex-wrap, and addition of overflow-hidden alter the container's layout behavior. The switch to inline-flex means the container will only be as wide as its content (up to max-width) rather than filling available space, and removing flex-wrap prevents 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 adding alt for ACP ModelIcon for consistency/accessibility

The ACP-specific branch correctly switches to currentMessage.model_id, but it drops the alt prop 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 redundant

The added logging and providerId fix-up in getModelList help harden against malformed model metadata and provide good diagnostics. Note that BaseLLMProvider.fetchModels() now also normalizes providerId, 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 normalizeContent method handles various content formats (string, array with text/image parts) but doesn't validate or handle malformed input gracefully. If content has 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 content is the expected type before processing

This 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 refreshAgentModels method uses hardcoded default values for all ACP agent models:

  • contextLength: 8192
  • maxTokens: 4096
  • vision: false, functionCall: true, reasoning: false, enableSearch: false

Consider:

  1. Documentation: Add a comment explaining why these specific defaults are appropriate for all ACP agents
  2. Uniqueness validation: No check that agent.id values are unique. Duplicate IDs could cause conflicts in model selection
  3. Error handling: No try-catch for getAcpAgents() - failures will bubble up unhandled

Example 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 symmetry

The abstractions look clean, but there are two design points worth double‑checking:

  1. AgentSessionManager.getSession(conversationId: string) only keys by conversationId, 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 getSession take the agent descriptor as well, or
    • Clearly documenting that an AgentSessionManager is scoped to a single agent so conversationId is sufficient.
  2. In AgentProcessManager, getConnection(agent: TDescriptor) accepts a generic descriptor, but getProcess/release take agentId: string. If descriptors can be richer than a string, you may want these methods to also accept TDescriptor for symmetry, or alternatively rename TDescriptor to TAgentId and 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 tweak

The 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-id prop 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 labels

The new ACP workdir strings and toolCall block mirror the en-US structure and placeholders, so they should integrate cleanly. If full localization is a goal for ko-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 inside activeProvider computed; consider using visibleProviders for default selection

Filtering visibleProviders to exclude the acp provider and deriving enabled/disabled lists from it makes sense. The acp special-case in activeProvider, 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 acp redirect into setActiveProvider (or a watch on route.params.providerId) so computed logic stays side-effect free.
  • In onMounted, pick the initial provider from visibleProviders instead of providerStore.sortedProviders to avoid briefly navigating to settings-provider with acp and then immediately redirecting to settings-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 sanitizing storeName

The per-provider store isolation (name: models_${providerId}, cwd: userDataPath/provider_models) and providerId normalization in both getProviderModels and setProviderModels should make the model cache much more robust against mismatched or missing providerId values.

Two minor follow-ups you might consider:

  • If there is any chance that providerId can contain path- or filename-unfriendly characters (/, \, :, etc.), normalize or hash it when building storeName to avoid filesystem issues.
  • The post-validation incorrectProviderIds checks will essentially always be empty because you correct model.providerId earlier 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 tested

These tests exercise the critical AcpContentMapper behaviors: 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 failed status 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, but listChanged may miss some repair cases

The getProviders flow does a nice job of:

  • Skipping MODEL_META-like entries (with providerId but no apiType).
  • 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: listChanged only checks for a length change or remaining entries without apiType. If you repair entries by merging them with templates (i.e., fill in missing fields but preserve length and add apiType), listChanged stays false and the healed data is never written back, so the underlying store remains in its pre-repair shape. Callers still get repairedProviders, 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

setProviders now filters out invalid entries (missing id or apiType) before persisting, which is good. However, several code paths still write provider lists directly via setSetting:

  • updateProvidersBatch uses batchUpdate.providers as-is.
  • updateProviderAtomic, addProviderAtomic, and reorderProvidersAtomic also call setSetting directly.

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., reuse setProviders from 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 deleteByConversation fails 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 runTransaction expects 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 definitions

The new AgentSessionState / AgentProcessHandle shapes are sensible and cover expected metadata. However, the same symbols also exist in src/main/presenter/llmProviderPresenter/agent/types.ts, which risks the two diverging over time.

Consider centralizing these type definitions (e.g., only in @shared/presenter and 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 strings

The 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 how toolCall.* was localized).

Also applies to: 73-82

test/main/presenter/llmProviderPresenter.test.ts (1)

4-9: SQLite presenter mock correctly matches new constructor requirements

The tests have been updated to:

  • Import ISQLitePresenter and build a mockSqlitePresenter that includes the new ACP session/workdir methods.
  • Pass this mock into LLMProviderPresenter wherever 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 ISQLitePresenter keeps 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 solid

The ACP workdir button/tooltip wiring matches the composable API: isAcpModel and hasWorkdir correctly gate visibility/state, and conversationId plus activeModelSource give the composable enough context for both chat and new-thread flows. The pending-workdir path should behave correctly with modelInfo-driven new-thread creation and the chat store’s acpWorkdirMap.

Minor nit: in the template you can drop the .value access on acpWorkdir.* 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 missing id

The 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' but id is unset, model-id will be undefined. ModelIcon will likely degrade gracefully, but you may want to either:

  • Also check for config.activeModel.value.id in the v-if, or
  • Provide a sensible fallback (e.g. a generic ACP icon) when id is missing.

This would make the UI a bit more robust to incomplete activeModel state.

src/renderer/src/stores/providerStore.ts (1)

227-248: optimizeProviderOrder currently ignores the enable flag

In optimizeProviderOrder, the computed newOrder is the same regardless of the enable boolean:

const newOrder = enable
  ? [...enabledOrder, providerId, ...disabledOrder]
  : [...enabledOrder, providerId, ...disabledOrder]

So the enable argument 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 enable from the signature; otherwise, adjust the false branch 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 usage

The composable cleanly encapsulates ACP workdir behavior:

  • isAcpModel gating ensures all logic is scoped to ACP agents with a valid id.
  • loadWorkdir only calls threadPresenter.getAcpWorkdir when both conversationId and agentId are present, and synchronizes the resulting path back into chatStore.setAcpWorkdirPreference, which lines up with how createThread and loadChatConfig use acpWorkdirMap (per the provided chat store snippets).
  • The pendingWorkdir path for “chosen before conversation exists” is handled by selectWorkdir + syncPendingWhenReady, so the workdir is applied once a conversation id appears, and the tooltip/button state stays in sync.
  • clearWorkdir correctly 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 of selectWorkdir, 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 conservative

Cloning settings into normalizedSettings and only attaching acpWorkdirMap when:

  • no map is present/empty,
  • providerId === 'acp',
  • and modelId is 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 acpWorkdirMap instead of just the selected modelId entry, 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 handling

The switch (profileDialogState.mode) correctly handles builtin-add, builtin-edit, custom-add, and custom-edit, followed by a single refreshAfterMutation and success toast, with savingProfile guarded via finally.

You might optionally add a default branch 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 removed result 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.confirm for a more consistent look, but functionally this is fine.


94-105: Consider localizing CMD, ARGS, and ENV labels

Minor 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 through t(...), 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 enhancement

The props and emits typing around AcpBuiltinAgent, AcpAgentProfile, and AcpBuiltinAgentId are precise and make the component easy to consume from TypeScript.

formatEnv safely handles empty env maps and falls back to settings.acp.none, returning a compact KEY=VALUE list.

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 paths

Normalizing model.type with a ModelType.Chat fallback in normalizeRendererModel, applyUserDefinedModelConfig, normalizeStoredModel, and the getModelList mapping makes type handling more robust when upstream metadata omits type. 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

updateEnabledState correctly maintains enabledModels per provider and reassigns the ref to trigger reactivity. The added console.log is useful during development but may be noisy in production if refreshProviderModels is 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 updateWorkdir and the clearSession semantics (resetting sessionId/status but preserving workdir) are coherent and should behave well with the ACP provider.

Two small optional refinements:

  • The literal 'idle' is used as an AgentSessionLifecycleStatus value in both updateWorkdir and clearSession. 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 to process.env.HOME || process.cwd() is fine for tests and most environments, but process.cwd() can be an unexpected location (e.g., install dir) if HOME is 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/emitAsText give 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.stringify output for large resource payloads 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.

handleToolCallUpdate plus getOrCreateToolCallState/emitToolCallStartIfNeeded/emitToolCallChunk/emitToolCallEnd provide 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 emitToolCallEnd by deleting from toolCallStates.

One possible edge case: if a tool call never emits a completed/failed status (e.g., session aborted mid‑call), its entry will remain in toolCallStates until 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.parse when 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 with migrateLegacyAgents.

replaceWithLegacyAgents:

  • Sanitizes incoming legacy agents via normalizeLegacyAgent.
  • Rebuilds builtins/customs from scratch, using BUILTIN_ORDER and default templates.
  • Persists normalized arrays and bumps the store version, returning sanitized configs.

This mirrors what migrateLegacyAgents does for the old agents key. 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 acp provider instance through the global presenter.
  • Casts to AcpProvider and calls refreshAgents(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=true but no matching permission option exists, the code defaults to cancelled with 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) and AcpCustomAgent (lines 693-700) have nearly identical structure, with AcpCustomAgent adding only an enabled field. This duplication may cause confusion. Consider making AcpAgentConfig a base type or merging them with optional enabled field.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1d526df and c8ec015.

📒 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)

Comment on lines +38 to +46
constructor(options: AcpSessionManagerOptions) {
this.providerId = options.providerId
this.processManager = options.processManager
this.sessionPersistence = options.sessionPersistence

app.on('before-quit', () => {
void this.clearAllSessions()
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +28 to +46
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
)
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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 3

Length of output: 1198


🏁 Script executed:

#!/bin/bash
# Check the baseAgentProvider file for imports and class definition
head -50 src/main/presenter/llmProviderPresenter/baseAgentProvider.ts

Length 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 -100

Length 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.ts

Length 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 -40

Length 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.

Comment on lines +64 to +79
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()
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +362 to +380
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)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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() || null

Or 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.

Suggested change
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.

Comment on lines +86 to +130
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
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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_at

Because 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 status to '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 status as required in AcpSessionUpsertData so 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_at

paired 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 -100

Length of output: 518


🏁 Script executed:

#!/bin/bash
# Look for the ACP sessions table class/file to understand the structure
fd "acpSessions" --type=ts

Length 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 5

Length 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.ts

Length 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 status required in AcpSessionUpsertData if it must always be provided, or
  • Conditionally update status only when present in data (suggested SQL fix in the review is sound; pair with status: null in 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.

Comment on lines +19 to +24
"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}"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
"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.

Comment on lines +20 to +24
"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}"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
"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.

Comment on lines +212 to +222
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 }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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" -C3

Length 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 -C3

Length 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 -C2

Length of output: 307


🏁 Script executed:

#!/bin/bash
# Look for all references to acpWorkdirMap to understand how it's used
rg -n "acpWorkdirMap" -C2

Length of output: 5318


🏁 Script executed:

#!/bin/bash
# Search for any save/persist operations that might follow syncPreference calls
rg -n "\.setAcpWorkdirPreference" -A10

Length 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 -C2

Length 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 -A5

Length 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 setAcpWorkdirPreference parameter from agentId to modelId for consistency
  • Add a comment or JSDoc to setAcpWorkdirPreference documenting that it updates in-memory only and persists indirectly when createThread is 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
Copy link
Contributor

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.

@zerob13 zerob13 merged commit 7015791 into dev Nov 20, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants