-
Notifications
You must be signed in to change notification settings - Fork 649
feat: add support for acp plan and file #1156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 8 commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
91602ad
feat: add init docs
zerob13 554c3b2
feat(acp): implement fs and terminal capabilities for full ACP support
zerob13 f215b53
feat(acp): add structured plan and mode update handling
zerob13 591f3b1
test(acp): add unit tests for ACP capabilities, fs handler, and conte…
zerob13 6aa51ef
feat: add support acp plan
zerob13 4915d8c
fix: i18n
zerob13 e812323
fix: add i18n
zerob13 1c824f9
feat: add mode switcher
zerob13 28c179d
chore: mode only show in agent conv
zerob13 b14f0dd
fix: message tool parser and mode switch
zerob13 0f10a04
feat: mask env and inject env on init
zerob13 86ef7f8
fix: code reviewer issues
zerob13 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| # ACP UI 集成实施总结 | ||
|
|
||
| ## 已完成的工作 | ||
|
|
||
| ### 1. 类型定义更新 ✅ | ||
| - 在 `src/shared/chat.d.ts` 中添加了 `'plan'` 类型到 `AssistantMessageBlock` | ||
|
|
||
| ### 2. Content Mapper 调整 ✅ | ||
| - 修改了 `src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts` | ||
| - `handlePlanUpdate` 现在创建独立的 `plan` 类型块 | ||
| - 移除了未使用的 `getStatusIcon` 方法 | ||
|
|
||
| ### 3. 新增 Plan 展示组件 ✅ | ||
| - 创建了 `src/renderer/src/components/message/MessageBlockPlan.vue` | ||
| - 功能包括: | ||
| - 任务列表展示(带状态图标:○ pending, ◐ in_progress, ● done, ⊘ skipped, ✕ failed) | ||
| - 进度条和完成统计 | ||
| - 可折叠/展开功能 | ||
| - 优先级标签显示 | ||
|
|
||
| ### 4. Mode 指示器增强 ✅ | ||
| - 修改了 `src/renderer/src/components/message/MessageBlockThink.vue` | ||
| - 添加了 `isModeChange` 和 `modeChangeId` computed 属性 | ||
| - 标题会自动显示 "模式已切换至:{mode}" 当检测到模式变化时 | ||
|
|
||
| ### 5. 终端输出和文件操作增强 ✅ | ||
| - 修改了 `src/renderer/src/components/message/MessageBlockToolCall.vue` | ||
| - 添加的功能: | ||
| - **终端输出**:使用 xterm.js 渲染终端输出(黑色背景,monospace 字体) | ||
| - **文件系统操作**:显示文件路径、操作类型(读/写)和结果(成功/失败) | ||
| - 自动检测工具名称以应用对应的 UI | ||
|
|
||
| ### 6. 注册新组件 ✅ | ||
| - 修改了 `src/renderer/src/components/message/MessageItemAssistant.vue` | ||
| - 添加了 `MessageBlockPlan` 组件的导入和渲染 | ||
|
|
||
| ### 7. i18n 翻译 ✅ | ||
| - 为所有 11 种语言添加了翻译: | ||
| - zh-CN(中文简体) | ||
| - en-US(英语) | ||
| - ja-JP(日语) | ||
| - ko-KR(韩语) | ||
| - ru-RU(俄语) | ||
| - fr-FR(法语) | ||
| - da-DK(丹麦语) | ||
| - fa-IR(波斯语) | ||
| - pt-BR(葡萄牙语) | ||
| - zh-HK(中文繁体 香港) | ||
| - zh-TW(中文繁体 台湾) | ||
|
|
||
| - 翻译键包括: | ||
| - `chat.features.modeChanged`: 模式切换提示 | ||
| - `toolCall.terminalOutput`: 终端输出 | ||
| - `toolCall.fileOperation/fileRead/fileWrite`: 文件操作 | ||
| - `toolCall.filePath/success/failed`: 文件路径和结果 | ||
| - `plan.title/completed`: 计划标题和完成状态 | ||
|
|
||
| ### 8. 代码质量检查 ✅ | ||
| - 运行 `pnpm run format` - 通过 ✅ | ||
| - 运行 `pnpm run lint` - 0 warnings, 0 errors ✅ | ||
| - 运行 `pnpm run typecheck` - 通过 ✅ | ||
|
|
||
| ## 新增文件 | ||
|
|
||
| 1. `src/renderer/src/components/message/MessageBlockPlan.vue` - Plan 展示组件 | ||
| 2. `src/renderer/src/i18n/*/plan.json` - 11 个语言的 plan 翻译文件 | ||
|
|
||
| ## 修改的文件 | ||
|
|
||
| 1. `src/shared/chat.d.ts` - 添加 plan 类型 | ||
| 2. `src/main/presenter/llmProviderPresenter/agent/acpContentMapper.ts` - 修改 plan 处理逻辑 | ||
| 3. `src/renderer/src/components/message/MessageBlockThink.vue` - 添加 mode 检测 | ||
| 4. `src/renderer/src/components/message/MessageBlockToolCall.vue` - 添加终端和文件系统 UI | ||
| 5. `src/renderer/src/components/message/MessageItemAssistant.vue` - 注册 plan 组件 | ||
| 6. `src/renderer/src/i18n/*/chat.json` (11 个文件) - 添加 modeChanged | ||
| 7. `src/renderer/src/i18n/*/toolCall.json` (11 个文件) - 添加新的工具调用翻译 | ||
| 8. `src/renderer/src/i18n/*/index.ts` (11 个文件) - 导入 plan 模块 | ||
|
|
||
| ## 如何测试 | ||
|
|
||
| ### 测试 Plan 展示 | ||
| ```bash | ||
| pnpm run dev | ||
| ``` | ||
| 1. 创建新对话,选择支持 ACP 的 Agent(如 claude-code-acp) | ||
| 2. 发送消息触发 Agent 返回计划,如:"请制定一个实现用户登录功能的计划" | ||
| 3. 观察消息流中是否出现 Plan 卡片组件 | ||
| 4. 验证进度条、状态图标、折叠功能是否正常 | ||
|
|
||
| ### 测试文件系统操作 | ||
| 1. 向 Agent 发送:"请读取 package.json 文件的内容" | ||
| 2. 等待 Agent 执行 `readTextFile` 工具调用 | ||
| 3. 点击工具调用卡片展开详情 | ||
| 4. 验证文件路径和操作状态是否正确显示 | ||
|
|
||
| ### 测试终端命令执行 | ||
| 1. 向 Agent 发送:"请执行 'ls -la' 命令查看当前目录" | ||
| 2. 等待 Agent 创建终端并执行命令 | ||
| 3. 点击工具调用卡片展开详情 | ||
| 4. 验证终端输出是否在黑色背景的终端窗口中正确显示 | ||
|
|
||
| ### 测试 Mode 变化通知 | ||
| 1. 使用支持 session mode 的 Agent | ||
| 2. 触发 Agent 切换模式 | ||
| 3. 观察 thinking 块是否显示 "模式已切换至:{mode_id}" | ||
|
|
||
| ### 测试多语言支持 | ||
| 1. 在设置中切换语言(英文、中文、日文等) | ||
| 2. 重复上述测试场景 | ||
| 3. 验证所有新增的文本标签是否正确显示对应语言的翻译 | ||
|
|
||
| ## 架构说明 | ||
|
|
||
| ### 数据流 | ||
| ``` | ||
| Agent (ACP Process) | ||
| ↓ | ||
| AcpContentMapper (处理 plan/mode 更新) | ||
| ↓ | ||
| MessageBlock (plan 类型块) | ||
| ↓ | ||
| MessageBlockPlan.vue (UI 渲染) | ||
| ``` | ||
|
|
||
| ### 终端输出流程 | ||
| ``` | ||
| Agent → createTerminal → terminalOutput | ||
| ↓ | ||
| Tool Call Response (包含 output) | ||
| ↓ | ||
| MessageBlockToolCall (检测 terminal 工具) | ||
| ↓ | ||
| xterm.js (渲染终端输出) | ||
| ``` | ||
|
|
||
| ## 注意事项 | ||
|
|
||
| 1. **性能**:xterm.js 已配置 scrollback: 1000,限制终端输出缓冲 | ||
| 2. **安全**:终端为只读模式 (`disableStdin: true`) | ||
| 3. **类型安全**:PlanEntry 接口在组件内部定义,避免跨模块导入问题 | ||
| 4. **响应式**:所有组件使用 Tailwind CSS,支持移动端显示 | ||
|
|
||
| ## 下一步建议 | ||
|
|
||
| 1. 在真实的 ACP Agent 环境中进行完整测试 | ||
| 2. 根据实际使用反馈调整 UI 细节 | ||
| 3. 考虑添加更多状态图标或动画效果 | ||
| 4. 优化长输出的显示性能 |
31 changes: 31 additions & 0 deletions
31
src/main/presenter/llmProviderPresenter/agent/acpCapabilities.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type * as schema from '@agentclientprotocol/sdk/dist/schema.js' | ||
|
|
||
| export interface AcpCapabilityOptions { | ||
| enableFs?: boolean | ||
| enableTerminal?: boolean | ||
| } | ||
|
|
||
| /** | ||
| * Build client capabilities object for ACP initialization. | ||
| * | ||
| * This determines what features the client (DeepChat) advertises to the agent. | ||
| * Agents use these capabilities to decide which operations to request. | ||
| */ | ||
| export function buildClientCapabilities( | ||
| options: AcpCapabilityOptions = {} | ||
| ): schema.ClientCapabilities { | ||
| const caps: schema.ClientCapabilities = {} | ||
|
|
||
| if (options.enableFs !== false) { | ||
| caps.fs = { | ||
| readTextFile: true, | ||
| writeTextFile: true | ||
| } | ||
| } | ||
|
|
||
| if (options.enableTerminal !== false) { | ||
| caps.terminal = true | ||
| } | ||
|
|
||
| return caps | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
src/main/presenter/llmProviderPresenter/agent/acpFsHandler.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| 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' | ||
|
|
||
| export interface FsHandlerOptions { | ||
| /** Session's working directory (workspace root). Null = allow all. */ | ||
| workspaceRoot: string | null | ||
| /** Maximum file size in bytes to read (default: 10MB) */ | ||
| maxReadSize?: number | ||
| } | ||
|
|
||
| /** | ||
| * Handles file system operations requested by ACP agents. | ||
| * | ||
| * This handler implements `fs/read_text_file` and `fs/write_text_file` methods | ||
| * as specified in the ACP protocol. It enforces workspace boundaries for security. | ||
| * | ||
| * @see https://agentclientprotocol.com/protocol/file-system | ||
| */ | ||
| export class AcpFsHandler { | ||
| private readonly workspaceRoot: string | null | ||
| private readonly maxReadSize: number | ||
|
|
||
| constructor(options: FsHandlerOptions) { | ||
| this.workspaceRoot = options.workspaceRoot ? path.resolve(options.workspaceRoot) : null | ||
| this.maxReadSize = options.maxReadSize ?? 10 * 1024 * 1024 // 10MB default | ||
| } | ||
|
|
||
| /** | ||
| * Validate that the path is within the workspace boundary. | ||
| * Throws RequestError if path escapes workspace. | ||
| */ | ||
| private validatePath(filePath: string): string { | ||
| const resolved = path.resolve(filePath) | ||
|
|
||
| if (this.workspaceRoot) { | ||
| const relative = path.relative(this.workspaceRoot, resolved) | ||
| if (relative.startsWith('..') || path.isAbsolute(relative)) { | ||
| throw RequestError.invalidParams({ path: filePath }, `Path escapes workspace: ${filePath}`) | ||
| } | ||
| } | ||
|
|
||
| return resolved | ||
| } | ||
|
|
||
| /** | ||
| * Read content from a text file. | ||
| * | ||
| * Supports optional line offset and limit for reading portions of large files. | ||
| */ | ||
| async readTextFile(params: schema.ReadTextFileRequest): Promise<schema.ReadTextFileResponse> { | ||
| const filePath = this.validatePath(params.path) | ||
|
|
||
| try { | ||
| const stat = await fs.stat(filePath) | ||
| if (stat.size > this.maxReadSize) { | ||
| throw RequestError.invalidParams( | ||
| { path: params.path, size: stat.size }, | ||
| `File too large: ${stat.size} bytes exceeds limit of ${this.maxReadSize}` | ||
| ) | ||
| } | ||
|
|
||
| const content = await fs.readFile(filePath, 'utf-8') | ||
| const lines = content.split('\n') | ||
|
|
||
| // Handle optional line/limit parameters (1-based line numbers per ACP spec) | ||
| const startLine = params.line ?? 1 | ||
| const limit = params.limit ?? lines.length | ||
|
|
||
| const startIndex = Math.max(0, startLine - 1) | ||
| const endIndex = startIndex + limit | ||
| const selectedLines = lines.slice(startIndex, endIndex) | ||
|
|
||
| return { content: selectedLines.join('\n') } | ||
| } catch (error) { | ||
| if (error instanceof RequestError) { | ||
| throw error | ||
| } | ||
| if ((error as NodeJS.ErrnoException).code === 'ENOENT') { | ||
| throw RequestError.resourceNotFound(params.path) | ||
| } | ||
| if ((error as NodeJS.ErrnoException).code === 'EACCES') { | ||
| throw RequestError.invalidParams({ path: params.path }, `Permission denied: ${params.path}`) | ||
| } | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Write content to a text file. | ||
| * | ||
| * Creates parent directories if they don't exist. | ||
| */ | ||
| async writeTextFile(params: schema.WriteTextFileRequest): Promise<schema.WriteTextFileResponse> { | ||
| const filePath = this.validatePath(params.path) | ||
|
|
||
| try { | ||
| // Ensure parent directory exists | ||
| const dir = path.dirname(filePath) | ||
| await fs.mkdir(dir, { recursive: true }) | ||
|
|
||
| await fs.writeFile(filePath, params.content, 'utf-8') | ||
| return {} | ||
| } catch (error) { | ||
| if (error instanceof RequestError) { | ||
| throw error | ||
| } | ||
| if ((error as NodeJS.ErrnoException).code === 'EACCES') { | ||
| throw RequestError.invalidParams({ path: params.path }, `Permission denied: ${params.path}`) | ||
| } | ||
| throw error | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap switch case declaration in a block to fix correctness issue.
The
const sessionUpdatedeclaration in the default case is accessible to other switch clauses, which can lead to subtle bugs. As flagged by static analysis, wrap the declaration in a block to restrict its scope.🧰 Tools
🪛 Biome (2.1.2)
[error] 68-68: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Safe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
🤖 Prompt for AI Agents