From f0fb8f592b88ad84ce9b7a5b907ff2c8987835d6 Mon Sep 17 00:00:00 2001 From: zerob13 Date: Fri, 20 Mar 2026 01:22:38 +0800 Subject: [PATCH 1/5] feat(acp): add session config options --- docs/specs/acp-session-config-options/plan.md | 121 ++++++ docs/specs/acp-session-config-options/spec.md | 93 +++++ .../specs/acp-session-config-options/tasks.md | 52 +++ package.json | 6 +- src/main/events.ts | 3 +- .../agentPresenter/acp/acpCapabilities.ts | 2 +- .../agentPresenter/acp/acpConfigState.ts | 204 ++++++++++ .../agentPresenter/acp/acpContentMapper.ts | 25 +- .../agentPresenter/acp/acpFsHandler.ts | 2 +- .../agentPresenter/acp/acpMessageFormatter.ts | 2 +- .../agentPresenter/acp/acpProcessManager.ts | 153 +++++-- .../agentPresenter/acp/acpSessionManager.ts | 88 +++- .../agentPresenter/acp/acpTerminalManager.ts | 6 +- .../presenter/agentPresenter/acp/index.ts | 11 + .../agentPresenter/acp/mcpConfigConverter.ts | 2 +- .../agentPresenter/acp/mcpTransportFilter.ts | 2 +- .../presenter/llmProviderPresenter/index.ts | 32 ++ .../providers/acpProvider.ts | 228 ++++++++++- src/main/presenter/newAgentPresenter/index.ts | 51 ++- .../src/components/chat/ChatStatusBar.vue | 376 +++++++++++++++++- src/renderer/src/events.ts | 3 +- src/renderer/src/pages/NewThreadPage.vue | 2 +- src/shared/types/presenters/index.d.ts | 3 + .../types/presenters/legacy.presenters.d.ts | 8 + .../presenters/llmprovider.presenter.d.ts | 30 ++ .../types/presenters/new-agent.presenter.d.ts | 7 + test/main/presenter/acpMcpPassthrough.test.ts | 2 +- test/main/presenter/acpProvider.test.ts | 151 +++++++ .../acpContentMapper.test.ts | 131 ++++-- .../newAgentPresenter.test.ts | 118 +++++- .../renderer/components/ChatStatusBar.test.ts | 205 +++++++++- 31 files changed, 1987 insertions(+), 132 deletions(-) create mode 100644 docs/specs/acp-session-config-options/plan.md create mode 100644 docs/specs/acp-session-config-options/spec.md create mode 100644 docs/specs/acp-session-config-options/tasks.md create mode 100644 src/main/presenter/agentPresenter/acp/acpConfigState.ts diff --git a/docs/specs/acp-session-config-options/plan.md b/docs/specs/acp-session-config-options/plan.md new file mode 100644 index 000000000..2fb4d1416 --- /dev/null +++ b/docs/specs/acp-session-config-options/plan.md @@ -0,0 +1,121 @@ +# ACP Session Config Options 实施计划 + +## 1. 当前基线 + +1. ACP 状态栏当前只把 provider/model 锁定为外层 ACP agent。 +2. ACP 内部 model/mode 能力部分来自旧 `models/modes`,没有统一配置状态。 +3. 新版 SDK `0.16.1` 已支持 `configOptions`、`config_option_update`、`session_info_update`、`usage_update`。 + +## 2. 设计决策 + +### 2.1 统一配置状态 + +新增共享类型: + +1. `AcpConfigOptionValue` +2. `AcpConfigOption` +3. `AcpConfigState` + +主进程通过 `normalizeAcpConfigState()` 统一把两类来源归一为同一结构: + +1. `configOptions` 直接映射,标记 `source=configOptions` +2. legacy `models/modes` 合成为伪 config option,标记 `source=legacy` + +### 2.2 Warmup / Session 双缓存 + +1. `AcpProcessHandle` 缓存 process 级 warmup config state +2. `AcpSessionRecord` 缓存 session 级 config state +3. `prepareAcpSession` 在 draft 建立后立即发出 config-ready 事件,让 renderer 无缝从 process cache 切到 session cache + +### 2.3 事件策略 + +新增事件: + +1. `ACP_WORKSPACE_EVENTS.SESSION_CONFIG_OPTIONS_READY` + +触发时机: + +1. process warmup 完成 +2. `prepareAcpSession` / `coreStream` 绑定 session 后 +3. 收到 `config_option_update` +4. `setSessionConfigOption` / legacy mode/model 写入成功后 + +### 2.4 Renderer 展示策略 + +状态栏维持双轨: + +1. 非 ACP:继续显示普通模型列表和 generation settings +2. ACP:显示 ACP options 面板,不再清空设置区 + +排序规则: + +1. `model` +2. `thought_level` +3. 其余按 agent 原顺序 + +读写规则: + +1. 有 `draftSessionId` 或活动 ACP session 时,走 session 级读写 +2. 仅有 process warmup 数据时,面板只读 + +## 3. 分阶段实施 + +### Phase 1:SDK 与主进程兼容 + +1. 升级 SDK 到 `0.16.1` +2. 修正 schema import 路径 +3. 兼容 `unstable_setSessionModel` / `KillTerminalRequest` 等 SDK 差异 + +### Phase 2:ACP 配置状态归一 + +1. 新增 `acpConfigState.ts` +2. `AcpProcessManager` warmup 归一 `configOptions/models/modes` +3. `AcpSessionManager` 建 session 时继承并缓存统一 state +4. `AcpContentMapper` 支持 `config_option_update` + +### Phase 3:Presenter 与事件 + +1. `AcpProvider` 增加 process/session config 读写接口 +2. `LLMProviderPresenter` 和 `NewAgentPresenter` 暴露代理方法 +3. 发出 `SESSION_CONFIG_OPTIONS_READY` + +### Phase 4:Renderer 状态栏 + +1. `NewThreadPage` 透传 ACP draft sessionId +2. `ChatStatusBar` 加入 ACP config 同步、只读控制和更新逻辑 +3. trigger 显示 `ACP agent / internal model` + +## 4. 测试策略 + +### 4.1 Main + +1. `AcpContentMapper` 覆盖 `config_option_update` +2. `AcpProvider.prepareSession` 发出 config-ready 事件 +3. `AcpProvider.setSessionConfigOption` 使用 agent 返回的全量 state 回写缓存 +4. `NewAgentPresenter` 覆盖 ACP session config 读写代理 + +### 4.2 Renderer + +1. ACP draft 首屏读取 process warmup config +2. draft sessionId 就绪后切换到 session config +3. 写入 ACP select/boolean option 时调用 session presenter +4. 非 ACP 原有状态栏行为无回归 + +## 5. 风险与缓解 + +1. 风险:不同 ACP agent 同时返回 `configOptions` 与 legacy 字段。 + 缓解:统一以 `configOptions` 为准,legacy 仅在缺失时启用。 + +2. 风险:renderer 在 draft 早期拿不到 sessionId,导致控件误可写。 + 缓解:基于 `activeAcpSessionId` 做只读门控。 + +3. 风险:新 SDK 增加的 session 通知被误判为未处理异常。 + 缓解:`session_info_update` 与 `usage_update` 静默兼容。 + +## 6. 质量门槛 + +1. `pnpm run typecheck` +2. `pnpm run format` +3. `pnpm run i18n` +4. `pnpm run lint` +5. 关键 main/renderer 测试通过 diff --git a/docs/specs/acp-session-config-options/spec.md b/docs/specs/acp-session-config-options/spec.md new file mode 100644 index 000000000..20478b9ff --- /dev/null +++ b/docs/specs/acp-session-config-options/spec.md @@ -0,0 +1,93 @@ +# ACP Session Config Options 规格 + +## 概述 + +为 ACP 接入协议级 `session config options`,让 ACP agent 的内部 `model`、`thought_level`、`mode`、布尔开关等能力在 DeepChat 状态栏中直接展示和修改。 + +本次改动同时完成: + +1. 升级 `@agentclientprotocol/sdk` 到 `0.16.1` +2. ACP 优先走新版 `configOptions` +3. 旧 `models/modes` 仅作为兼容兜底 +4. 预热阶段拿到的配置立即可展示,不等待首条消息 + +## 背景与动机 + +1. 当前 ACP 外层模型选择器只表示“ACP agent”,内部 session model 被固定,用户无法直观看到真实内部模型。 +2. 协议已提供 `session config options`,可以统一承载 `model`、`thought_level`、`mode` 以及其他 agent 自定义配置。 +3. ACP warmup 阶段已经能拿到这些信息,继续延迟到首条消息后再展示会造成首屏空窗。 + +## 用户故事 + +### US-1:看到 ACP 内部真实模型 + +作为用户,我希望状态栏显示 `ACP agent / 内部 model`,这样我能确认当前 ACP session 真正在用哪个模型。 + +### US-2:在新线程阶段就能看配置 + +作为用户,我希望 ACP draft 刚建立时就能看到 agent 的可配置项,而不是要先发一条消息。 + +### US-3:统一修改 ACP session 配置 + +作为用户,我希望在同一个状态栏面板里修改 ACP 的 `model`、`thought_level`、`mode` 和布尔类开关。 + +## 功能需求 + +### A. 配置来源与兼容策略 + +- [ ] ACP 主路径使用协议 `configOptions` +- [ ] 当 agent 未返回 `configOptions` 时,回退到 legacy `models/modes` +- [ ] 若同时返回两套字段,只采用 `configOptions` + +### B. 外层 agent 与内层 session model 分离 + +- [ ] 状态栏外层仍显示 ACP agent +- [ ] ACP 内部 `model` 不写回现有通用 `SessionGenerationSettings` +- [ ] ACP agent 不允许通过普通 `setSessionModel` 切走 provider/model + +### C. Warmup 与缓存 + +- [ ] warmup 的 `newSession/loadSession` 结果要归一为统一 `AcpConfigState` +- [ ] 预热缓存应绑定到 process handle +- [ ] `prepareAcpSession` 建立 draft/session 后立即把缓存灌入 session,并发出 ready 事件 + +### D. Renderer 状态栏 + +- [ ] 非 ACP 路径保持现有模型选择和 generation settings 行为 +- [ ] ACP 路径改为展示 ACP options 面板 +- [ ] `category=model` 作为首要选项,并参与 trigger 文案展示 +- [ ] `category=thought_level` 排在 `model` 后,保留 agent 返回的 label/value +- [ ] `category=mode` 与其他 generic 选项保持 agent 顺序 +- [ ] draft 无 sessionId 时展示 warmup 数据但控件只读 +- [ ] draft sessionId 就绪后切到 session 级读写 + +### E. 事件与接口 + +- [ ] 新增 `AcpConfigOption`、`AcpConfigOptionValue`、`AcpConfigState` +- [ ] `ILlmProviderPresenter` 增加 ACP process/session config 读写接口 +- [ ] `INewAgentPresenter` 增加 ACP session config 读写接口 +- [ ] 新增 renderer 事件 `ACP_WORKSPACE_EVENTS.SESSION_CONFIG_OPTIONS_READY` + +## 验收标准 + +- [ ] ACP trigger 可显示 `agentId / internal model` +- [ ] 新线程 ACP draft 能先显示 warmup config +- [ ] draft sessionId 就绪后可切到 session config 并允许写入 +- [ ] `config_option_update` 会整组替换当前 config state +- [ ] 旧 ACP agent 只有 `models/modes` 时仍可正常展示和切换 + +## 非目标 + +1. 不把 ACP `model/thought_level/mode` 混入通用 provider generation settings。 +2. 不重做非 ACP 状态栏 UI。 +3. 不在本次把 `systemPrompt/temperature/contextLength/maxTokens` 合并进 ACP options。 + +## 约束 + +1. 继续遵循现有 Presenter + EventBus 架构。 +2. 所有兼容逻辑集中在 ACP 主进程归一层,不在 renderer 分散判断 legacy 字段。 +3. UI 不新增独立 ACP 设置页,先复用状态栏入口。 + +## 开放问题 + +无。 diff --git a/docs/specs/acp-session-config-options/tasks.md b/docs/specs/acp-session-config-options/tasks.md new file mode 100644 index 000000000..95eb0e862 --- /dev/null +++ b/docs/specs/acp-session-config-options/tasks.md @@ -0,0 +1,52 @@ +# ACP Session Config Options 任务清单 + +## T0 规格文档 + +- [x] 新建 `spec.md` +- [x] 新建 `plan.md` +- [x] 新建 `tasks.md` + +## T1 SDK 与共享类型 + +- [x] 升级 `@agentclientprotocol/sdk` 到 `0.16.1` +- [x] 修正 ACP schema import 路径 +- [x] 新增 `AcpConfigOptionValue` +- [x] 新增 `AcpConfigOption` +- [x] 新增 `AcpConfigState` + +## T2 ACP 主进程归一层 + +- [x] 新增 `acpConfigState.ts` +- [x] warmup 解析 `configOptions` +- [x] legacy `models/modes` 回退归一 +- [x] `config_option_update` 整组替换 session config state +- [x] `session_info_update` / `usage_update` 静默兼容 + +## T3 Presenter / EventBus + +- [x] `AcpProcessHandle` / `AcpSessionRecord` 缓存统一 config state +- [x] 新增 `SESSION_CONFIG_OPTIONS_READY` 事件 +- [x] `ILlmProviderPresenter` 增加 ACP process/session config 读写接口 +- [x] `INewAgentPresenter` 增加 ACP session config 读写接口 + +## T4 Renderer 状态栏 + +- [x] `NewThreadPage` 透传 `acpDraftSessionId` +- [x] `ChatStatusBar` 增加 ACP config 拉取与事件订阅 +- [x] ACP trigger 显示 `agent / internal model` +- [x] draft 无 sessionId 时显示 warmup config 且只读 +- [x] draft sessionId 就绪后切换到 session 级读写 + +## T5 测试 + +- [x] 更新 `acpContentMapper.test.ts` +- [x] 更新 `acpProvider.test.ts` +- [x] 更新 `newAgentPresenter.test.ts` +- [x] 更新 `ChatStatusBar.test.ts` + +## T6 质量门禁 + +- [x] `pnpm run format` +- [x] `pnpm run i18n` +- [x] `pnpm run lint` +- [x] 运行关键测试并记录结果 diff --git a/package.json b/package.json index b28cc46b8..03e35b381 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "update-shadcn": "node scripts/update-shadcn.js" }, "dependencies": { - "@agentclientprotocol/sdk": "^0.5.1", + "@agentclientprotocol/sdk": "^0.16.1", "@anthropic-ai/sdk": "^0.53.0", "@antv/infographic": "^0.2.7", "@aws-sdk/client-bedrock": "^3.958.0", @@ -116,8 +116,6 @@ "@lingual/i18n-check": "0.8.12", "@mcp-ui/client": "^5.13.3", "@pinia/colada": "^0.20.0", - "@unovis/ts": "1.6.4", - "@unovis/vue": "1.6.4", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.18", "@tiptap/core": "^2.11.7", @@ -137,6 +135,8 @@ "@types/node": "^22.19.3", "@types/xlsx": "^0.0.35", "@typescript/native-preview": "7.0.0-dev.20260115.1", + "@unovis/ts": "1.6.4", + "@unovis/vue": "1.6.4", "@vee-validate/zod": "^4.15.1", "@vitejs/plugin-vue": "^6.0.3", "@vitest/ui": "^3.2.4", diff --git a/src/main/events.ts b/src/main/events.ts index 435e7eb98..fdf231ba1 100644 --- a/src/main/events.ts +++ b/src/main/events.ts @@ -274,7 +274,8 @@ export const WORKSPACE_EVENTS = { // ACP-specific workspace events export const ACP_WORKSPACE_EVENTS = { SESSION_MODES_READY: 'acp-workspace:session-modes-ready', // Session modes available - SESSION_COMMANDS_READY: 'acp-workspace:session-commands-ready' // Session commands available + SESSION_COMMANDS_READY: 'acp-workspace:session-commands-ready', // Session commands available + SESSION_CONFIG_OPTIONS_READY: 'acp-workspace:session-config-options-ready' // Session config options available } export const ACP_DEBUG_EVENTS = { diff --git a/src/main/presenter/agentPresenter/acp/acpCapabilities.ts b/src/main/presenter/agentPresenter/acp/acpCapabilities.ts index ec7b64c62..0aad138c2 100644 --- a/src/main/presenter/agentPresenter/acp/acpCapabilities.ts +++ b/src/main/presenter/agentPresenter/acp/acpCapabilities.ts @@ -1,4 +1,4 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' export interface AcpCapabilityOptions { enableFs?: boolean diff --git a/src/main/presenter/agentPresenter/acp/acpConfigState.ts b/src/main/presenter/agentPresenter/acp/acpConfigState.ts new file mode 100644 index 000000000..54d3bca5d --- /dev/null +++ b/src/main/presenter/agentPresenter/acp/acpConfigState.ts @@ -0,0 +1,204 @@ +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' +import type { AcpConfigOption, AcpConfigOptionValue, AcpConfigState } from '@shared/presenter' + +export const LEGACY_MODEL_CONFIG_ID = '__acp_legacy_model__' +export const LEGACY_MODE_CONFIG_ID = '__acp_legacy_mode__' + +type NormalizableConfigStateInput = { + configOptions?: schema.SessionConfigOption[] | null + models?: schema.SessionModelState | null + modes?: schema.SessionModeState | null +} + +const normalizeSelectOptions = ( + options: schema.SessionConfigSelectOptions +): AcpConfigOptionValue[] => { + return options.flatMap((entry) => { + if ('group' in entry) { + return entry.options.map((option) => ({ + value: option.value, + label: option.name, + description: option.description ?? null, + groupId: entry.group, + groupLabel: entry.name + })) + } + + return { + value: entry.value, + label: entry.name, + description: entry.description ?? null, + groupId: null, + groupLabel: null + } + }) +} + +const normalizeConfigOption = (option: schema.SessionConfigOption): AcpConfigOption => { + if (option.type === 'boolean') { + return { + id: option.id, + label: option.name, + description: option.description ?? null, + type: 'boolean', + category: option.category ?? null, + currentValue: option.currentValue + } + } + + return { + id: option.id, + label: option.name, + description: option.description ?? null, + type: 'select', + category: option.category ?? null, + currentValue: option.currentValue, + options: normalizeSelectOptions(option.options) + } +} + +const buildLegacyModelOption = ( + models?: schema.SessionModelState | null +): AcpConfigOption | undefined => { + if (!models?.availableModels?.length) { + return undefined + } + + return { + id: LEGACY_MODEL_CONFIG_ID, + label: 'Model', + description: null, + type: 'select', + category: 'model', + currentValue: models.currentModelId, + options: models.availableModels.map((model) => ({ + value: model.modelId, + label: model.name, + description: model.description ?? null, + groupId: null, + groupLabel: null + })) + } +} + +const buildLegacyModeOption = ( + modes?: schema.SessionModeState | null +): AcpConfigOption | undefined => { + if (!modes?.availableModes?.length) { + return undefined + } + + return { + id: LEGACY_MODE_CONFIG_ID, + label: 'Mode', + description: null, + type: 'select', + category: 'mode', + currentValue: modes.currentModeId, + options: modes.availableModes.map((mode) => ({ + value: mode.id, + label: mode.name, + description: mode.description ?? null, + groupId: null, + groupLabel: null + })) + } +} + +export const createEmptyAcpConfigState = ( + source: AcpConfigState['source'] = 'legacy' +): AcpConfigState => ({ + source, + options: [] +}) + +export const normalizeAcpConfigState = (input: NormalizableConfigStateInput): AcpConfigState => { + if (input.configOptions !== undefined && input.configOptions !== null) { + return { + source: 'configOptions', + options: input.configOptions.map(normalizeConfigOption) + } + } + + const options = [buildLegacyModelOption(input.models), buildLegacyModeOption(input.modes)].filter( + (option): option is AcpConfigOption => Boolean(option) + ) + + return { + source: 'legacy', + options + } +} + +export const getAcpConfigOption = ( + state: AcpConfigState | null | undefined, + configId: string +): AcpConfigOption | undefined => state?.options.find((option) => option.id === configId) + +export const getAcpConfigOptionByCategory = ( + state: AcpConfigState | null | undefined, + category: string +): AcpConfigOption | undefined => state?.options.find((option) => option.category === category) + +export const getAcpConfigOptionLabel = ( + option: AcpConfigOption | null | undefined +): string | null => { + if (!option) { + return null + } + + if (option.type !== 'select') { + return option.currentValue ? 'On' : 'Off' + } + + const currentValue = String(option.currentValue) + return option.options?.find((entry) => entry.value === currentValue)?.label ?? currentValue +} + +export const getLegacyModeState = ( + state: AcpConfigState | null | undefined +): + | { + availableModes: Array<{ id: string; name: string; description: string }> + currentModeId?: string + } + | undefined => { + const option = state?.options.find( + (entry) => entry.id === LEGACY_MODE_CONFIG_ID || entry.category === 'mode' + ) + if (!option || option.type !== 'select') { + return undefined + } + + return { + availableModes: + option.options?.map((entry) => ({ + id: entry.value, + name: entry.label, + description: entry.description ?? '' + })) ?? [], + currentModeId: typeof option.currentValue === 'string' ? option.currentValue : undefined + } +} + +export const updateAcpConfigStateValue = ( + state: AcpConfigState | null | undefined, + configId: string, + value: string | boolean +): AcpConfigState | null => { + if (!state) { + return null + } + + return { + ...state, + options: state.options.map((option) => + option.id === configId + ? { + ...option, + currentValue: value + } + : option + ) + } +} diff --git a/src/main/presenter/agentPresenter/acp/acpContentMapper.ts b/src/main/presenter/agentPresenter/acp/acpContentMapper.ts index 376370b5c..6b9abb4ce 100644 --- a/src/main/presenter/agentPresenter/acp/acpContentMapper.ts +++ b/src/main/presenter/agentPresenter/acp/acpContentMapper.ts @@ -1,6 +1,8 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' +import type { AcpConfigState } from '@shared/presenter' import type { AssistantMessageBlock } from '@shared/chat' import { createStreamEvent, type LLMCoreStreamEvent } from '@shared/types/core/llm-events' +import { normalizeAcpConfigState } from './acpConfigState' export interface PlanEntry { content: string @@ -21,6 +23,8 @@ export interface MappedContent { description: string input?: { hint: string } | null }> + /** Unified ACP session config state */ + configState?: AcpConfigState } interface ToolCallState { @@ -68,6 +72,13 @@ export class AcpContentMapper { ) this.handleAvailableCommandsUpdate(update, payload) break + case 'config_option_update': + this.handleConfigOptionUpdate(update, payload) + break + case 'session_info_update': + case 'usage_update': + // These updates are useful for stateful clients but do not affect chat rendering. + break case 'user_message_chunk': // ignore echo break @@ -268,6 +279,18 @@ export class AcpContentMapper { payload.availableCommands = commands } + private handleConfigOptionUpdate( + update: Extract< + schema.SessionNotification['update'], + { sessionUpdate: 'config_option_update' } + >, + payload: MappedContent + ) { + payload.configState = normalizeAcpConfigState({ + configOptions: update.configOptions + }) + } + private formatToolCallContent( contents?: schema.ToolCallContent[] | null, joiner: string = '\n' diff --git a/src/main/presenter/agentPresenter/acp/acpFsHandler.ts b/src/main/presenter/agentPresenter/acp/acpFsHandler.ts index 4943b5538..4240c92df 100644 --- a/src/main/presenter/agentPresenter/acp/acpFsHandler.ts +++ b/src/main/presenter/agentPresenter/acp/acpFsHandler.ts @@ -1,7 +1,7 @@ import * as fs from 'fs/promises' import * as path from 'path' import { RequestError } from '@agentclientprotocol/sdk' -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' import { buildBinaryReadGuidance, shouldRejectAcpTextRead } from '@/lib/binaryReadGuard' export interface FsHandlerOptions { diff --git a/src/main/presenter/agentPresenter/acp/acpMessageFormatter.ts b/src/main/presenter/agentPresenter/acp/acpMessageFormatter.ts index 85a61af38..976f4b32e 100644 --- a/src/main/presenter/agentPresenter/acp/acpMessageFormatter.ts +++ b/src/main/presenter/agentPresenter/acp/acpMessageFormatter.ts @@ -1,4 +1,4 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' import type { ChatMessage, ModelConfig } from '@shared/presenter' interface NormalizedContent { diff --git a/src/main/presenter/agentPresenter/acp/acpProcessManager.ts b/src/main/presenter/agentPresenter/acp/acpProcessManager.ts index f419e6cb2..01a3565ba 100644 --- a/src/main/presenter/agentPresenter/acp/acpProcessManager.ts +++ b/src/main/presenter/agentPresenter/acp/acpProcessManager.ts @@ -9,15 +9,22 @@ import type { ClientSideConnection as ClientSideConnectionType, Client } from '@agentclientprotocol/sdk' -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' import type { Stream } from '@agentclientprotocol/sdk/dist/stream.js' -import type { AcpAgentConfig } from '@shared/presenter' +import type { AcpAgentConfig, AcpConfigState } from '@shared/presenter' import type { AgentProcessHandle, AgentProcessManager } from './types' import { getShellEnvironment } from './shellEnvHelper' import { RuntimeHelper } from '@/lib/runtimeHelper' import { buildClientCapabilities } from './acpCapabilities' import { AcpFsHandler } from './acpFsHandler' import { AcpTerminalManager } from './acpTerminalManager' +import { + createEmptyAcpConfigState, + getAcpConfigOptionByCategory, + getLegacyModeState, + normalizeAcpConfigState, + updateAcpConfigStateValue +} from './acpConfigState' import { eventBus, SendTarget } from '@/eventbus' import { ACP_WORKSPACE_EVENTS } from '@/events' @@ -30,6 +37,7 @@ export interface AcpProcessHandle extends AgentProcessHandle { boundConversationId?: string /** The working directory this process was spawned with */ workdir: string + configState?: AcpConfigState availableModes?: Array<{ id: string; name: string; description: string }> currentModeId?: string mcpCapabilities?: schema.McpCapabilities @@ -244,8 +252,11 @@ export class AcpProcessManager implements AgentProcessManager { - console.warn(`[ACP] Failed to fetch modes during warmup for agent ${agent.id}:`, error) + void this.fetchProcessConfigState(handle).catch((error) => { + console.warn( + `[ACP] Failed to fetch config options during warmup for agent ${agent.id}:`, + error + ) }) this.applyPreferredMode(handle, preferredModeId) console.info( @@ -273,6 +284,13 @@ export class AcpProcessManager implements AgentProcessManager handle.workdir === resolvedWorkdir && this.isHandleAlive(handle) + ) + const handle = candidates[0] + if (!handle) { + return undefined + } + return handle.configState ?? createEmptyAcpConfigState('legacy') + } + registerSessionListener( agentId: string, sessionId: string, @@ -538,14 +596,9 @@ export class AcpProcessManager implements AgentProcessManager - currentModelId?: string - } - modes?: { - availableModes?: Array<{ id: string }> - currentModeId?: string - } + configOptions?: schema.SessionConfigOption[] | null + models?: schema.SessionModelState | null + modes?: schema.SessionModeState | null agentCapabilities?: { mcpCapabilities?: schema.McpCapabilities loadSession?: boolean @@ -569,19 +622,22 @@ export class AcpProcessManager implements AgentProcessManager ({ - id: m.id, - name: m.name ?? m.id, - description: m.description ?? '' - }) - ) + const initAvailableModes = resultData.modes?.availableModes?.map((m) => ({ + id: m.id, + name: m.name ?? m.id, + description: m.description ?? '' + })) if (initAvailableModes) { console.info( `[ACP] Available modes: ${JSON.stringify(initAvailableModes.map((m) => m.id) ?? [])}` ) console.info(`[ACP] Current mode: ${resultData.modes?.currentModeId}`) } + handleSeed.configState = normalizeAcpConfigState({ + configOptions: resultData.configOptions, + models: resultData.models, + modes: resultData.modes + }) handleSeed.availableModes = initAvailableModes handleSeed.currentModeId = resultData.modes?.currentModeId } catch (error) { @@ -615,6 +671,7 @@ export class AcpProcessManager implements AgentProcessManager { + private async fetchProcessConfigState(handle: AcpProcessHandle): Promise { if (!this.isHandleAlive(handle)) return try { const response = await handle.connection.newSession({ @@ -960,26 +1017,33 @@ export class AcpProcessManager implements AgentProcessManager ({ - id: mode.id, - name: mode.name ?? mode.id, - description: mode.description ?? '' - })) - // Preserve user-selected preferred mode if it exists in the available list + handle.configState = normalizeAcpConfigState({ + configOptions: response.configOptions, + models: response.models, + modes: response.modes + }) + + const legacyModeState = getLegacyModeState(handle.configState) + if (legacyModeState?.availableModes?.length) { + handle.availableModes = legacyModeState.availableModes if ( handle.currentModeId && - handle.availableModes.some((m) => m.id === handle.currentModeId) + handle.availableModes.some((mode) => mode.id === handle.currentModeId) ) { - // keep preferred - } else if (modes.currentModeId) { - handle.currentModeId = modes.currentModeId + const modeOption = getAcpConfigOptionByCategory(handle.configState, 'mode') + if (modeOption?.type === 'select') { + handle.configState = + updateAcpConfigStateValue(handle.configState, modeOption.id, handle.currentModeId) ?? + handle.configState + } + } else if (legacyModeState.currentModeId) { + handle.currentModeId = legacyModeState.currentModeId } else { handle.currentModeId = handle.availableModes[0]?.id ?? handle.currentModeId } this.notifyModesReady(handle) } + this.notifyConfigOptionsReady(handle) if (response.sessionId) { try { @@ -993,7 +1057,10 @@ export class AcpProcessManager implements AgentProcessManager void> workdir: string + configState?: AcpConfigState availableModes?: Array<{ id: string; name: string; description: string }> currentModeId?: string availableCommands?: Array<{ @@ -203,9 +211,15 @@ export class AcpSessionManager { console.warn('[ACP] Failed to persist session metadata:', error) }) - const availableModes = session.availableModes ?? handle.availableModes + let configState = + session.configState ?? handle.configState ?? createEmptyAcpConfigState('legacy') + const legacyModeState = getLegacyModeState(configState) + const availableModes = + session.availableModes ?? legacyModeState?.availableModes ?? handle.availableModes // Prefer handle.currentModeId (which may contain preferredMode from warmup) over session default - let currentModeId = handle.currentModeId ?? session.currentModeId + let currentModeId = + handle.currentModeId ?? session.currentModeId ?? legacyModeState?.currentModeId + handle.configState = configState handle.availableModes = availableModes handle.currentModeId = currentModeId @@ -221,6 +235,12 @@ export class AcpSessionManager { sessionId: session.sessionId, modeId: currentModeId }) + const modeOption = getAcpConfigOptionByCategory(configState, 'mode') + if (modeOption?.type === 'select') { + configState = + updateAcpConfigStateValue(configState, modeOption.id, currentModeId) ?? configState + handle.configState = configState + } console.info( `[ACP] Applied preferred mode "${currentModeId}" to session ${session.sessionId} for conversation ${conversationId}` ) @@ -246,6 +266,7 @@ export class AcpSessionManager { connection: handle.connection, detachHandlers: detachListeners, workdir, + configState, availableModes, currentModeId } @@ -276,6 +297,7 @@ export class AcpSessionManager { workdir: string ): Promise<{ sessionId: string + configState: AcpConfigState availableModes?: Array<{ id: string; name: string; description: string }> currentModeId?: string }> { @@ -330,13 +352,15 @@ export class AcpSessionManager { ) const persistedSessionId = persistedSession?.sessionId?.trim() || null - type SessionModes = { - availableModes?: Array<{ id: string; name: string; description?: string | null }> - currentModeId?: string - } - let sessionId = '' - let modes: SessionModes | undefined + let configState = handle.configState ?? createEmptyAcpConfigState('legacy') + let responseModeState: + | { + availableModes?: Array<{ id: string; name: string; description?: string | null }> + currentModeId?: string + } + | undefined + let sessionResponse: schema.LoadSessionResponse | schema.NewSessionResponse | undefined const canLoadSession = Boolean(handle.supportsLoadSession) if (canLoadSession && persistedSessionId) { @@ -347,7 +371,13 @@ export class AcpSessionManager { sessionId: persistedSessionId }) sessionId = persistedSessionId - modes = loadResponse.modes ?? undefined + sessionResponse = loadResponse + responseModeState = loadResponse.modes ?? undefined + configState = normalizeAcpConfigState({ + configOptions: loadResponse.configOptions, + models: loadResponse.models, + modes: loadResponse.modes + }) console.info( `[ACP] Loaded persisted session ${sessionId} for conversation ${conversationId} (agent ${agent.id})` ) @@ -365,19 +395,33 @@ export class AcpSessionManager { mcpServers }) sessionId = response.sessionId - modes = response.modes ?? undefined + sessionResponse = response + responseModeState = response.modes ?? undefined + configState = normalizeAcpConfigState({ + configOptions: response.configOptions, + models: response.models, + modes: response.modes + }) } + if (!sessionResponse) { + throw new Error('[ACP] Session initialization did not return a response payload') + } + + const legacyModeState = getLegacyModeState(configState) + // Extract modes from response if available const availableModes = - modes?.availableModes?.map((m) => ({ - id: m.id, - name: m.name ?? m.id, - description: m.description ?? '' - })) ?? handle.availableModes + legacyModeState?.availableModes ?? + responseModeState?.availableModes?.map((mode) => ({ + id: mode.id, + name: mode.name ?? mode.id, + description: mode.description ?? '' + })) ?? + handle.availableModes const preferredModeId = handle.currentModeId - const responseModeId = modes?.currentModeId + const responseModeId = legacyModeState?.currentModeId ?? responseModeState?.currentModeId let currentModeId = preferredModeId if ( !currentModeId || @@ -386,6 +430,13 @@ export class AcpSessionManager { currentModeId = responseModeId ?? currentModeId ?? availableModes?.[0]?.id } + const modeOption = getAcpConfigOptionByCategory(configState, 'mode') + if (modeOption?.type === 'select' && currentModeId) { + configState = + updateAcpConfigStateValue(configState, modeOption.id, currentModeId) ?? configState + } + + handle.configState = configState handle.availableModes = availableModes handle.currentModeId = currentModeId @@ -403,6 +454,7 @@ export class AcpSessionManager { return { sessionId, + configState, availableModes, currentModeId } diff --git a/src/main/presenter/agentPresenter/acp/acpTerminalManager.ts b/src/main/presenter/agentPresenter/acp/acpTerminalManager.ts index 53249d010..79bb08c4a 100644 --- a/src/main/presenter/agentPresenter/acp/acpTerminalManager.ts +++ b/src/main/presenter/agentPresenter/acp/acpTerminalManager.ts @@ -2,7 +2,7 @@ import { spawn } from 'node-pty' import type { IPty } from 'node-pty' import { nanoid } from 'nanoid' import { RequestError } from '@agentclientprotocol/sdk' -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' interface TerminalState { id: string @@ -154,9 +154,7 @@ export class AcpTerminalManager { /** * Kill a terminal command without releasing the terminal. */ - async killTerminal( - params: schema.KillTerminalCommandRequest - ): Promise { + async killTerminal(params: schema.KillTerminalRequest): Promise { const state = this.getTerminal(params.terminalId) if (!state.killed && !state.exitStatus) { diff --git a/src/main/presenter/agentPresenter/acp/index.ts b/src/main/presenter/agentPresenter/acp/index.ts index 6330394a6..f601fc48c 100644 --- a/src/main/presenter/agentPresenter/acp/index.ts +++ b/src/main/presenter/agentPresenter/acp/index.ts @@ -11,6 +11,17 @@ export { AcpSessionPersistence } from './acpSessionPersistence' export { buildClientCapabilities, type AcpCapabilityOptions } from './acpCapabilities' export { AcpMessageFormatter } from './acpMessageFormatter' export { AcpContentMapper } from './acpContentMapper' +export { + LEGACY_MODEL_CONFIG_ID, + LEGACY_MODE_CONFIG_ID, + createEmptyAcpConfigState, + getAcpConfigOption, + getAcpConfigOptionByCategory, + getAcpConfigOptionLabel, + getLegacyModeState, + normalizeAcpConfigState, + updateAcpConfigStateValue +} from './acpConfigState' export { AcpFsHandler } from './acpFsHandler' export { AcpTerminalManager } from './acpTerminalManager' export { AgentFileSystemHandler } from './agentFileSystemHandler' diff --git a/src/main/presenter/agentPresenter/acp/mcpConfigConverter.ts b/src/main/presenter/agentPresenter/acp/mcpConfigConverter.ts index d6ff4ab49..12fcbfe74 100644 --- a/src/main/presenter/agentPresenter/acp/mcpConfigConverter.ts +++ b/src/main/presenter/agentPresenter/acp/mcpConfigConverter.ts @@ -1,4 +1,4 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' import type { MCPServerConfig } from '@shared/presenter' const normalizeStringRecordToArray = ( diff --git a/src/main/presenter/agentPresenter/acp/mcpTransportFilter.ts b/src/main/presenter/agentPresenter/acp/mcpTransportFilter.ts index f2cda9838..80cecc085 100644 --- a/src/main/presenter/agentPresenter/acp/mcpTransportFilter.ts +++ b/src/main/presenter/agentPresenter/acp/mcpTransportFilter.ts @@ -1,4 +1,4 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' export function filterMcpServersByTransportSupport( servers: schema.McpServer[], diff --git a/src/main/presenter/llmProviderPresenter/index.ts b/src/main/presenter/llmProviderPresenter/index.ts index c511e1d4a..ae9ca658c 100644 --- a/src/main/presenter/llmProviderPresenter/index.ts +++ b/src/main/presenter/llmProviderPresenter/index.ts @@ -13,6 +13,7 @@ import { IConfigPresenter, ISQLitePresenter, IToolPresenter, + AcpConfigState, AcpWorkdirInfo, AcpDebugRequest, AcpDebugRunResult @@ -554,6 +555,17 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { return provider.getProcessModes(agentId, workdir) } + async getAcpProcessConfigOptions( + agentId: string, + workdir: string + ): Promise { + const provider = this.getAcpProviderInstance() + if (!provider) { + return null + } + return provider.getProcessConfigOptions(agentId, workdir) + } + async setAcpPreferredProcessMode(agentId: string, workdir: string, modeId: string) { const provider = this.getAcpProviderInstance() if (!provider) return @@ -588,6 +600,26 @@ export class LLMProviderPresenter implements ILlmProviderPresenter { return await provider.getSessionModes(conversationId) } + async getAcpSessionConfigOptions(conversationId: string): Promise { + const provider = this.getAcpProviderInstance() + if (!provider) { + return null + } + return await provider.getSessionConfigOptions(conversationId) + } + + async setAcpSessionConfigOption( + conversationId: string, + configId: string, + value: string | boolean + ): Promise { + const provider = this.getAcpProviderInstance() + if (!provider) { + throw new Error('[ACP] ACP provider not found') + } + return await provider.setSessionConfigOption(conversationId, configId, value) + } + async getAcpSessionCommands(conversationId: string): Promise< Array<{ name: string diff --git a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts index 865500f6f..ab2633949 100644 --- a/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts +++ b/src/main/presenter/llmProviderPresenter/providers/acpProvider.ts @@ -1,6 +1,7 @@ -import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' +import type * as schema from '@agentclientprotocol/sdk/dist/schema/index.js' import { BaseLLMProvider, SUMMARY_TITLES_PROMPT } from '../baseProvider' import type { + AcpConfigState, ChatMessage, LLMResponse, MCPToolDefinition, @@ -31,6 +32,13 @@ import { AcpContentMapper, AcpMessageFormatter, buildClientCapabilities, + getAcpConfigOption, + getAcpConfigOptionByCategory, + getLegacyModeState, + LEGACY_MODEL_CONFIG_ID, + LEGACY_MODE_CONFIG_ID, + normalizeAcpConfigState, + updateAcpConfigStateValue, type AcpProcessHandle, type AcpSessionRecord } from '../../agentPresenter/acp' @@ -57,6 +65,23 @@ type PendingPermissionState = { reject: (error: Error) => void } +type AcpConnectionWithModelSelection = { + unstable_setSessionModel?: ( + params: schema.SetSessionModelRequest + ) => Promise +} + +async function setSessionModelCompat( + connection: AcpConnectionWithModelSelection, + params: schema.SetSessionModelRequest +): Promise { + if (!connection.unstable_setSessionModel) { + throw new Error('[ACP] Session model selection is not supported by this SDK connection.') + } + + return connection.unstable_setSessionModel(params) +} + export class AcpProvider extends BaseLLMProvider { private readonly processManager: AcpProcessManager private readonly sessionManager: AcpSessionManager @@ -338,6 +363,12 @@ export class AcpProvider extends BaseLLMProvider { session.currentModeId, session.availableModes ) + this.emitSessionConfigOptionsReady( + conversationKey, + agent.id, + session.workdir, + session.configState + ) this.emitSessionCommandsReady(conversationKey, agent.id, session.availableCommands ?? []) const promptBlocks = this.messageFormatter.format(messages, modelConfig) @@ -428,6 +459,12 @@ export class AcpProvider extends BaseLLMProvider { session.currentModeId, session.availableModes ) + this.emitSessionConfigOptionsReady( + conversationId, + agent.id, + session.workdir, + session.configState + ) this.emitSessionCommandsReady(conversationId, agent.id, session.availableCommands ?? []) } @@ -454,6 +491,10 @@ export class AcpProvider extends BaseLLMProvider { return this.processManager.getProcessModes(agentId, workdir) ?? undefined } + public getProcessConfigOptions(agentId: string, workdir: string): AcpConfigState | null { + return this.processManager.getProcessConfigState(agentId, workdir) ?? null + } + public async setPreferredProcessMode(agentId: string, workdir: string, modeId: string) { const agent = await this.getAgentById(agentId) if (!agent) return @@ -735,7 +776,10 @@ export class AcpProvider extends BaseLLMProvider { payload: body }) attachSession(activeSessionId) - const response = await connection.setSessionModel(body as schema.SetSessionModelRequest) + const response = await setSessionModelCompat( + connection, + body as schema.SetSessionModelRequest + ) pushEvent({ kind: 'response', action: 'setSessionModel', @@ -866,6 +910,39 @@ export class AcpProvider extends BaseLLMProvider { } this.emitSessionCommandsReady(conversationId, agentId, mapped.availableCommands) } + + if (mapped.configState && currentSession) { + currentSession.configState = mapped.configState + const legacyModeState = getLegacyModeState(mapped.configState) + if (legacyModeState) { + currentSession.availableModes = legacyModeState.availableModes + currentSession.currentModeId = legacyModeState.currentModeId ?? currentSession.currentModeId + this.emitSessionModesReady( + conversationId, + agentId, + currentSession.workdir, + currentSession.currentModeId, + currentSession.availableModes + ) + } + + const updated = this.processManager.updateBoundProcessConfigState( + conversationId, + mapped.configState + ) + if (!updated) { + console.warn( + `[ACP] Bound process not found for conversation ${conversationId} while updating config state.` + ) + } + + this.emitSessionConfigOptionsReady( + conversationId, + agentId, + currentSession.workdir, + mapped.configState + ) + } } private emitSessionModesReady( @@ -900,6 +977,24 @@ export class AcpProvider extends BaseLLMProvider { }) } + private emitSessionConfigOptionsReady( + conversationId: string, + agentId: string, + workdir: string, + configState?: AcpConfigState | null + ): void { + eventBus.sendToRenderer( + ACP_WORKSPACE_EVENTS.SESSION_CONFIG_OPTIONS_READY, + SendTarget.ALL_WINDOWS, + { + conversationId, + agentId, + workdir, + configState: configState ?? normalizeAcpConfigState({}) + } + ) + } + private async handlePermissionRequest( queue: EventQueue, params: schema.RequestPermissionRequest, @@ -1152,6 +1247,12 @@ export class AcpProvider extends BaseLLMProvider { throw new Error(`[ACP] No session found for conversation ${conversationId}`) } + const configModeOption = getAcpConfigOptionByCategory(session.configState, 'mode') + if (configModeOption?.type === 'select' && configModeOption.id !== LEGACY_MODE_CONFIG_ID) { + await this.setSessionConfigOption(conversationId, configModeOption.id, modeId) + return + } + const previousMode = session.currentModeId ?? 'default' const availableModes = session.availableModes ?? [] const availableModeIds = availableModes.map((m) => m.id) @@ -1176,12 +1277,21 @@ export class AcpProvider extends BaseLLMProvider { ) await session.connection.setSessionMode({ sessionId: session.sessionId, modeId }) session.currentModeId = modeId + session.configState = + updateAcpConfigStateValue(session.configState, LEGACY_MODE_CONFIG_ID, modeId) ?? + session.configState const updated = this.processManager.updateBoundProcessMode(conversationId, modeId) if (!updated) { console.warn( `[ACP] Bound process not found for conversation ${conversationId} while setting mode "${modeId}".` ) } + this.emitSessionConfigOptionsReady( + conversationId, + session.agentId, + session.workdir, + session.configState + ) eventBus.sendToRenderer(ACP_WORKSPACE_EVENTS.SESSION_MODES_READY, SendTarget.ALL_WINDOWS, { conversationId, agentId: session.agentId, @@ -1214,6 +1324,14 @@ export class AcpProvider extends BaseLLMProvider { return null } + const legacyModeState = getLegacyModeState(session.configState) + if (legacyModeState) { + return { + current: legacyModeState.currentModeId ?? session.currentModeId ?? 'default', + available: legacyModeState.availableModes + } + } + const result = { current: session.currentModeId ?? 'default', available: session.availableModes ?? [] @@ -1227,6 +1345,112 @@ export class AcpProvider extends BaseLLMProvider { return result } + async getSessionConfigOptions(conversationId: string): Promise { + const session = this.sessionManager.getSession(conversationId) + if (!session) { + return null + } + return session.configState ?? null + } + + async setSessionConfigOption( + conversationId: string, + configId: string, + value: string | boolean + ): Promise { + const session = this.sessionManager.getSession(conversationId) + if (!session) { + throw new Error(`[ACP] No session found for conversation ${conversationId}`) + } + + const option = getAcpConfigOption(session.configState, configId) + if (!option) { + throw new Error( + `[ACP] Config option "${configId}" is unavailable for conversation ${conversationId}` + ) + } + + let nextConfigState: AcpConfigState | null = null + + if (configId === LEGACY_MODE_CONFIG_ID) { + if (typeof value !== 'string') { + throw new Error('[ACP] Legacy mode config option expects a string value') + } + await session.connection.setSessionMode({ sessionId: session.sessionId, modeId: value }) + session.currentModeId = value + nextConfigState = + updateAcpConfigStateValue(session.configState, configId, value) ?? + session.configState ?? + null + } else if (configId === LEGACY_MODEL_CONFIG_ID) { + if (typeof value !== 'string') { + throw new Error('[ACP] Legacy model config option expects a string value') + } + await setSessionModelCompat(session.connection, { + sessionId: session.sessionId, + modelId: value + }) + nextConfigState = + updateAcpConfigStateValue(session.configState, configId, value) ?? + session.configState ?? + null + } else { + const response = + typeof value === 'boolean' + ? await session.connection.setSessionConfigOption({ + sessionId: session.sessionId, + configId, + type: 'boolean', + value + }) + : await session.connection.setSessionConfigOption({ + sessionId: session.sessionId, + configId, + value + }) + nextConfigState = normalizeAcpConfigState({ + configOptions: response.configOptions + }) + } + + if (!nextConfigState) { + return null + } + + session.configState = nextConfigState + const legacyModeState = getLegacyModeState(nextConfigState) + if (legacyModeState) { + session.availableModes = legacyModeState.availableModes + session.currentModeId = legacyModeState.currentModeId ?? session.currentModeId + this.emitSessionModesReady( + conversationId, + session.agentId, + session.workdir, + session.currentModeId, + session.availableModes + ) + } + + const updated = this.processManager.updateBoundProcessConfigState( + conversationId, + nextConfigState + ) + if (!updated) { + console.warn( + `[ACP] Bound process not found for conversation ${conversationId} while setting config option "${configId}".` + ) + } + + this.emitSessionConfigOptionsReady( + conversationId, + session.agentId, + session.workdir, + nextConfigState + ) + + return nextConfigState + } + async getSessionCommands(conversationId: string): Promise< Array<{ name: string diff --git a/src/main/presenter/newAgentPresenter/index.ts b/src/main/presenter/newAgentPresenter/index.ts index 7949c14bd..2b75d7d92 100644 --- a/src/main/presenter/newAgentPresenter/index.ts +++ b/src/main/presenter/newAgentPresenter/index.ts @@ -23,6 +23,7 @@ import type { import type { Message } from '@shared/chat' import type { SearchResult } from '@shared/types/core/search' import type { + AcpConfigState, IConfigPresenter, ILlmProviderPresenter, ISkillPresenter, @@ -826,21 +827,38 @@ export class NewAgentPresenter { > { const session = this.sessionManager.get(sessionId) if (!session) return [] - const agent = await this.resolveAgentImplementation(session.agentId) - const state = await agent.getSessionState(sessionId) - let providerId = state?.providerId ?? '' - if (!providerId) { - const acpAgents = await this.configPresenter.getAcpAgents() - if (acpAgents.some((item) => item.id === session.agentId)) { - providerId = 'acp' - } - } - if (providerId !== 'acp') { + if (!(await this.isAcpBackedSession(sessionId, session.agentId))) { return [] } return await this.llmProviderPresenter.getAcpSessionCommands(sessionId) } + async getAcpSessionConfigOptions(sessionId: string): Promise { + const session = this.sessionManager.get(sessionId) + if (!session) { + return null + } + if (!(await this.isAcpBackedSession(sessionId, session.agentId))) { + return null + } + return await this.llmProviderPresenter.getAcpSessionConfigOptions(sessionId) + } + + async setAcpSessionConfigOption( + sessionId: string, + configId: string, + value: string | boolean + ): Promise { + const session = this.sessionManager.get(sessionId) + if (!session) { + throw new Error(`Session not found: ${sessionId}`) + } + if (!(await this.isAcpBackedSession(sessionId, session.agentId))) { + throw new Error('ACP session config options are only available for ACP sessions.') + } + return await this.llmProviderPresenter.setAcpSessionConfigOption(sessionId, configId, value) + } + async getPermissionMode(sessionId: string): Promise { const session = this.sessionManager.get(sessionId) if (!session) { @@ -1096,6 +1114,19 @@ export class NewAgentPresenter { } } + private async isAcpBackedSession(sessionId: string, agentId: string): Promise { + const agent = await this.resolveAgentImplementation(agentId) + const state = await agent.getSessionState(sessionId) + let providerId = state?.providerId ?? '' + if (!providerId) { + const acpAgents = await this.configPresenter.getAcpAgents() + if (acpAgents.some((item) => item.id === agentId)) { + providerId = 'acp' + } + } + return providerId === 'acp' + } + private async findReusableDraftSession( agentId: string, projectDir: string, diff --git a/src/renderer/src/components/chat/ChatStatusBar.vue b/src/renderer/src/components/chat/ChatStatusBar.vue index 41463a14a..e2b60ae62 100644 --- a/src/renderer/src/components/chat/ChatStatusBar.vue +++ b/src/renderer/src/components/chat/ChatStatusBar.vue @@ -2,7 +2,7 @@
- +