Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,053 changes: 1,053 additions & 0 deletions docs/ACP_PROTOCOL_GAP_ANALYSIS.md

Large diffs are not rendered by default.

148 changes: 148 additions & 0 deletions docs/ACP_UI_IMPLEMENTATION_SUMMARY.md
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. 优化长输出的显示性能
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "DeepChat",
"version": "0.5.1",
"version": "0.5.2",
"description": "DeepChat,一个简单易用的AI客户端",
"main": "./out/main/index.js",
"author": "ThinkInAIXYZ",
Expand Down
52 changes: 39 additions & 13 deletions src/main/presenter/configPresenter/acpInitHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { AcpBuiltinAgentId, AcpAgentConfig, AcpAgentProfile } from '@shared
import { spawn } from 'node-pty'
import type { IPty } from 'node-pty'
import { RuntimeHelper } from '@/lib/runtimeHelper'
import { getShellEnvironment } from '../llmProviderPresenter/agent/shellEnvHelper'

const execAsync = promisify(exec)

Expand Down Expand Up @@ -238,7 +239,7 @@ class AcpInitHelper {
commands: initConfig.commands
})

const envVars = this.buildEnvironmentVariables(
const envVars = await this.buildEnvironmentVariables(
profile,
useBuiltinRuntime,
npmRegistry,
Expand Down Expand Up @@ -270,7 +271,7 @@ class AcpInitHelper {
hasWebContents: !!webContents
})

const envVars = this.buildEnvironmentVariables(
const envVars = await this.buildEnvironmentVariables(
agent,
useBuiltinRuntime,
npmRegistry,
Expand Down Expand Up @@ -489,12 +490,12 @@ class AcpInitHelper {
/**
* Build environment variables for the terminal
*/
private buildEnvironmentVariables(
private async buildEnvironmentVariables(
profile: AcpAgentProfile | AcpAgentConfig,
useBuiltinRuntime: boolean,
npmRegistry: string | null,
uvRegistry: string | null
): Record<string, string> {
): Promise<Record<string, string>> {
console.log('[ACP Init] Building environment variables:', {
useBuiltinRuntime,
npmRegistry,
Expand All @@ -515,11 +516,36 @@ class AcpInitHelper {
})
console.log('[ACP Init] Added system environment variables:', systemEnvCount)

// Merge shell environment (includes user PATH from shell startup)
const pathKeys = ['PATH', 'Path', 'path']
const existingPaths: string[] = []
pathKeys.forEach((key) => {
const value = env[key]
if (value) existingPaths.push(value)
})

try {
const shellEnv = await getShellEnvironment()
Object.entries(shellEnv).forEach(([key, value]) => {
if (value !== undefined && value !== '' && !pathKeys.includes(key)) {
env[key] = value
}
})
const shellPath = shellEnv.PATH || shellEnv.Path || shellEnv.path
if (shellPath) {
existingPaths.unshift(shellPath)
}
} catch (error) {
console.warn('[ACP Init] Failed to merge shell environment variables:', error)
}

// Prepare PATH merging
const HOME_DIR = app.getPath('home')
const defaultPaths = this.runtimeHelper.getDefaultPaths(HOME_DIR)
const allPaths = [...existingPaths, ...defaultPaths]

// Add runtime paths to PATH if using builtin runtime
if (useBuiltinRuntime) {
const pathKey = process.platform === 'win32' ? 'Path' : 'PATH'
const existingPath = env[pathKey] || ''
const separator = process.platform === 'win32' ? ';' : ':'
const runtimePaths: string[] = []

const uvRuntimePath = this.runtimeHelper.getUvRuntimePath()
Expand All @@ -544,16 +570,16 @@ class AcpInitHelper {
}

if (runtimePaths.length > 0) {
env[pathKey] = [...runtimePaths, existingPath].filter(Boolean).join(separator)
console.log('[ACP Init] Updated PATH with runtime paths:', {
runtimePaths,
finalPathLength: env[pathKey].length
})
allPaths.unshift(...runtimePaths)
} else {
console.warn('[ACP Init] No runtime paths available to add to PATH')
}
}

// Normalize and set PATH
const normalizedPath = this.runtimeHelper.normalizePathEnv(allPaths)
env[normalizedPath.key] = normalizedPath.value

// Add registry environment variables if using builtin runtime
if (useBuiltinRuntime) {
if (npmRegistry && npmRegistry !== '') {
Expand Down Expand Up @@ -610,7 +636,7 @@ class AcpInitHelper {
// Merge PATH variables
const pathKey = process.platform === 'win32' ? 'Path' : 'PATH'
const separator = process.platform === 'win32' ? ';' : ':'
const existingPath = env[pathKey] || ''
const existingPath = env[pathKey] || env[normalizedPath.key] || ''
env[pathKey] = [value, existingPath].filter(Boolean).join(separator)
console.log('[ACP Init] Merged custom PATH from profile:', {
customPath: value,
Expand Down
31 changes: 31 additions & 0 deletions src/main/presenter/llmProviderPresenter/agent/acpCapabilities.ts
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
}
Loading