From 21e99dd604731000f86deb09178e71a51b59b88b Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 08:47:18 +0800 Subject: [PATCH 01/84] Adds VS Code debug and task configurations for development --- .vscode/launch.json | 23 +++++++++++++++++++++++ .vscode/tasks.json | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f8e3687 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "ProleCoder: Extension Development Host", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}/vscode/extension", + "${workspaceFolder}" + ], + "env": { + "PATH": "${workspaceFolder}/target/debug${pathSeparator}${env:PATH}", + "Path": "${workspaceFolder}/target/debug${pathSeparator}${env:Path}" + }, + "outFiles": [ + "${workspaceFolder}/vscode/extension/out/**/*.js" + ], + "preLaunchTask": "ProleCoder: build debug dependencies" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ea85c22 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,44 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "ProleCoder: build debug dependencies", + "dependsOrder": "sequence", + "dependsOn": [ + "ProleCoder: build CLI", + "ProleCoder: build VS Code extension" + ], + "problemMatcher": [] + }, + { + "label": "ProleCoder: build CLI", + "type": "shell", + "command": "cargo", + "args": [ + "build", + "-p", + "prole-coder-cli" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "problemMatcher": [] + }, + { + "label": "ProleCoder: build VS Code extension", + "type": "shell", + "command": "pnpm", + "args": [ + "--filter", + "prole-coder-vscode", + "build" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "problemMatcher": "$tsc" + } + ] +} From efb78c8b79f51fa10d89f1f658b6ad8de615bf3d Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 08:47:19 +0800 Subject: [PATCH 02/84] Refactors Phase 4/5 boundaries and adds Phase 5 UX tasks to documentation --- README.md | 27 ++++++++++++++++++--------- docs/phase-tasks.md | 21 +++++++++++++-------- docs/release.md | 2 +- docs/roadmap.md | 14 +++++++------- docs/testing.md | 3 ++- docs/tui.md | 4 ++-- docs/vscode-extension.md | 16 ++++++++++------ 7 files changed, 53 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index d847d9d..8f0fe4b 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线均已完成。Phase 4 的 VS Code 深度集成已完成,包含原 14 项能力以及 P4-15 到 P4-18 的 Codex-like UX 收敛:默认使用 VS Code 原生 Chat 右侧入口、简化审批 UX、保持连续会话心智并自动压缩历史上下文。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩和简化审批 UX 均已完成。之后进入 Phase 5:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线、Phase 4 的 VS Code 深度集成以及 Phase 5 的 VS Code 插件 UX 体验优化均已完成。Phase 4 保留 14 项深度集成能力;Phase 5 单独收敛 Codex-like UX 与诊断体验:默认使用 VS Code 原生 Chat 右侧入口、简化审批 UX、保持连续会话心智、自动压缩历史上下文,并把完整错误写入 `ProleCoder` Output Channel。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX 和 Output Channel 错误诊断均已完成。之后进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -675,17 +675,13 @@ extension.ts - [x] P4-5:RPC 高频事件输出节流与批量发送策略,实时 wire 层支持 `agent.eventBatch`,Run Log 与 replay 仍保持逐事件 `seq` 事实来源。 - [x] P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已新增 `RpcServerManager.cancel()`、Cancel 按钮和运行中 composer 状态收口。 - [x] P4-7:Problems 面板 diagnostics 通过 diagnostic attachments 进入 Context Builder,VS Code 发送 turn 时会采集当前 Problems 快照,按协议 attachment 上限裁剪并优先保留 error。 -- [x] P4-8:Terminal command approval,审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要和持久化语义;P4-16 后主审批 modal 不再暴露复杂持久化选项。 +- [x] P4-8:Terminal command approval,审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要和持久化语义;P5-2 后主审批 modal 不再暴露复杂持久化选项。 - [x] P4-9:审批持久化存储,RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 - [x] P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已新增 Open Settings 命令,打开 VS Code 设置并展示 RPC server capability、默认模型、预算、审批能力、RPC command/state;配置只包含非敏感 RPC/FIM 选项,不保存 API Key。 - [x] P4-11:真实 hunk 级 patch 审批,首版限定 `apply_patch`,Core/RPC 支持 selected hunk 决策、校验未知/重复 hunk、Run Log 记录 selected/all 范围,VS Code modal 可选择 hunks 并通过 `agent.approve.hunks` 回传;审批事件 payload 已同步协议 fixture。 - [x] P4-12:FIM completion preview,已新增 `agent.previewFim` RPC、DeepSeek beta FIM adapter、fixture provider 预览和 VS Code inline completion provider,模型选择只依赖 server capability 的 `supportsFim`。 - [x] P4-13:VSIX alpha / pre-release 打包与插件安装说明,已新增 `pnpm run vsix:alpha`,在 `target/vsix/` 生成可安装 pre-release VSIX 与 SHA-256 校验和,并在 `docs/release.md` 记录 clean 环境安装验收路径。 - [x] P4-14:补齐 end-to-end 集成测试覆盖,已在 `pnpm run vscode:test-electron` 中接入本地 JSON-RPC fixture server,覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume 和隔离 VS Code profile 启动;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的 P4-13 路径手动验收。 -- [x] P4-15:原生 VS Code Chat Participant `@prole`,让常规入口默认打开 VS Code Chat 侧栏体验;保留 Activity Bar Webview 作为 Run List / Context Capsule / diff 等高级面板。 -- [x] P4-16:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 Select Hunks 边界;持久化策略继续由后端策略控制,不在主弹窗里暴露复杂选项。 -- [x] P4-17:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 -- [x] P4-18:补齐单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 验收标准: @@ -694,12 +690,25 @@ extension.ts - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - 配置界面不保存 API Key,只管理非敏感配置。 +- `docs/phase-tasks.md` 的 Phase 4 条目已全部标记为 `[x]`,README 可以把 Phase 4 表述为整阶段完成。 + +### Phase 5:VS Code 插件 UX 体验优化 + +- [x] P5-1:原生 VS Code Chat Participant `@prole`,让常规入口默认打开 VS Code Chat 侧栏体验;保留 Activity Bar Webview 作为 Run List / Context Capsule / diff 等高级面板。 +- [x] P5-2:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 Select Hunks 边界;持久化策略继续由后端策略控制,不在主弹窗里暴露复杂选项。 +- [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 +- [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 +- [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 + +验收标准: + - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- `docs/phase-tasks.md` 的 Phase 4 条目已全部标记为 `[x]`,README 可以把 Phase 4 表述为整阶段完成。 +- 主审批弹窗保持简单;完整错误诊断可在 `Output > ProleCoder` 查看。 +- `docs/phase-tasks.md` 的 Phase 5 条目已全部标记为 `[x]`,README 可以把 Phase 5 表述为整阶段完成。 -### Phase 5:TUI 与生态扩展 +### Phase 6:TUI 与生态扩展 - [ ] TUI RPC 入口和事件流消费。 - [ ] Chat/Plan/Diff/Tools/Context/Settings 页面。 @@ -709,7 +718,7 @@ extension.ts - [ ] 更细的 replay 语义。 - [ ] MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具和审计包导出。 -### Phase 6:自由软件发布 +### Phase 7:自由软件发布 - [x] 确定许可证:AGPL-3.0-or-later 已作为项目许可证策略写入 README,后续发布阶段补齐正式 `LICENSE` 文件和源码提供说明。 - [ ] 发布 `LICENSE`、源码获取说明和网络服务源码提供说明。 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index cc30038..566bf39 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成;Phase 4 包含原 14 项 VS Code 深度集成能力以及 P4-15 到 P4-18 的 Codex-like UX 收敛。 +状态:Phase 1、Phase 2、Phase 3、Phase 4、Phase 5 已完成;Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 单独记录 VS Code 插件 UX 体验优化。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -100,19 +100,24 @@ | [x] | P4-5:RPC 高频事件输出节流与批量发送策略 | `docs/rpc-server.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:实时 live event wire 层支持 `agent.eventBatch` 批量发送,VS Code manager 按序分发;Run Log 仍逐事件写入并保持 `seq` 为事实来源,`agent.resume` replay 仍按单事件结构输出。 | | [x] | P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:新增 `RpcServerManager.cancel()` typed helper、Cancel 按钮和运行中 composer 状态;覆盖 typed cancel RPC 边界测试。 | | [x] | P4-7:Problems 面板诊断进入 Context Builder | `README.md`、`docs/vscode-extension.md`、`docs/context-capsule.md` | 已完成:VS Code 发送 turn 时采集 Problems 快照,并通过 `agent.sendTurn.attachments` 的 diagnostic attachment 注入;前端按协议 attachment 上限裁剪、优先保留 error,Core/Context Builder 继续负责 attachment 校验和 token 预算。 | -| [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P4-16 后 VS Code 主审批弹窗不再暴露复杂持久化选项。 | +| [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批弹窗不再暴露复杂持久化选项。 | | [x] | P4-9:审批持久化存储 | `docs/approval-model.md`、`docs/tool-system.md`、`docs/vscode-extension.md` | 已完成:RPC pending queue 支持 session 内存复用和 workspace `.prole-coder/approvals.v1.json` 存储;继续在 Core 和 RPC 层禁止 network/destructive 风险持久化。 | | [x] | P4-10:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 仍只由 RPC server 环境读取。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | P4-11:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code modal 提供 selected hunk quick pick;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | | [x] | P4-12:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | | [x] | P4-13:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | -| [x] | P4-15:原生 VS Code Chat Participant `@prole` | `README.md`、`docs/vscode-extension.md` | 已完成:贡献 `contributes.chatParticipants` 和 `vscode.chat.createChatParticipant`,让 `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 侧栏并填入 `@prole`;Activity Bar Webview 保留为高级状态面板。验收:`pnpm -r typecheck`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`。 | -| [x] | P4-16:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批弹窗收敛为 Approve / Reject;`apply_patch` 多 hunk 继续提供 Select Hunks quick pick;持久化批准能力保留在后端策略与 RPC 队列,不在主审批弹窗里暴露复杂选项。验收:`commands.test.ts` 覆盖简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | -| [x] | P4-17:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | -| [x] | P4-18:测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | +## Phase 5:VS Code 插件 UX 体验优化 -## Phase 5:TUI 与生态扩展 +| 状态 | 任务 | 来源 | 说明 | +| --- | --- | --- | --- | +| [x] | P5-1:原生 VS Code Chat Participant `@prole` | `README.md`、`docs/vscode-extension.md` | 已完成:贡献 `contributes.chatParticipants` 和 `vscode.chat.createChatParticipant`,让 `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 侧栏并填入 `@prole`;Activity Bar Webview 保留为高级状态面板。验收:`pnpm -r typecheck`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`。 | +| [x] | P5-2:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批弹窗收敛为 Approve / Reject;`apply_patch` 多 hunk 继续提供 Select Hunks quick pick;持久化批准能力保留在后端策略与 RPC 队列,不在主审批弹窗里暴露复杂选项。验收:`commands.test.ts` 覆盖简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | +| [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | +| [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | +| [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | + +## Phase 6:TUI 与生态扩展 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -123,7 +128,7 @@ | [ ] | 更细的 replay 语义 | `docs/rpc-server.md`、`docs/tool-system.md`、`docs/run-log.md` | 明确 resume 时哪些事件原样回放、哪些需要历史标记,并与 pending approval / hunk 审批状态保持一致。 | | [ ] | MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具、审计包导出 | `docs/roadmap.md` | 生态扩展应在核心闭环、编辑器体验和 DeepSeek 差异化稳定后推进。 | -## Phase 6:发布与治理 +## Phase 7:发布与治理 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | diff --git a/docs/release.md b/docs/release.md index f3bfcab..308f6a5 100644 --- a/docs/release.md +++ b/docs/release.md @@ -61,7 +61,7 @@ target/vsix/prole-coder-vscode-0.1.0-alpha.vsix.sha256 `vscode/extension/scripts/packageAlphaVsix.mjs` 使用 `@vscode/vsce` 的 `preRelease: true` 打包选项,保留 `package.json` 中的稳定版本号,并通过文件名中的 `alpha` 标识渠道。脚本会校验 VSIX manifest 中的 VS Code pre-release 标记、publisher/name/version 一致性、`onChatParticipant:prole-coder.chatParticipant` activation event 和 `@prole` Chat Participant 贡献点,并写出 SHA-256 校验和。产物位于被忽略的 `target/vsix/`,不提交到仓库。 -当前 alpha VSIX 用于本地安装和 clean 环境验收,不等同于 Marketplace / Open VSX 发布。正式对外发布前仍需在 Phase 6 补齐 `LICENSE` 文件、源码获取说明、发布 notes、公开 release checksum 和可复现构建说明。 +当前 alpha VSIX 用于本地安装和 clean 环境验收,不等同于 Marketplace / Open VSX 发布。正式对外发布前仍需在 Phase 7 补齐 `LICENSE` 文件、源码获取说明、发布 notes、公开 release checksum 和可复现构建说明。 ## VSIX clean 环境安装验收 diff --git a/docs/roadmap.md b/docs/roadmap.md index 9bd58a5..e8c8f40 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # 路线图 -状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列、Phase 4 VS Code 深度集成与 Codex-like UX 收敛均已完成。 +状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列、Phase 4 VS Code 深度集成和 Phase 5 VS Code 插件 UX 体验优化均已完成。 本文档把 README 中的大阶段拆成更可执行的优先级。README 保留项目入口和高层计划;这里记录跨模块的落地顺序、取舍和验收重点。具体任务的阶段、状态和来源统一登记在 `docs/phase-tasks.md`,阶段条目标记完成前应同步检查并更新该索引。 @@ -86,8 +86,8 @@ Phase 1 收官后优化池: P0 不追求: - 完整 VS Code Sidebar:已移入 Phase 3。 -- 完整 TUI:已移入 Phase 5。 -- VS Code/TUI 真实前端 UI 接入:Phase 3 优先 VS Code,Phase 5 再补齐 TUI。 +- 完整 TUI:已移入 Phase 6。 +- VS Code/TUI 真实前端 UI 接入:Phase 3 优先 VS Code,Phase 6 再补齐 TUI。 - MCP 生态。 - 多 provider UI。 - 大仓库 1M token 基准。 @@ -105,17 +105,17 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 18 项 VS Code 深度集成任务,在不推翻现有 Sidebar Chat 的前提下补齐 Codex-like 原生 Chat 入口、简化审批和自动上下文压缩;TUI 随后进入 Phase 5,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 已单独完成 Codex-like 原生 Chat 入口、简化审批、自动上下文压缩和 Output Channel 错误诊断。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 4 新增 P4-15 到 P4-18 的 Codex-like UX 收敛已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices 和自动上下文压缩 attachment。 +- Phase 5 新增 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment 和 Output Channel 错误诊断。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 - `agent.cancel` 类型化 helper 与 Chat Cancel UI 已接入,并与 Terminal approval 做轻量 composer UX review。 - Problems 面板诊断已通过 diagnostic attachments 进入 Context Builder,插件不新增独立 diagnostics 状态同步 RPC。 -- Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P4-16 后 VS Code 主审批弹窗保持 Approve / Reject。 +- Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P5-2 后 VS Code 主审批弹窗保持 Approve / Reject。 - 审批持久化存储已支持 session/workspace,继续禁止 network/destructive 风险持久化。 - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 @@ -131,7 +131,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 18 项 VS Code 已完成的验收重点: -- Phase 4 的 18 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`,README 可以写 Phase 4 全部完成。 +- Phase 4 的 14 个条目与 Phase 5 的 5 个 UX 条目已全部在 `docs/phase-tasks.md` 标记 `[x]`,README 可以分别写 Phase 4 和 Phase 5 全部完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/testing.md b/docs/testing.md index 2afc635..312ffde 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -69,10 +69,11 @@ pnpm run vscode:test-electron test-only command 和 auto approval 同时要求 VS Code `ExtensionMode.Test` 以及 `PROLE_CODER_VSCODE_TEST=1` / `PROLE_CODER_VSCODE_TEST_AUTO_APPROVE=1` 环境变量,普通扩展激活不会注册这些测试入口。 -P4-15 到 P4-18 的 Codex-like UX 收敛继续复用这条 extension-host 入口,并补齐以下确定性覆盖: +Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 extension-host 入口,并补齐以下确定性覆盖: - `automaticContext.test.ts` 覆盖历史对话压缩、字符预算裁剪、空历史跳过、Sidebar timeline 转换、单条 timeline 消息限长和 attachment 上限合并。 - `chatParticipantCore.test.ts` 覆盖原生 `@prole` Chat Participant turn runner、命令到 run mode 的映射、sendTurn response 前早到事件缓冲、assistant delta streaming、缺少 RPC client 的错误和自动上下文进度提示。 +- `logging.test.ts` 覆盖 `ProleCoder` Output Channel 日志格式与输出分发;`chatParticipantCore.test.ts` 还覆盖 RPC turn 失败写入 logger,确保完整错误可在 Output 面板诊断。 - `commands.test.ts` 覆盖简化后的审批 choices:主弹窗只暴露 `Approve` / `Reject`,`Approve` 映射一次性批准,`apply_patch` 多 hunk 走 `Select Hunks`。 - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 diff --git a/docs/tui.md b/docs/tui.md index 9d9be81..f607f74 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -1,6 +1,6 @@ # 终端界面(TUI) -状态:草案,保留为正式前端但优先级排在 VS Code 核心体验之后,当前归入 Phase 5。 +状态:草案,保留为正式前端但优先级排在 VS Code 核心体验之后,当前归入 Phase 6。 TUI 是 `ProleCoder` 的终端前端。它应支持键盘驱动的代码工作流,同时与 VS Code 插件共享 Agent Core 行为。 @@ -40,7 +40,7 @@ TUI 是 `ProleCoder` 的终端前端。它应支持键盘驱动的代码工作 下一步: -- 等 VS Code 核心体验和共享 RPC 事件管线稳定后,进入 TUI Phase 5 实现。 +- 等 VS Code 核心体验和共享 RPC 事件管线稳定后,进入 TUI Phase 6 实现。 - 为 `prole-coder-tui` 增加命令行入口和配置加载。 - 连接 Rust Agent RPC Server 的 stdio request loop,消费 `agent.event` 事件流。 - 把 `ApprovalPromptModel` 接到真实 `tool.approvalRequired`,并把用户决定发送为 `agent.approve` / `agent.reject`。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index e6cb73f..31a7024 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成,包含原 14 项深度集成能力以及 P4-15 到 P4-18 的 Codex-like 原生 Chat UX 收敛。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX 和自动上下文压缩均已实现。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code 插件 UX 体验优化已完成 Codex-like 原生 Chat UX 与诊断收敛。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩和 `ProleCoder` Output Channel 错误诊断均已实现。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -48,6 +48,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server - 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item。 - 同一 run/turn 的连续 `assistant.delta` 会合并为一条 assistant 消息,避免流式输出刷屏。 - 提供 prompt 输入、mode 选择和运行中 Cancel 按钮;通过 Webview `submitTurn` 消息调用 typed `RpcServerManager.sendTurn()`,发送时把 Problems 快照转换为 diagnostic attachments,并按协议 attachment 上限优先保留 error;accepted 后等待同一 run 的 terminal event 收口输入状态,Cancel 会调用 typed `RpcServerManager.cancel()`。 +- 失败状态会在 Sidebar Chat 中显示短消息,并把 sendTurn、Run List refresh/resume/cancel、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误写入 VS Code `Output > ProleCoder`。 `vscode/extension/src/commands.ts` 还提供 `requestApproval`: @@ -125,17 +126,20 @@ Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: 5. P4-5:RPC 高频事件输出节流与批量发送策略,已完成:实时 live event 支持 `agent.eventBatch`,保持 Run Log `seq` 与 replay 语义稳定。 6. P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已完成:`RpcServerManager.cancel()` 和 Sidebar Chat Cancel 按钮接入真实 RPC。 7. P4-7:通过 diagnostic attachments 读取 Problems 面板诊断并交给 Agent Core,已完成:发送 turn 时采集 Problems 快照,并按协议 attachment 上限裁剪。 -8. P4-8:Terminal command approval 展示命令、cwd、风险等级、上一条 shell 输出摘要和持久化语义,已完成:shared protocol payload 和后端策略已支持;P4-16 后主审批弹窗不再暴露持久化选项。 +8. P4-8:Terminal command approval 展示命令、cwd、风险等级、上一条 shell 输出摘要和持久化语义,已完成:shared protocol payload 和后端策略已支持;P5-2 后主审批弹窗不再暴露持久化选项。 9. P4-9:审批持久化存储,已完成:RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 10. P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已完成:Open Settings 命令展示 `agent.initialize` 返回的 capability data、RPC command/state 和 API Key 不落 VS Code settings 的边界。 11. P4-11:真实 hunk 级 patch 审批,已完成:`apply_patch` 可选择 hunks,RPC/Core 校验 hunk id 并只应用已批准 hunks,审批事件 payload 已同步 fixture。 12. P4-12:FIM completion preview,已完成:VS Code 原生 inline completion 通过 `agent.previewFim` 获取 preview,模型选择只依赖 server capability。 13. P4-13:VSIX alpha / pre-release 打包与安装说明,已完成:`pnpm run vsix:alpha` 会生成 `target/vsix/prole-coder-vscode-0.1.0-alpha.vsix` 和 `.sha256` 校验和,VSIX manifest 标记为 VS Code pre-release;`docs/release.md` 记录 clean user-data/extensions 目录下的安装验收步骤。 14. P4-14:补齐 end-to-end 集成测试覆盖,已完成:`pnpm run vscode:test-electron` 使用本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume,并使用隔离 VS Code profile 避免本机状态影响测试;VSIX 安装后基础交互按 `docs/release.md` 的 clean 环境路径验收。 -15. P4-15:原生 Chat 入口,已完成:贡献 `@prole` Chat Participant,并让 `ProleCoder: Open Chat` 优先打开 VS Code Chat 侧栏;普通 Activity Bar Webview 继续承载 Run List、Context Capsule 和更详细事件视图。 -16. P4-16:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 Select Hunks;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主弹窗。 -17. P4-17:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 -18. P4-18:测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 +Phase 5 UX 体验优化清单与 `docs/phase-tasks.md` 对齐: + +1. P5-1:原生 Chat 入口,已完成:贡献 `@prole` Chat Participant,并让 `ProleCoder: Open Chat` 优先打开 VS Code Chat 侧栏;普通 Activity Bar Webview 继续承载 Run List、Context Capsule 和更详细事件视图。 +2. P5-2:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 Select Hunks;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主弹窗。 +3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 +4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 +5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 6f485c691863358b2d73f822a2c56a176c86a593 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 08:47:19 +0800 Subject: [PATCH 03/84] Adds ProleCoder Output Channel for comprehensive error diagnostics --- vscode/extension/src/chatParticipant.ts | 3 + vscode/extension/src/chatParticipantCore.ts | 6 +- vscode/extension/src/chatView.ts | 20 +++++-- vscode/extension/src/extension.ts | 64 ++++++++++++++------- vscode/extension/src/logging.ts | 32 +++++++++++ 5 files changed, 98 insertions(+), 27 deletions(-) create mode 100644 vscode/extension/src/logging.ts diff --git a/vscode/extension/src/chatParticipant.ts b/vscode/extension/src/chatParticipant.ts index fc7e562..49e8f53 100644 --- a/vscode/extension/src/chatParticipant.ts +++ b/vscode/extension/src/chatParticipant.ts @@ -15,11 +15,13 @@ import { type ChatParticipantRpcClient, } from "./chatParticipantCore"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; +import type { ProleLogger } from "./logging"; export function registerProleChatParticipant( context: vscode.ExtensionContext, rpcClient: ChatParticipantRpcClient | undefined, workspaceRoot: string | undefined, + logger?: ProleLogger, ): vscode.Disposable { const participant = vscode.chat.createChatParticipant( CHAT_PARTICIPANT_ID, @@ -37,6 +39,7 @@ export function registerProleChatParticipant( ...(attachments.length === 0 ? {} : { attachments }), }, response, + ...(logger === undefined ? {} : { logger }), token, }); }, diff --git a/vscode/extension/src/chatParticipantCore.ts b/vscode/extension/src/chatParticipantCore.ts index 3b17845..e6862f4 100644 --- a/vscode/extension/src/chatParticipantCore.ts +++ b/vscode/extension/src/chatParticipantCore.ts @@ -13,6 +13,7 @@ import { type ConversationContextMessage, } from "./automaticContext"; import { DEFAULT_CHAT_MODE, sendTurnParams } from "./chatInput"; +import type { ProleLogger } from "./logging"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; export const CHAT_PARTICIPANT_ID = "prole-coder.chatParticipant"; @@ -53,6 +54,7 @@ export interface ChatParticipantTurnOptions { readonly rpcClient?: ChatParticipantRpcClient; readonly request: ChatParticipantTurnRequest; readonly response: ChatParticipantResponseStream; + readonly logger?: ProleLogger; readonly token?: CancellationTokenLike; } @@ -151,7 +153,9 @@ export async function runChatParticipantTurn( return await terminalPromise; } catch (error) { - return errorResult(`ProleCoder turn failed: ${errorMessage(error)}`, runId); + const message = `ProleCoder turn failed: ${errorMessage(error)}`; + options.logger?.error(message); + return errorResult(message, runId); } finally { eventSubscription.dispose(); cancellationSubscription?.dispose(); diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index fdb51cd..8c73772 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -26,6 +26,7 @@ import { type ContextVizSnapshot, } from "./contextViz"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; +import type { ProleLogger } from "./logging"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; import { RUN_LIST_LIMIT, @@ -126,6 +127,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private readonly extensionUri: vscode.Uri, rpcClient?: ChatRpcClient, private readonly workspaceRoot?: string, + private readonly logger?: ProleLogger, ) { this.rpcClient = rpcClient; this.rpcSubscription = rpcClient?.onEvent((event) => { @@ -276,6 +278,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos ); } catch (error) { const messageText = `Failed to send turn: ${errorMessage(error)}`; + this.logger?.error(messageText); this.setSubmission({ ...idleSubmission(), status: "failed", @@ -345,7 +348,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos const result = await this.rpcClient.listRuns({ limit: RUN_LIST_LIMIT }); this.setRunList(readyRunList(result, this.runList.selectedRunId)); } catch (error) { - this.setRunList(failedRunList(`Failed to load runs: ${errorMessage(error)}`, this.runList)); + const messageText = `Failed to load runs: ${errorMessage(error)}`; + this.logger?.error(messageText); + this.setRunList(failedRunList(messageText, this.runList)); } } @@ -374,7 +379,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos : `No events to replay for ${result.runId}.`; this.setRunList(readyRunList({ runs: this.runList.runs }, result.runId, message)); } catch (error) { - this.setRunList(failedRunList(`Failed to resume run: ${errorMessage(error)}`, this.runList)); + const messageText = `Failed to resume run: ${errorMessage(error)}`; + this.logger?.error(messageText); + this.setRunList(failedRunList(messageText, this.runList)); } } @@ -415,6 +422,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos }); } catch (error) { const messageText = `Failed to cancel turn: ${errorMessage(error)}`; + this.logger?.error(messageText); this.setSubmission({ ...this.submission, message: messageText, @@ -1126,7 +1134,9 @@ function renderChatViewHtml( const loading = status === "loading"; refreshRunsButton.disabled = loading; runMessageRoot.className = "run-message " + status; - runMessageRoot.textContent = typeof state.message === "string" ? state.message : ""; + const runMessage = typeof state.message === "string" ? state.message : ""; + runMessageRoot.textContent = runMessage; + runMessageRoot.title = runMessage; runListRoot.replaceChildren(); if (runs.length === 0) { @@ -1423,7 +1433,9 @@ function renderChatViewHtml( setComposerBusy(busy, cancelable); cancelButton.dataset.runId = runId; submissionRoot.className = "submission " + status; - submissionRoot.textContent = typeof state.message === "string" ? state.message : ""; + const submissionMessage = typeof state.message === "string" ? state.message : ""; + submissionRoot.textContent = submissionMessage; + submissionRoot.title = typeof state.error === "string" ? state.error : submissionMessage; if (status === "running") { promptInput.value = ""; } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 03696a1..5f583fc 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -10,13 +10,19 @@ import { } from "./commands"; import { createPatchDiffPreviewController } from "./diffPreview"; import { registerFimInlineCompletionProvider } from "./fimPreviewVscode"; +import { createOutputLogger, type ProleLogger } from "./logging"; import { RpcServerManager, readRpcServerLaunchConfig } from "./rpcServer"; export function activate(context: vscode.ExtensionContext): void { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; - const rpcServer = createRpcServerManager(context); - const chatView = new ProleChatViewProvider(context.extensionUri, rpcServer, workspaceRoot); - const chatParticipant = registerProleChatParticipant(context, rpcServer, workspaceRoot); + const outputChannel = vscode.window.createOutputChannel("ProleCoder"); + const logger = createOutputLogger(outputChannel); + const notifier = createExtensionNotifier(logger); + context.subscriptions.push(outputChannel); + + const rpcServer = createRpcServerManager(context, notifier); + const chatView = new ProleChatViewProvider(context.extensionUri, rpcServer, workspaceRoot, logger); + const chatParticipant = registerProleChatParticipant(context, rpcServer, workspaceRoot, logger); const openChat = registerOpenChatCommand( vscode.commands, vscode.window, @@ -27,10 +33,10 @@ export function activate(context: vscode.ExtensionContext): void { vscode.commands, { showInformationMessage(message) { - return vscode.window.showInformationMessage(message); + return notifier.info(message); }, showWarningMessage(message) { - return vscode.window.showWarningMessage(message); + return notifier.warn(message); }, openSettings(query) { return vscode.commands.executeCommand("workbench.action.openSettings", query); @@ -51,11 +57,7 @@ export function activate(context: vscode.ExtensionContext): void { const approvalController = new ApprovalEventController( rpcServer, vscode.window, - { - warn(message) { - return vscode.window.showWarningMessage(message); - }, - }, + notifier, testApprovalRequester(context), patchDiffPreviewController, ); @@ -64,9 +66,8 @@ export function activate(context: vscode.ExtensionContext): void { context.subscriptions.push(rpcServer); if (rpcServer.autoStart) { void rpcServer.start().catch((error: unknown) => { - void vscode.window.showWarningMessage( - `prole-coder RPC server failed to start: ${errorMessage(error)}`, - ); + const message = `prole-coder RPC server failed to start: ${errorMessage(error)}`; + void notifier.error(message); }); } } @@ -76,7 +77,10 @@ export function deactivate(): void { // VS Code disposes context subscriptions, including the RPC server manager. } -function createRpcServerManager(context: vscode.ExtensionContext): RpcServerManager | undefined { +function createRpcServerManager( + context: vscode.ExtensionContext, + notifier: ExtensionNotifier, +): RpcServerManager | undefined { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (workspaceRoot === undefined) { return undefined; @@ -89,17 +93,33 @@ function createRpcServerManager(context: vscode.ExtensionContext): RpcServerMana trusted: vscode.workspace.isTrusted, }, extensionVersion: extensionVersion(context), - notifier: { - info(message) { - return vscode.window.showInformationMessage(message); - }, - warn(message) { - return vscode.window.showWarningMessage(message); - }, - }, + notifier, }); } +interface ExtensionNotifier { + info(message: string): unknown; + warn(message: string): unknown; + error(message: string): unknown; +} + +function createExtensionNotifier(logger: ProleLogger): ExtensionNotifier { + return { + info(message) { + logger.info(message); + return vscode.window.showInformationMessage(message); + }, + warn(message) { + logger.warn(message); + return vscode.window.showWarningMessage(message); + }, + error(message) { + logger.error(message); + return vscode.window.showWarningMessage(message); + }, + }; +} + function extensionVersion(context: vscode.ExtensionContext): string { const packageJson = context.extension.packageJSON as { version?: unknown }; return typeof packageJson.version === "string" ? packageJson.version : "0.1.0"; diff --git a/vscode/extension/src/logging.ts b/vscode/extension/src/logging.ts new file mode 100644 index 0000000..ee1161a --- /dev/null +++ b/vscode/extension/src/logging.ts @@ -0,0 +1,32 @@ +export type ProleLogLevel = "error" | "info" | "warn"; + +export interface ProleLogger { + error(message: string): void; + info(message: string): void; + warn(message: string): void; +} + +export interface OutputAppender { + appendLine(value: string): unknown; +} + +export function createOutputLogger( + output: OutputAppender, + now: () => Date = () => new Date(), +): ProleLogger { + return { + error(message) { + output.appendLine(formatLogLine("error", message, now())); + }, + info(message) { + output.appendLine(formatLogLine("info", message, now())); + }, + warn(message) { + output.appendLine(formatLogLine("warn", message, now())); + }, + }; +} + +export function formatLogLine(level: ProleLogLevel, message: string, time: Date): string { + return `[${time.toISOString()}] ${level.toUpperCase()} ${message}`; +} From fdeec9454a5afdd0885fdc1a0160de9f10580b51 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 08:47:19 +0800 Subject: [PATCH 04/84] Adds unit tests for Output Channel logging and RPC failure logging --- .../test/chatParticipantCore.test.ts | 52 ++++++++++++++++++- vscode/extension/test/logging.test.ts | 37 +++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 vscode/extension/test/logging.test.ts diff --git a/vscode/extension/test/chatParticipantCore.test.ts b/vscode/extension/test/chatParticipantCore.test.ts index 9d78d9b..24550b6 100644 --- a/vscode/extension/test/chatParticipantCore.test.ts +++ b/vscode/extension/test/chatParticipantCore.test.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; -import type { SendTurnParams } from "@prole-coder/protocol" with { +import type { SendTurnParams, SendTurnResult } from "@prole-coder/protocol" with { "resolution-mode": "import", }; @@ -113,6 +113,25 @@ test("runChatParticipantTurn handles terminal events buffered before sendTurn re }); }); +test("runChatParticipantTurn logs RPC failures before returning chat errors", async () => { + const rpc = new RejectingChatParticipantRpcClient( + new Error("DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required"), + ); + const response = new FakeChatResponseStream(); + const logger = new FakeLogger(); + const result = await runChatParticipantTurn({ + rpcClient: rpc, + request: { + prompt: "hello", + }, + response, + logger, + }); + + assert.ok(result.errorDetails?.message.includes("DEEPSEEK_API_KEY is required")); + assert.deepEqual(logger.errors, [result.errorDetails?.message]); +}); + test("runChatParticipantTurn returns chat errors without an RPC client", async () => { const response = new FakeChatResponseStream(); const result = await runChatParticipantTurn({ @@ -168,7 +187,7 @@ class FakeChatParticipantRpcClient implements ChatParticipantRpcClient { } class EarlyTerminalChatParticipantRpcClient extends FakeChatParticipantRpcClient { - override async sendTurn(params: SendTurnParams) { + override async sendTurn(params: SendTurnParams): Promise { this.sendTurns.push(params); this.emit(agentEvent(1, "assistant.delta", { text: "early" })); this.emit(agentEvent(2, "run.completed", { summary: "done" })); @@ -180,6 +199,17 @@ class EarlyTerminalChatParticipantRpcClient extends FakeChatParticipantRpcClient } } +class RejectingChatParticipantRpcClient extends FakeChatParticipantRpcClient { + constructor(private readonly error: Error) { + super(); + } + + override async sendTurn(params: SendTurnParams): Promise { + this.sendTurns.push(params); + throw this.error; + } +} + class FakeChatResponseStream implements ChatParticipantResponseStream { readonly markdownParts: string[] = []; readonly progressParts: string[] = []; @@ -193,6 +223,24 @@ class FakeChatResponseStream implements ChatParticipantResponseStream { } } +class FakeLogger { + readonly errors: string[] = []; + readonly infos: string[] = []; + readonly warnings: string[] = []; + + error(message: string): void { + this.errors.push(message); + } + + info(message: string): void { + this.infos.push(message); + } + + warn(message: string): void { + this.warnings.push(message); + } +} + function agentEvent(seq: number, type: string, payload: unknown): AgentEventEnvelope { return { seq, diff --git a/vscode/extension/test/logging.test.ts b/vscode/extension/test/logging.test.ts new file mode 100644 index 0000000..3bfbd15 --- /dev/null +++ b/vscode/extension/test/logging.test.ts @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createOutputLogger, formatLogLine } from "../src/logging.js"; + +test("formatLogLine includes timestamp, level, and full message", () => { + assert.equal( + formatLogLine( + "error", + "Failed to send turn: DeepSeek provider configuration failed.", + new Date("2026-05-31T00:00:00.000Z"), + ), + "[2026-05-31T00:00:00.000Z] ERROR Failed to send turn: DeepSeek provider configuration failed.", + ); +}); + +test("createOutputLogger appends all log levels to the output sink", () => { + const lines: string[] = []; + const logger = createOutputLogger( + { + appendLine(value) { + lines.push(value); + }, + }, + () => new Date("2026-05-31T00:00:00.000Z"), + ); + + logger.info("RPC initialized."); + logger.warn("RPC stderr preview."); + logger.error("Failed to load runs."); + + assert.deepEqual(lines, [ + "[2026-05-31T00:00:00.000Z] INFO RPC initialized.", + "[2026-05-31T00:00:00.000Z] WARN RPC stderr preview.", + "[2026-05-31T00:00:00.000Z] ERROR Failed to load runs.", + ]); +}); From b7a14adba590519cddae58406c15f46f8843067e Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 14:13:32 +0800 Subject: [PATCH 05/84] Updates Phase 5 status and adds remaining UX workflow tasks --- README.md | 12 +++++++++--- docs/phase-tasks.md | 12 ++++++++++-- docs/roadmap.md | 11 ++++++----- docs/testing.md | 10 ++++++++++ docs/vscode-extension.md | 10 ++++++++-- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8f0fe4b..a94044b 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线、Phase 4 的 VS Code 深度集成以及 Phase 5 的 VS Code 插件 UX 体验优化均已完成。Phase 4 保留 14 项深度集成能力;Phase 5 单独收敛 Codex-like UX 与诊断体验:默认使用 VS Code 原生 Chat 右侧入口、简化审批 UX、保持连续会话心智、自动压缩历史上下文,并把完整错误写入 `ProleCoder` Output Channel。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX 和 Output Channel 错误诊断均已完成。之后进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流正在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 的 P5-1 到 P5-5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收和 `ProleCoder` Output Channel 错误诊断。接下来 Phase 5 继续补齐插件内 SecretStorage 配置 DeepSeek API key、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX 和 Output Channel 错误诊断均已完成。Phase 5 全部完成后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -692,13 +692,19 @@ extension.ts - 配置界面不保存 API Key,只管理非敏感配置。 - `docs/phase-tasks.md` 的 Phase 4 条目已全部标记为 `[x]`,README 可以把 Phase 4 表述为整阶段完成。 -### Phase 5:VS Code 插件 UX 体验优化 +### Phase 5:VS Code Codex-like UX 与开发工作流 - [x] P5-1:原生 VS Code Chat Participant `@prole`,让常规入口默认打开 VS Code Chat 侧栏体验;保留 Activity Bar Webview 作为 Run List / Context Capsule / diff 等高级面板。 - [x] P5-2:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 Select Hunks 边界;持久化策略继续由后端策略控制,不在主弹窗里暴露复杂选项。 - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 +- [ ] P5-6:DeepSeek API key SecretStorage 与 provider status:插件提供 Configure/Clear API Key 和 provider status 入口;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 +- [ ] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;配置/清除 key 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 +- [ ] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream、status、staged/unstaged diff 和 commit summary;git CLI 只作受控 fallback,cwd 必须来自 repository root。 +- [ ] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 +- [ ] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit/test summary,生成 PR title/body markdown;首版只预览/复制/打开入口,不自动创建 PR。 +- [ ] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 验收标准: @@ -706,7 +712,7 @@ extension.ts - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 - 主审批弹窗保持简单;完整错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 条目已全部标记为 `[x]`,README 可以把 Phase 5 表述为整阶段完成。 +- `docs/phase-tasks.md` 的 Phase 5 条目尚未全部标记为 `[x]`,README 不得把 Phase 5 表述为整阶段完成。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 566bf39..f95732b 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4、Phase 5 已完成;Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 单独记录 VS Code 插件 UX 体验优化。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成;Phase 5 正在进行中。Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 记录 VS Code Codex-like UX 与开发工作流。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -107,7 +107,9 @@ | [x] | P4-12:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | | [x] | P4-13:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | -## Phase 5:VS Code 插件 UX 体验优化 +## Phase 5:VS Code Codex-like UX 与开发工作流 + +状态:进行中。P5-1 到 P5-5 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收和 Output Channel;P5-6 起根据第十轮讨论补齐 API key 配置、错误恢复和 GitLens-like commit / PR 文案生成工作流。Phase 5 只有本节全部条目都标记 `[x]` 后,README 才能写整阶段完成。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -116,6 +118,12 @@ | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | +| [ ] | P5-6:DeepSeek API key SecretStorage 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / provider status 入口;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`,不改变 CLI env 路径。 | +| [ ] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 计划:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;API key 配置/清除后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | +| [ ] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 计划:优先使用 VS Code Git API 采集 repository、branch/upstream、staged/unstaged diff、status 和 commit summary;git CLI 仅作受控 fallback,cwd 必须来自 repository root;大 diff 通过 Context Capsule 预算和 omitted source 可观察。 | +| [ ] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | +| [ ] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary/test summary,生成 PR title/body markdown;首版提供预览/复制/打开入口,不自动创建 PR。 | +| [ ] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 计划:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index e8c8f40..08dc5bf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # 路线图 -状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列、Phase 4 VS Code 深度集成和 Phase 5 VS Code 插件 UX 体验优化均已完成。 +状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列和 Phase 4 VS Code 深度集成已完成;Phase 5 VS Code Codex-like UX 与开发工作流正在进行中。 本文档把 README 中的大阶段拆成更可执行的优先级。README 保留项目入口和高层计划;这里记录跨模块的落地顺序、取舍和验收重点。具体任务的阶段、状态和来源统一登记在 `docs/phase-tasks.md`,阶段条目标记完成前应同步检查并更新该索引。 @@ -105,12 +105,13 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 已单独完成 Codex-like 原生 Chat 入口、简化审批、自动上下文压缩和 Output Channel 错误诊断。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 正在推进 VS Code Codex-like UX 与开发工作流。P5-1 到 P5-5 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收和 Output Channel 错误诊断;P5-6 起继续补齐插件内 API key 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 新增 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment 和 Output Channel 错误诊断。 +- Phase 5 的 P5-1 到 P5-5 Codex-like UX 与诊断收敛已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment 和 Output Channel 错误诊断。 +- Phase 5 的 P5-6 到 P5-11 正在规划/推进:DeepSeek API key SecretStorage/provider status、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;G4 自动 commit / push / create PR 暂不纳入首版完成口径。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 - `agent.cancel` 类型化 helper 与 Chat Cancel UI 已接入,并与 Terminal approval 做轻量 composer UX review。 @@ -129,9 +130,9 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - Context Capsule 可视化已完成:Sidebar Chat 消费 `context.built` metadata,展示三层 token 分布、来源纳入/省略、manifest、cache 和 estimator 摘要。 - Phase 3 命令风险分类器已完成:识别网络访问、依赖安装、远程 git、发布和破坏性命令,并在审批前升级风险。 -已完成的验收重点: +当前验收重点: -- Phase 4 的 14 个条目与 Phase 5 的 5 个 UX 条目已全部在 `docs/phase-tasks.md` 标记 `[x]`,README 可以分别写 Phase 4 和 Phase 5 全部完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-6 到 P5-11 未完成,README 不得把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/testing.md b/docs/testing.md index 312ffde..79b8e73 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -74,10 +74,20 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `automaticContext.test.ts` 覆盖历史对话压缩、字符预算裁剪、空历史跳过、Sidebar timeline 转换、单条 timeline 消息限长和 attachment 上限合并。 - `chatParticipantCore.test.ts` 覆盖原生 `@prole` Chat Participant turn runner、命令到 run mode 的映射、sendTurn response 前早到事件缓冲、assistant delta streaming、缺少 RPC client 的错误和自动上下文进度提示。 - `logging.test.ts` 覆盖 `ProleCoder` Output Channel 日志格式与输出分发;`chatParticipantCore.test.ts` 还覆盖 RPC turn 失败写入 logger,确保完整错误可在 Output 面板诊断。 + - `commands.test.ts` 覆盖简化后的审批 choices:主弹窗只暴露 `Approve` / `Reject`,`Approve` 映射一次性批准,`apply_patch` 多 hunk 走 `Select Hunks`。 - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 +Phase 5 P5-6 到 P5-11 的 API key 与 Git 工作流需要继续补齐以下确定性覆盖: + +- provider secret 纯逻辑测试:SecretStorage 优先、process env fallback、missing status、child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 +- notifier/redaction 测试:Output Channel、toast message 和 RPC startup failure 都必须统一脱敏 SecretStorage/env key。 +- API key 配置 UX 测试:Configure/Clear 命令、provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 +- Git context 测试:无仓库、多仓库、staged diff、unstaged fallback、base branch 选择和大 diff attachment 预算/omitted source。 +- commit/PR 生成测试:Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit;Generate PR Description 输出 markdown 且不自动创建 PR。 +- 文档/打包验收:P5-6 到 P5-10 完成前继续运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 + ## 新增测试的协作要求 - PR 或提交说明中标明测试类型:unit、integration、regression、live、demo 或 stress。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 31a7024..e67e3aa 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code 插件 UX 体验优化已完成 Codex-like 原生 Chat UX 与诊断收敛。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩和 `ProleCoder` Output Channel 错误诊断均已实现。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流正在进行中,P5-1 到 P5-5 的原生 Chat UX 与诊断收敛已完成,P5-6 起继续补齐 API key 配置、错误恢复和 Git 工作流。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩和 `ProleCoder` Output Channel 错误诊断均已实现。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -133,13 +133,19 @@ Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: 12. P4-12:FIM completion preview,已完成:VS Code 原生 inline completion 通过 `agent.previewFim` 获取 preview,模型选择只依赖 server capability。 13. P4-13:VSIX alpha / pre-release 打包与安装说明,已完成:`pnpm run vsix:alpha` 会生成 `target/vsix/prole-coder-vscode-0.1.0-alpha.vsix` 和 `.sha256` 校验和,VSIX manifest 标记为 VS Code pre-release;`docs/release.md` 记录 clean user-data/extensions 目录下的安装验收步骤。 14. P4-14:补齐 end-to-end 集成测试覆盖,已完成:`pnpm run vscode:test-electron` 使用本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume,并使用隔离 VS Code profile 避免本机状态影响测试;VSIX 安装后基础交互按 `docs/release.md` 的 clean 环境路径验收。 -Phase 5 UX 体验优化清单与 `docs/phase-tasks.md` 对齐: +Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐: 1. P5-1:原生 Chat 入口,已完成:贡献 `@prole` Chat Participant,并让 `ProleCoder: Open Chat` 优先打开 VS Code Chat 侧栏;普通 Activity Bar Webview 继续承载 Run List、Context Capsule 和更详细事件视图。 2. P5-2:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 Select Hunks;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主弹窗。 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 +6. P5-6:DeepSeek API key SecretStorage 与 provider status,计划:插件内配置 key,SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 +7. P5-7:统一 redaction 与 API key 错误恢复 UX,计划:notifier/logger 统一脱敏 SecretStorage/env key,API key 配置后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 +8. P5-8:Git context 只读采集与大 diff attachment 管线,计划:优先使用 VS Code Git API,git CLI 仅作受控 fallback,Context Capsule 负责大 diff 预算与 omitted source 可观察性。 +9. P5-9:Generate Commit Message,计划:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit。 +10. P5-10:Generate PR Description,计划:根据 base branch、diff/stat、commit summary 和 test summary 生成 PR title/body markdown,不自动创建 PR。 +11. P5-11:Phase 5 UX 工作流验收,计划:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 477a2966749562f160d143c334d2569f4522738a Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:41 +0800 Subject: [PATCH 06/84] Marks Phase 5 as complete across all documentation --- README.md | 16 ++++++++-------- docs/phase-tasks.md | 16 ++++++++-------- docs/roadmap.md | 10 +++++----- docs/vscode-extension.md | 14 +++++++------- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index a94044b..0703570 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流正在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 的 P5-1 到 P5-5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收和 `ProleCoder` Output Channel 错误诊断。接下来 Phase 5 继续补齐插件内 SecretStorage 配置 DeepSeek API key、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX 和 Output Channel 错误诊断均已完成。Phase 5 全部完成后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线、Phase 4 的 VS Code 深度集成以及 Phase 5:VS Code Codex-like UX 与开发工作流均已完成。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 配置 DeepSeek API key、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key 配置和 Git 工作流均已完成。下一步进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -699,12 +699,12 @@ extension.ts - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 -- [ ] P5-6:DeepSeek API key SecretStorage 与 provider status:插件提供 Configure/Clear API Key 和 provider status 入口;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 -- [ ] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;配置/清除 key 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 -- [ ] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream、status、staged/unstaged diff 和 commit summary;git CLI 只作受控 fallback,cwd 必须来自 repository root。 -- [ ] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 -- [ ] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit/test summary,生成 PR title/body markdown;首版只预览/复制/打开入口,不自动创建 PR。 -- [ ] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 +- [x] P5-6:DeepSeek API key SecretStorage 与 provider status:插件提供 Configure/Clear API Key 和 provider status 入口;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 +- [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;配置/清除 key 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 +- [x] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream 和 staged diff;git CLI 只作受控 fallback 采集 unstaged diff、diff stat、branch diff、commit summary 和 ref 检测,cwd 来自 repository root。 +- [x] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 +- [x] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版只提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 +- [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 验收标准: @@ -712,7 +712,7 @@ extension.ts - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 - 主审批弹窗保持简单;完整错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 条目尚未全部标记为 `[x]`,README 不得把 Phase 5 表述为整阶段完成。 +- `docs/phase-tasks.md` 的 Phase 5 条目已全部标记为 `[x]`,README 可把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index f95732b..0897a8b 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成;Phase 5 正在进行中。Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 记录 VS Code Codex-like UX 与开发工作流。 +状态:Phase 1、Phase 2、Phase 3、Phase 4、Phase 5 已完成。Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 记录 VS Code Codex-like UX 与开发工作流;下一阶段进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -109,7 +109,7 @@ | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-5 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收和 Output Channel;P5-6 起根据第十轮讨论补齐 API key 配置、错误恢复和 GitLens-like commit / PR 文案生成工作流。Phase 5 只有本节全部条目都标记 `[x]` 后,README 才能写整阶段完成。 +状态:已完成。P5-1 到 P5-11 已全部完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key 配置、错误恢复、只读 Git context 和 GitLens-like commit / PR 文案生成工作流。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -118,12 +118,12 @@ | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | -| [ ] | P5-6:DeepSeek API key SecretStorage 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / provider status 入口;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`,不改变 CLI env 路径。 | -| [ ] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 计划:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;API key 配置/清除后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | -| [ ] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 计划:优先使用 VS Code Git API 采集 repository、branch/upstream、staged/unstaged diff、status 和 commit summary;git CLI 仅作受控 fallback,cwd 必须来自 repository root;大 diff 通过 Context Capsule 预算和 omitted source 可观察。 | -| [ ] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | -| [ ] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 计划:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary/test summary,生成 PR title/body markdown;首版提供预览/复制/打开入口,不自动创建 PR。 | -| [ ] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 计划:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。 | +| [x] | P5-6:DeepSeek API key SecretStorage 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Show Provider Status` 命令;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`,不改变 CLI env 路径。 | +| [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;API key 配置/清除后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | +| [x] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | +| [x] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | +| [x] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | +| [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 08dc5bf..83ccde0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # 路线图 -状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列和 Phase 4 VS Code 深度集成已完成;Phase 5 VS Code Codex-like UX 与开发工作流正在进行中。 +状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列、Phase 4 VS Code 深度集成和 Phase 5 VS Code Codex-like UX 与开发工作流已完成。 本文档把 README 中的大阶段拆成更可执行的优先级。README 保留项目入口和高层计划;这里记录跨模块的落地顺序、取舍和验收重点。具体任务的阶段、状态和来源统一登记在 `docs/phase-tasks.md`,阶段条目标记完成前应同步检查并更新该索引。 @@ -105,13 +105,13 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 正在推进 VS Code Codex-like UX 与开发工作流。P5-1 到 P5-5 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收和 Output Channel 错误诊断;P5-6 起继续补齐插件内 API key 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 已完成 VS Code Codex-like UX 与开发工作流。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内 API key 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-5 Codex-like UX 与诊断收敛已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment 和 Output Channel 错误诊断。 -- Phase 5 的 P5-6 到 P5-11 正在规划/推进:DeepSeek API key SecretStorage/provider status、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;G4 自动 commit / push / create PR 暂不纳入首版完成口径。 +- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage/provider status、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛。 +- G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 - `agent.cancel` 类型化 helper 与 Chat Cancel UI 已接入,并与 Terminal approval 做轻量 composer UX review。 @@ -132,7 +132,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-6 到 P5-11 未完成,README 不得把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目和 Phase 5 的 11 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;README 可把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index e67e3aa..dc23d86 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流正在进行中,P5-1 到 P5-5 的原生 Chat UX 与诊断收敛已完成,P5-6 起继续补齐 API key 配置、错误恢复和 Git 工作流。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩和 `ProleCoder` Output Channel 错误诊断均已实现。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流已完成。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key 配置、统一 redaction 和 Git 工作流均已实现。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -140,12 +140,12 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 -6. P5-6:DeepSeek API key SecretStorage 与 provider status,计划:插件内配置 key,SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 -7. P5-7:统一 redaction 与 API key 错误恢复 UX,计划:notifier/logger 统一脱敏 SecretStorage/env key,API key 配置后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 -8. P5-8:Git context 只读采集与大 diff attachment 管线,计划:优先使用 VS Code Git API,git CLI 仅作受控 fallback,Context Capsule 负责大 diff 预算与 omitted source 可观察性。 -9. P5-9:Generate Commit Message,计划:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit。 -10. P5-10:Generate PR Description,计划:根据 base branch、diff/stat、commit summary 和 test summary 生成 PR title/body markdown,不自动创建 PR。 -11. P5-11:Phase 5 UX 工作流验收,计划:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 +6. P5-6:DeepSeek API key SecretStorage 与 provider status,已完成:插件内配置/清除 key 与查看 provider status;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 +7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key,API key 配置后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 +8. P5-8:Git context 只读采集与大 diff attachment 管线,已完成:优先使用 VS Code Git API,git CLI 仅作受控 fallback,commit/PR 命令把 diff context 作为 `explicit_content` attachment 进入 Context Capsule 管线。 +9. P5-9:Generate Commit Message,已完成:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit;staged 为空时才询问是否使用 unstaged diff。 +10. P5-10:Generate PR Description,已完成:根据 upstream/main/master/用户选择的 base、diff/stat 和 commit summary 生成 PR title/body markdown,用带标题的 untitled markdown 预览承载结果,不自动创建 PR。 +11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 8d9118d9d526ed61d473875a2338081eb5022c54 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:41 +0800 Subject: [PATCH 07/84] Adds security documentation for API key handling and redaction --- docs/security-model.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/security-model.md b/docs/security-model.md index 6123bb6..29fc422 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -7,6 +7,7 @@ ## 边界 - API Key 不得进入 run log。 +- VS Code 插件中的 DeepSeek API key 存入 SecretStorage,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 `DEEPSEEK_API_KEY`。 - `.env` 和本地状态必须被 git 忽略。 - tool call 执行前必须校验 schema。 - 写入应通过 patch application。 @@ -30,11 +31,12 @@ - 工具结果进入 run log 或 prompt 前可通过统一入口转为已脱敏、已截断 JSON;截断边界通过 `runLogTruncation` 记录。 - 写入前检查 workspace 路径。 - 命令类工具取消或超时时清理子进程树;Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底。 +- VS Code 插件统一在 notifier/logger/command warning 边界对 SecretStorage/env API key 脱敏,覆盖 Output Channel、toast、RPC startup failure 和 Git workflow generation failure。 - CI 检查格式、lint、测试和类型。 ## 后续增强 -- 扩展统一脱敏层,覆盖更多 API Key 形态、环境变量、证书、前端历史回放和 provider 错误正文。 +- 扩展统一脱敏层,覆盖更多 API Key 形态、证书、前端历史回放和 provider 错误正文。 - 为敏感路径建立三层拒绝/忽略规则:硬安全排除默认覆盖 `.env`、`.secrets/`、`.secret/`、`.git/`、`.agents/`、证书、token 文件和常见云服务凭据,不能被用户 ignore 规则重新纳入;默认工程排除覆盖 `target/`、`node_modules/`、`dist/`、`build/`;用户上下文排除使用 `.gitignore` 和 `.prole-coderignore`。 - 持续扩展命令风险分类器覆盖面;当前已在审批前识别网络访问、依赖安装、发布、远程 git 操作、删除和 reset 等高风险行为。 - 按平台实现并测试更强 sandbox 边界;Windows、Linux 和 macOS 的能力差异需要在文档和测试中分别说明。 From 00519fa5b90a7f262ddcf4d7dcd138fd135574ab Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 08/84] Updates testing documentation to reflect completed Phase 5 test coverage --- docs/testing.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 79b8e73..e790f1f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -79,14 +79,14 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 -Phase 5 P5-6 到 P5-11 的 API key 与 Git 工作流需要继续补齐以下确定性覆盖: - -- provider secret 纯逻辑测试:SecretStorage 优先、process env fallback、missing status、child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 -- notifier/redaction 测试:Output Channel、toast message 和 RPC startup failure 都必须统一脱敏 SecretStorage/env key。 -- API key 配置 UX 测试:Configure/Clear 命令、provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 -- Git context 测试:无仓库、多仓库、staged diff、unstaged fallback、base branch 选择和大 diff attachment 预算/omitted source。 -- commit/PR 生成测试:Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit;Generate PR Description 输出 markdown 且不自动创建 PR。 -- 文档/打包验收:P5-6 到 P5-10 完成前继续运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 +Phase 5 P5-6 到 P5-11 的 API key 与 Git 工作流已补齐以下确定性覆盖: + +- `providerSecrets.test.ts` 覆盖 SecretStorage 优先、process env fallback、missing status、child env 覆盖值和 redaction source。 +- `notifier.test.ts` 覆盖 Output Channel/toast message 统一脱敏 SecretStorage/env key。 +- `providerSecretCommands.test.ts` 覆盖 Configure/Clear 命令、provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 +- `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 +- `rpcServer.test.ts` 覆盖 RPC child env 注入和 key 轮换后重启使用新 env;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 +- 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 ## 新增测试的协作要求 From 6659c5e68d8f9f9e3219bb9724ba3e54d6d65e88 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 09/84] Registers new VS Code commands for API key management and Git workflows --- vscode/extension/package.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 4540210..8d0292a 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -16,6 +16,11 @@ "onStartupFinished", "onCommand:prole-coder.openChat", "onCommand:prole-coder.openSettings", + "onCommand:prole-coder.configureDeepSeekApiKey", + "onCommand:prole-coder.clearDeepSeekApiKey", + "onCommand:prole-coder.showProviderStatus", + "onCommand:prole-coder.generateCommitMessage", + "onCommand:prole-coder.generatePrDescription", "onView:prole-coder.chat", "onChatParticipant:prole-coder.chatParticipant" ], @@ -29,6 +34,26 @@ { "command": "prole-coder.openSettings", "title": "ProleCoder: Open Settings" + }, + { + "command": "prole-coder.configureDeepSeekApiKey", + "title": "ProleCoder: Configure DeepSeek API Key" + }, + { + "command": "prole-coder.clearDeepSeekApiKey", + "title": "ProleCoder: Clear DeepSeek API Key" + }, + { + "command": "prole-coder.showProviderStatus", + "title": "ProleCoder: Show Provider Status" + }, + { + "command": "prole-coder.generateCommitMessage", + "title": "ProleCoder: Generate Commit Message" + }, + { + "command": "prole-coder.generatePrDescription", + "title": "ProleCoder: Generate PR Description" } ], "chatParticipants": [ From 2a5b35ad1330f1b5a734bc0f036eaefff9c65173 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 10/84] Integrates message redaction into chat participant and chat view --- vscode/extension/src/chatParticipant.ts | 3 ++ vscode/extension/src/chatParticipantCore.ts | 18 ++++++++--- vscode/extension/src/chatView.ts | 34 +++++++++++++++------ 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/vscode/extension/src/chatParticipant.ts b/vscode/extension/src/chatParticipant.ts index 49e8f53..d0566f2 100644 --- a/vscode/extension/src/chatParticipant.ts +++ b/vscode/extension/src/chatParticipant.ts @@ -16,12 +16,14 @@ import { } from "./chatParticipantCore"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; +import type { MessageRedactor } from "./redaction"; export function registerProleChatParticipant( context: vscode.ExtensionContext, rpcClient: ChatParticipantRpcClient | undefined, workspaceRoot: string | undefined, logger?: ProleLogger, + redactor?: MessageRedactor, ): vscode.Disposable { const participant = vscode.chat.createChatParticipant( CHAT_PARTICIPANT_ID, @@ -40,6 +42,7 @@ export function registerProleChatParticipant( }, response, ...(logger === undefined ? {} : { logger }), + ...(redactor === undefined ? {} : { redactor }), token, }); }, diff --git a/vscode/extension/src/chatParticipantCore.ts b/vscode/extension/src/chatParticipantCore.ts index e6862f4..0b0b369 100644 --- a/vscode/extension/src/chatParticipantCore.ts +++ b/vscode/extension/src/chatParticipantCore.ts @@ -14,6 +14,7 @@ import { } from "./automaticContext"; import { DEFAULT_CHAT_MODE, sendTurnParams } from "./chatInput"; import type { ProleLogger } from "./logging"; +import type { MessageRedactor } from "./redaction"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; export const CHAT_PARTICIPANT_ID = "prole-coder.chatParticipant"; @@ -55,6 +56,7 @@ export interface ChatParticipantTurnOptions { readonly request: ChatParticipantTurnRequest; readonly response: ChatParticipantResponseStream; readonly logger?: ProleLogger; + readonly redactor?: MessageRedactor; readonly token?: CancellationTokenLike; } @@ -115,7 +117,7 @@ export async function runChatParticipantTurn( return; } - handleParticipantEvent(event, options.response, finish); + handleParticipantEvent(event, options.response, finish, options.redactor); }); const cancellationSubscription = options.token?.onCancellationRequested(() => { cancellationState.requested = true; @@ -135,7 +137,7 @@ export async function runChatParticipantTurn( for (const event of bufferedEvents) { if (event.runId === runId) { - handleParticipantEvent(event, options.response, finish); + handleParticipantEvent(event, options.response, finish, options.redactor); } } @@ -153,7 +155,10 @@ export async function runChatParticipantTurn( return await terminalPromise; } catch (error) { - const message = `ProleCoder turn failed: ${errorMessage(error)}`; + const message = redactMessage( + `ProleCoder turn failed: ${errorMessage(error)}`, + options.redactor, + ); options.logger?.error(message); return errorResult(message, runId); } finally { @@ -183,6 +188,7 @@ function handleParticipantEvent( event: AgentEventEnvelope, response: ChatParticipantResponseStream, finish: (result: ChatParticipantResult) => void, + redactor?: MessageRedactor, ): void { const payload = record(event.payload); switch (event.type) { @@ -213,7 +219,7 @@ function handleParticipantEvent( }); return; case "run.failed": { - const message = terminalMessage(payload, "Run failed."); + const message = redactMessage(terminalMessage(payload, "Run failed."), redactor); response.markdown(`\n\n${message}`); finish(errorResult(message, event.runId)); return; @@ -275,6 +281,10 @@ function errorResult(message: string, runId?: string): ChatParticipantResult { }; } +function redactMessage(message: string, redactor: MessageRedactor | undefined): string { + return redactor?.redact(message) ?? message; +} + function canceledResult(runId?: string): ChatParticipantResult { return { metadata: { diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 8c73772..1d59bba 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -27,6 +27,7 @@ import { } from "./contextViz"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; +import type { MessageRedactor } from "./redaction"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; import { RUN_LIST_LIMIT, @@ -128,6 +129,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos rpcClient?: ChatRpcClient, private readonly workspaceRoot?: string, private readonly logger?: ProleLogger, + private readonly redactor?: MessageRedactor, ) { this.rpcClient = rpcClient; this.rpcSubscription = rpcClient?.onEvent((event) => { @@ -189,6 +191,10 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos }; } + isIdle(): boolean { + return !this.submission.busy; + } + dispose(): void { this.rpcSubscription?.dispose(); this.viewMessageSubscription?.dispose(); @@ -278,12 +284,13 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos ); } catch (error) { const messageText = `Failed to send turn: ${errorMessage(error)}`; - this.logger?.error(messageText); + const redacted = this.redact(messageText); + this.logger?.error(redacted); this.setSubmission({ ...idleSubmission(), status: "failed", - message: messageText, - error: messageText, + message: redacted, + error: redacted, }); } } @@ -349,8 +356,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.setRunList(readyRunList(result, this.runList.selectedRunId)); } catch (error) { const messageText = `Failed to load runs: ${errorMessage(error)}`; - this.logger?.error(messageText); - this.setRunList(failedRunList(messageText, this.runList)); + const redacted = this.redact(messageText); + this.logger?.error(redacted); + this.setRunList(failedRunList(redacted, this.runList)); } } @@ -380,8 +388,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.setRunList(readyRunList({ runs: this.runList.runs }, result.runId, message)); } catch (error) { const messageText = `Failed to resume run: ${errorMessage(error)}`; - this.logger?.error(messageText); - this.setRunList(failedRunList(messageText, this.runList)); + const redacted = this.redact(messageText); + this.logger?.error(redacted); + this.setRunList(failedRunList(redacted, this.runList)); } } @@ -422,11 +431,12 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos }); } catch (error) { const messageText = `Failed to cancel turn: ${errorMessage(error)}`; - this.logger?.error(messageText); + const redacted = this.redact(messageText); + this.logger?.error(redacted); this.setSubmission({ ...this.submission, - message: messageText, - error: messageText, + message: redacted, + error: redacted, canceling: false, }); } @@ -472,6 +482,10 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.terminalRuns.delete(oldest); } } + + private redact(message: string): string { + return this.redactor?.redact(message) ?? message; + } } function renderChatViewHtml( From 012d38a9bf88ac48ede1e4f32ddfc371b1bfe3e0 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 11/84] Wires up API key management, Git workflows, and redaction in extension activation --- vscode/extension/src/extension.ts | 99 +++++++++++++++++++++---------- 1 file changed, 69 insertions(+), 30 deletions(-) diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 5f583fc..b1137e1 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -10,19 +10,41 @@ import { } from "./commands"; import { createPatchDiffPreviewController } from "./diffPreview"; import { registerFimInlineCompletionProvider } from "./fimPreviewVscode"; -import { createOutputLogger, type ProleLogger } from "./logging"; +import { + GENERATE_COMMIT_MESSAGE_COMMAND, + GENERATE_PR_DESCRIPTION_COMMAND, + generateCommitMessage, + generatePrDescription, +} from "./gitWorkflow"; +import { createVscodeGitRepositoryProvider, createVscodeMarkdownSink } from "./gitWorkflowVscode"; +import { createOutputLogger } from "./logging"; +import { createExtensionNotifier, type ExtensionNotifier } from "./notifier"; +import { registerProviderSecretCommands } from "./providerSecretCommands"; +import { + DEEPSEEK_API_KEY_SECRET_ID, + deepSeekEnvOverride, + providerSecretRedactionValues, + resolveDeepSeekApiKey, +} from "./providerSecrets"; +import { MutableSecretRedactor } from "./redaction"; import { RpcServerManager, readRpcServerLaunchConfig } from "./rpcServer"; -export function activate(context: vscode.ExtensionContext): void { +export async function activate(context: vscode.ExtensionContext): Promise { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const outputChannel = vscode.window.createOutputChannel("ProleCoder"); const logger = createOutputLogger(outputChannel); - const notifier = createExtensionNotifier(logger); + const secretRedactor = new MutableSecretRedactor(); + const initialSecretStatus = resolveDeepSeekApiKey({ + secretValue: await context.secrets.get(DEEPSEEK_API_KEY_SECRET_ID), + processEnv: process.env, + }); + secretRedactor.update(providerSecretRedactionValues(initialSecretStatus)); + const notifier = createExtensionNotifier(logger, vscode.window, secretRedactor); context.subscriptions.push(outputChannel); - const rpcServer = createRpcServerManager(context, notifier); - const chatView = new ProleChatViewProvider(context.extensionUri, rpcServer, workspaceRoot, logger); - const chatParticipant = registerProleChatParticipant(context, rpcServer, workspaceRoot, logger); + const rpcServer = createRpcServerManager(context, notifier, deepSeekEnvOverride(initialSecretStatus)); + const chatView = new ProleChatViewProvider(context.extensionUri, rpcServer, workspaceRoot, logger, secretRedactor); + const chatParticipant = registerProleChatParticipant(context, rpcServer, workspaceRoot, logger, secretRedactor); const openChat = registerOpenChatCommand( vscode.commands, vscode.window, @@ -49,8 +71,46 @@ export function activate(context: vscode.ExtensionContext): void { retainContextWhenHidden: true, }, }); + const providerSecretCommands = registerProviderSecretCommands({ + commands: vscode.commands, + window: vscode.window, + secrets: context.secrets, + processEnv: process.env, + redactor: secretRedactor, + rpcServer, + isRpcIdle: () => chatView.isIdle(), + }); + const gitRepositoryProvider = createVscodeGitRepositoryProvider(); + const markdownSink = createVscodeMarkdownSink(); + const gitWorkflowCommands = [ + vscode.commands.registerCommand(GENERATE_COMMIT_MESSAGE_COMMAND, () => + generateCommitMessage({ + repositories: gitRepositoryProvider, + window: vscode.window, + agent: rpcServer, + redactor: secretRedactor, + }), + ), + vscode.commands.registerCommand(GENERATE_PR_DESCRIPTION_COMMAND, () => + generatePrDescription({ + repositories: gitRepositoryProvider, + window: vscode.window, + agent: rpcServer, + markdownSink, + redactor: secretRedactor, + }), + ), + ]; - context.subscriptions.push(openChat, openSettings, chatView, chatViewRegistration, chatParticipant); + context.subscriptions.push( + openChat, + openSettings, + chatView, + chatViewRegistration, + chatParticipant, + ...providerSecretCommands, + ...gitWorkflowCommands, + ); registerTestCommands(context, chatView); if (rpcServer !== undefined && workspaceRoot !== undefined) { const patchDiffPreviewController = createPatchDiffPreviewController(context, rpcServer, workspaceRoot); @@ -80,6 +140,7 @@ export function deactivate(): void { function createRpcServerManager( context: vscode.ExtensionContext, notifier: ExtensionNotifier, + processEnv: Record, ): RpcServerManager | undefined { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (workspaceRoot === undefined) { @@ -93,33 +154,11 @@ function createRpcServerManager( trusted: vscode.workspace.isTrusted, }, extensionVersion: extensionVersion(context), + processEnv, notifier, }); } -interface ExtensionNotifier { - info(message: string): unknown; - warn(message: string): unknown; - error(message: string): unknown; -} - -function createExtensionNotifier(logger: ProleLogger): ExtensionNotifier { - return { - info(message) { - logger.info(message); - return vscode.window.showInformationMessage(message); - }, - warn(message) { - logger.warn(message); - return vscode.window.showWarningMessage(message); - }, - error(message) { - logger.error(message); - return vscode.window.showWarningMessage(message); - }, - }; -} - function extensionVersion(context: vscode.ExtensionContext): string { const packageJson = context.extension.packageJSON as { version?: unknown }; return typeof packageJson.version === "string" ? packageJson.version : "0.1.0"; From 287f46a7e4c61613813f10ee5c4ad6575308713c Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 12/84] Implements Git workflow commands for commit message and PR description generation --- vscode/extension/src/gitWorkflow.ts | 372 ++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 vscode/extension/src/gitWorkflow.ts diff --git a/vscode/extension/src/gitWorkflow.ts b/vscode/extension/src/gitWorkflow.ts new file mode 100644 index 0000000..ff4d59d --- /dev/null +++ b/vscode/extension/src/gitWorkflow.ts @@ -0,0 +1,372 @@ +import type { SendTurnParams, SendTurnResult, TurnAttachment } from "@prole-coder/protocol" with { + "resolution-mode": "import", +}; + +import type { MessageRedactor } from "./redaction"; +import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; + +export const GENERATE_COMMIT_MESSAGE_COMMAND = "prole-coder.generateCommitMessage"; +export const GENERATE_PR_DESCRIPTION_COMMAND = "prole-coder.generatePrDescription"; + +const USE_UNSTAGED_CHANGES_LABEL = "Use Unstaged"; + +type AgentTextTerminalResult = + | { + readonly state: "completed"; + readonly text: string; + } + | { + readonly state: "failed"; + readonly error: Error; + }; + +export interface GitWorkflowRepository { + readonly label: string; + readonly rootPath: string; + readonly branch?: string | undefined; + readonly upstream?: string | undefined; + readonly inputBox?: { + value: string; + }; + stagedDiff(): Promise; + unstagedDiff(): Promise; + diffStat(base: string): Promise; + branchDiff(base: string): Promise; + commitSummary(base: string): Promise; + refExists(ref: string): Promise; +} + +export interface GitWorkflowRepositoryProvider { + repositories(): Promise; +} + +export interface GitWorkflowWindow { + showInformationMessage(message: string): unknown; + showWarningMessage( + message: string, + ...items: string[] + ): string | undefined | PromiseLike; + showQuickPick?( + items: readonly GitWorkflowRepositoryQuickPickItem[], + options: { readonly placeHolder: string }, + ): + | GitWorkflowRepositoryQuickPickItem + | undefined + | PromiseLike; + showInputBox?(options: { readonly prompt: string; readonly title: string }): string | undefined | PromiseLike; +} + +export interface GitWorkflowRepositoryQuickPickItem { + readonly label: string; + readonly description: string; + readonly repository: GitWorkflowRepository; +} + +export interface GitWorkflowAgent { + onEvent(handler: (event: AgentEventEnvelope) => void): DisposableLike; + sendTurn(params: SendTurnParams): Promise; +} + +export interface GitWorkflowMarkdownSink { + showMarkdown(content: string, title: string): Promise | void; + copyToClipboard?(content: string): Promise | void; +} + +export interface GenerateCommitMessageOptions { + readonly repositories: GitWorkflowRepositoryProvider; + readonly window: GitWorkflowWindow; + readonly agent?: GitWorkflowAgent | undefined; + readonly redactor?: MessageRedactor | undefined; +} + +export interface GeneratePrDescriptionOptions extends GenerateCommitMessageOptions { + readonly markdownSink: GitWorkflowMarkdownSink; +} + +export async function generateCommitMessage(options: GenerateCommitMessageOptions): Promise { + try { + await generateCommitMessageUnchecked(options); + } catch (error) { + options.window.showWarningMessage( + redactMessage(`Failed to generate commit message: ${errorMessage(error)}`, options.redactor), + ); + } +} + +async function generateCommitMessageUnchecked(options: GenerateCommitMessageOptions): Promise { + if (options.agent === undefined) { + options.window.showWarningMessage("Open a trusted workspace before generating a commit message."); + return; + } + + const repository = await selectRepository(options.repositories, options.window); + if (repository === undefined) { + return; + } + + const diff = await commitDiff(repository, options.window); + if (diff === undefined) { + return; + } + + const message = await collectAgentText(options.agent, { + message: "Generate a concise Conventional Commit message for the attached staged diff. Respond with only the commit message.", + mode: "ask", + attachments: [explicitContentAttachment("git/staged.diff", diff)], + }); + + const value = message.trim(); + if (repository.inputBox !== undefined) { + repository.inputBox.value = value; + } + options.window.showInformationMessage("Commit message generated in Source Control."); +} + +export async function generatePrDescription(options: GeneratePrDescriptionOptions): Promise { + try { + await generatePrDescriptionUnchecked(options); + } catch (error) { + options.window.showWarningMessage( + redactMessage(`Failed to generate PR description: ${errorMessage(error)}`, options.redactor), + ); + } +} + +async function generatePrDescriptionUnchecked(options: GeneratePrDescriptionOptions): Promise { + if (options.agent === undefined) { + options.window.showWarningMessage("Open a trusted workspace before generating a PR description."); + return; + } + + const repository = await selectRepository(options.repositories, options.window); + if (repository === undefined) { + return; + } + + const base = await selectBaseRef(repository, options.window); + if (base === undefined) { + return; + } + + const [diffStat, branchDiff, commits] = await Promise.all([ + repository.diffStat(base), + repository.branchDiff(base), + repository.commitSummary(base), + ]); + const content = await collectAgentText(options.agent, { + message: + "Generate a pull request title and markdown body for the attached branch context. Include Summary, Tests, Risks, and Follow-ups sections. Respond with markdown only.", + mode: "ask", + attachments: [ + explicitContentAttachment( + `git/pr-${base}.md`, + [ + `Base: ${base}`, + `Branch: ${repository.branch ?? "unknown"}`, + "", + "## Commits", + commits, + "", + "## Diff Stat", + diffStat, + "", + "## Diff", + branchDiff, + ].join("\n"), + ), + ], + }); + + const markdown = content.trim(); + await options.markdownSink.showMarkdown(markdown, "ProleCoder PR Description"); + await options.markdownSink.copyToClipboard?.(markdown); + options.window.showInformationMessage("PR description generated as markdown."); +} + +async function selectRepository( + provider: GitWorkflowRepositoryProvider, + window: GitWorkflowWindow, +): Promise { + const repositories = await provider.repositories(); + if (repositories.length === 0) { + window.showWarningMessage("Open a Git repository before using ProleCoder Git workflows."); + return undefined; + } + + if (repositories.length === 1) { + return repositories[0]; + } + + if (window.showQuickPick === undefined) { + window.showWarningMessage("Multiple Git repositories are open; choose a repository first."); + return undefined; + } + + const selected = await window.showQuickPick( + repositories.map((repository) => ({ + label: repository.label, + description: repository.rootPath, + repository, + })), + { + placeHolder: "Choose a repository for the ProleCoder Git workflow", + }, + ); + return selected?.repository; +} + +async function commitDiff( + repository: GitWorkflowRepository, + window: GitWorkflowWindow, +): Promise { + const staged = await repository.stagedDiff(); + if (staged.trim().length > 0) { + return staged; + } + + const selected = await window.showWarningMessage( + "No staged diff found. Use unstaged changes as context?", + USE_UNSTAGED_CHANGES_LABEL, + ); + if (selected !== USE_UNSTAGED_CHANGES_LABEL) { + return undefined; + } + + const unstaged = await repository.unstagedDiff(); + if (unstaged.trim().length === 0) { + window.showWarningMessage("No unstaged changes found."); + return undefined; + } + return unstaged; +} + +async function selectBaseRef( + repository: GitWorkflowRepository, + window: GitWorkflowWindow, +): Promise { + if (repository.upstream !== undefined && repository.upstream.length > 0) { + return repository.upstream; + } + + for (const candidate of ["main", "master"]) { + if (await repository.refExists(candidate)) { + return candidate; + } + } + + const value = await window.showInputBox?.({ + title: "Choose PR Base", + prompt: "Enter the base branch or ref for the PR description.", + }); + const normalized = value?.trim(); + if (normalized === undefined || normalized.length === 0) { + window.showWarningMessage("Choose a base branch before generating a PR description."); + return undefined; + } + return normalized; +} + +async function collectAgentText(agent: GitWorkflowAgent, params: SendTurnParams): Promise { + let runId: string | undefined; + const bufferedEvents: AgentEventEnvelope[] = []; + const textParts: string[] = []; + let terminalResult: AgentTextTerminalResult | undefined; + let resolveTerminal: ((result: AgentTextTerminalResult) => void) | undefined; + + const terminalPromise = new Promise((resolve) => { + resolveTerminal = resolve; + }); + + const subscription = agent.onEvent((event) => { + if (runId === undefined) { + bufferedEvents.push(event); + return; + } + handleAgentEvent(event, runId, textParts, finish); + }); + + try { + const result = await agent.sendTurn(params); + runId = result.runId; + for (const event of bufferedEvents) { + handleAgentEvent(event, runId, textParts, finish); + } + if (terminalResult !== undefined) { + return terminalText(terminalResult); + } + return terminalText(await terminalPromise); + } finally { + subscription.dispose(); + } + + function finish(result: AgentTextTerminalResult): void { + if (terminalResult !== undefined) { + return; + } + terminalResult = result; + resolveTerminal?.(result); + } +} + +function handleAgentEvent( + event: AgentEventEnvelope, + runId: string, + textParts: string[], + finish: (result: AgentTextTerminalResult) => void, +): void { + if (event.runId !== runId) { + return; + } + + if (event.type === "assistant.delta" && isRecord(event.payload) && typeof event.payload["text"] === "string") { + textParts.push(event.payload["text"]); + return; + } + + if (event.type === "run.completed") { + finish({ state: "completed", text: textParts.join("") }); + return; + } + + if (event.type === "run.failed") { + finish({ state: "failed", error: new Error(terminalMessage(event, "Agent run failed.")) }); + return; + } + + if (event.type === "run.canceled") { + finish({ state: "failed", error: new Error(terminalMessage(event, "Agent run canceled.")) }); + } +} + +function terminalText(result: AgentTextTerminalResult): string { + if (result.state === "completed") { + return result.text; + } + throw result.error; +} + +function explicitContentAttachment(path: string, text: string): TurnAttachment { + return { + kind: "explicit_content", + path, + text, + }; +} + +function terminalMessage(event: AgentEventEnvelope, fallback: string): string { + const payload = isRecord(event.payload) ? event.payload : undefined; + const message = payload?.["message"] ?? payload?.["reason"] ?? payload?.["summary"]; + return typeof message === "string" && message.length > 0 ? message : fallback; +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function redactMessage(message: string, redactor: MessageRedactor | undefined): string { + return redactor?.redact(message) ?? message; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} From ec0269d9cb607f2a6651fff696cc232f804a8310 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 13/84] Implements VS Code-specific Git repository provider and markdown sink --- vscode/extension/src/gitWorkflowVscode.ts | 141 ++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 vscode/extension/src/gitWorkflowVscode.ts diff --git a/vscode/extension/src/gitWorkflowVscode.ts b/vscode/extension/src/gitWorkflowVscode.ts new file mode 100644 index 0000000..67d4748 --- /dev/null +++ b/vscode/extension/src/gitWorkflowVscode.ts @@ -0,0 +1,141 @@ +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +import * as vscode from "vscode"; + +import type { GitWorkflowRepository, GitWorkflowRepositoryProvider } from "./gitWorkflow"; + +const execFileAsync = promisify(execFile); + +interface VscodeGitExtension { + getAPI(version: 1): VscodeGitApi; +} + +interface VscodeGitApi { + readonly repositories: readonly VscodeGitRepository[]; +} + +interface VscodeGitRepository { + readonly rootUri: vscode.Uri; + readonly inputBox: { + value: string; + }; + readonly state: { + readonly HEAD?: { + readonly name?: string; + readonly upstream?: { + readonly name?: string; + readonly remote?: string; + }; + }; + }; + diffIndexWithHEAD?(): Promise; +} + +export function createVscodeGitRepositoryProvider(): GitWorkflowRepositoryProvider { + return { + async repositories() { + const extension = vscode.extensions.getExtension("vscode.git"); + const gitExtension = extension?.isActive === true ? extension.exports : await extension?.activate(); + const api = gitExtension?.getAPI(1); + return api?.repositories.map(repositoryFromVscodeGit) ?? []; + }, + }; +} + +export function createVscodeMarkdownSink(): { + showMarkdown(content: string, title: string): Promise; + copyToClipboard(content: string): Promise; +} { + return { + async showMarkdown(content, title) { + const document = await vscode.workspace.openTextDocument( + vscode.Uri.from({ + scheme: "untitled", + path: "/prole-coder/" + Date.now().toString(36) + "/" + untitledMarkdownName(title), + }), + ); + const editor = await vscode.window.showTextDocument(document, { + preview: false, + }); + const inserted = await editor.edit((edit) => { + edit.insert(new vscode.Position(0, 0), content); + }); + if (!inserted) { + throw new Error("Failed to open markdown preview."); + } + await vscode.languages.setTextDocumentLanguage(editor.document, "markdown"); + await vscode.commands.executeCommand("workbench.action.keepEditor"); + }, + async copyToClipboard(content) { + await vscode.env.clipboard.writeText(content); + }, + }; +} + +function untitledMarkdownName(title: string): string { + const base = title + .trim() + .replace(/[<>:"/\\|?*\x00-\x1f]+/gu, "-") + .replace(/\s+/gu, " ") + .slice(0, 80) + .trim(); + return (base.length === 0 ? "ProleCoder PR Description" : base) + ".md"; +} + +function repositoryFromVscodeGit(repository: VscodeGitRepository): GitWorkflowRepository { + const rootPath = repository.rootUri.fsPath; + return { + label: repository.rootUri.fsPath.split(/[\\/]/u).at(-1) ?? repository.rootUri.fsPath, + rootPath, + branch: repository.state.HEAD?.name, + upstream: upstreamName(repository.state.HEAD?.upstream), + inputBox: repository.inputBox, + async stagedDiff() { + if (repository.diffIndexWithHEAD !== undefined) { + const diff = await repository.diffIndexWithHEAD(); + if (diff.trim().length > 0) { + return diff; + } + } + return git(rootPath, ["diff", "--cached", "--no-ext-diff", "--"]); + }, + async unstagedDiff() { + return git(rootPath, ["diff", "--no-ext-diff", "--"]); + }, + async diffStat(base) { + return git(rootPath, ["diff", "--stat", "--no-ext-diff", base + "...HEAD", "--"]); + }, + async branchDiff(base) { + return git(rootPath, ["diff", "--no-ext-diff", base + "...HEAD", "--"]); + }, + async commitSummary(base) { + return git(rootPath, ["log", "--oneline", base + "..HEAD"]); + }, + async refExists(ref) { + const result = await git(rootPath, ["rev-parse", "--verify", "--quiet", ref]).catch(() => ""); + return result.trim().length > 0; + }, + }; +} + +async function git(cwd: string, args: readonly string[]): Promise { + const result = await execFileAsync("git", [...args], { + cwd, + windowsHide: true, + maxBuffer: 16 * 1024 * 1024, + }); + return result.stdout; +} + +function upstreamName(upstream: { readonly name?: string; readonly remote?: string } | undefined): string | undefined { + if (upstream === undefined) { + return undefined; + } + const name = upstream.name; + const remote = upstream.remote; + if (name === undefined || name.length === 0) { + return undefined; + } + return remote === undefined || remote.length === 0 ? name : remote + "/" + name; +} From 413317ac6297068fbb61db620f42074cd6f1d6ae Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:42 +0800 Subject: [PATCH 14/84] Creates modular notifier with redaction support --- vscode/extension/src/notifier.ts | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 vscode/extension/src/notifier.ts diff --git a/vscode/extension/src/notifier.ts b/vscode/extension/src/notifier.ts new file mode 100644 index 0000000..b98b5b6 --- /dev/null +++ b/vscode/extension/src/notifier.ts @@ -0,0 +1,38 @@ +import type { ProleLogger } from "./logging"; +import type { MessageRedactor } from "./redaction"; +import { passthroughRedactor } from "./redaction"; + +export interface ExtensionNotifier { + info(message: string): unknown; + warn(message: string): unknown; + error(message: string): unknown; +} + +export interface ToastMessenger { + showInformationMessage(message: string): unknown; + showWarningMessage(message: string): unknown; +} + +export function createExtensionNotifier( + logger: ProleLogger, + window: ToastMessenger, + redactor: MessageRedactor = passthroughRedactor, +): ExtensionNotifier { + return { + info(message) { + const redacted = redactor.redact(message); + logger.info(redacted); + return window.showInformationMessage(redacted); + }, + warn(message) { + const redacted = redactor.redact(message); + logger.warn(redacted); + return window.showWarningMessage(redacted); + }, + error(message) { + const redacted = redactor.redact(message); + logger.error(redacted); + return window.showWarningMessage(redacted); + }, + }; +} From e241012c2e3e6873c4329d54f5133f1946d32f7f Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 15/84] Implements provider secret commands for API key management --- .../extension/src/providerSecretCommands.ts | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 vscode/extension/src/providerSecretCommands.ts diff --git a/vscode/extension/src/providerSecretCommands.ts b/vscode/extension/src/providerSecretCommands.ts new file mode 100644 index 0000000..360ac26 --- /dev/null +++ b/vscode/extension/src/providerSecretCommands.ts @@ -0,0 +1,157 @@ +import { + DEEPSEEK_API_KEY_SECRET_ID, + deepSeekEnvOverride, + formatProviderSecretStatus, + providerSecretRedactionValues, + resolveDeepSeekApiKey, + type DeepSeekSecretResolution, +} from "./providerSecrets"; +import type { MutableSecretRedactor } from "./redaction"; + +export const CONFIGURE_DEEPSEEK_API_KEY_COMMAND = "prole-coder.configureDeepSeekApiKey"; +export const CLEAR_DEEPSEEK_API_KEY_COMMAND = "prole-coder.clearDeepSeekApiKey"; +export const SHOW_PROVIDER_STATUS_COMMAND = "prole-coder.showProviderStatus"; + +export interface CommandRegistry { + registerCommand(command: string, callback: () => unknown): DisposableLike; +} + +export interface DisposableLike { + dispose(): unknown; +} + +export interface SecretStorageLike { + get(key: string): PromiseLike; + store(key: string, value: string): PromiseLike; + delete(key: string): PromiseLike; +} + +export interface SecretWindowMessenger { + showInputBox(options: { + readonly ignoreFocusOut: true; + readonly password: true; + readonly prompt: string; + readonly title: string; + }): string | undefined | PromiseLike; + showInformationMessage(message: string): unknown; + showWarningMessage(message: string): unknown; +} + +export interface ProviderSecretRpcServer { + readonly status: string; + setProcessEnv(env: Record): void; + start(): Promise; + stop(): void; +} + +export interface RegisterProviderSecretCommandOptions { + readonly commands: CommandRegistry; + readonly window: SecretWindowMessenger; + readonly secrets: SecretStorageLike; + readonly processEnv?: Record | undefined; + readonly redactor?: MutableSecretRedactor; + readonly rpcServer?: ProviderSecretRpcServer | undefined; + readonly isRpcIdle?: () => boolean; +} + +export interface ProviderSecretRuntimeState { + status: DeepSeekSecretResolution; +} + +export function registerProviderSecretCommands( + options: RegisterProviderSecretCommandOptions, +): readonly DisposableLike[] { + const state: ProviderSecretRuntimeState = { + status: resolveDeepSeekApiKey({ processEnv: options.processEnv }), + }; + + void refreshProviderSecretState(options, state); + + return [ + options.commands.registerCommand(CONFIGURE_DEEPSEEK_API_KEY_COMMAND, async () => { + const value = await options.window.showInputBox({ + title: "Configure DeepSeek API Key", + prompt: "Enter the DeepSeek API key for ProleCoder. It is stored in VS Code SecretStorage.", + password: true, + ignoreFocusOut: true, + }); + if (value === undefined) { + return undefined; + } + + const normalized = value.trim(); + if (normalized.length === 0) { + return options.window.showWarningMessage("DeepSeek API key was not changed."); + } + + await options.secrets.store(DEEPSEEK_API_KEY_SECRET_ID, normalized); + const status = await refreshProviderSecretState(options, state); + await restartRpcAfterSecretChange(options, "DeepSeek API key saved."); + return options.window.showInformationMessage(formatProviderSecretStatus(status)); + }), + options.commands.registerCommand(CLEAR_DEEPSEEK_API_KEY_COMMAND, async () => { + await options.secrets.delete(DEEPSEEK_API_KEY_SECRET_ID); + const status = await refreshProviderSecretState(options, state); + await restartRpcAfterSecretChange(options, "DeepSeek API key cleared."); + return options.window.showInformationMessage(formatProviderSecretStatus(status)); + }), + options.commands.registerCommand(SHOW_PROVIDER_STATUS_COMMAND, async () => { + const status = await refreshProviderSecretState(options, state); + return options.window.showInformationMessage(formatProviderSecretStatus(status)); + }), + ]; +} + +export async function refreshProviderSecretState( + options: Pick, + state?: ProviderSecretRuntimeState, +): Promise { + const secretValue = await options.secrets.get(DEEPSEEK_API_KEY_SECRET_ID); + const status = resolveDeepSeekApiKey({ + secretValue, + processEnv: options.processEnv, + }); + options.rpcServer?.setProcessEnv(deepSeekEnvOverride(status)); + options.redactor?.update(providerSecretRedactionValues(status)); + if (state !== undefined) { + state.status = status; + } + return status; +} + +async function restartRpcAfterSecretChange( + options: Pick, + prefix: string, +): Promise { + const rpcServer = options.rpcServer; + if (rpcServer === undefined || rpcServer.status === "stopped" || rpcServer.status === "failed") { + return; + } + + if (rpcServer.status !== "ready") { + options.window.showInformationMessage(`${prefix} It will be used after the RPC server restarts.`); + return; + } + + if (options.isRpcIdle?.() === false) { + options.window.showInformationMessage(`${prefix} It will be used after the current turn finishes.`); + return; + } + + rpcServer.stop(); + try { + await rpcServer.start(); + } catch (error) { + options.window.showWarningMessage( + redactMessage(`DeepSeek API key updated, but RPC restart failed: ${errorMessage(error)}`, options.redactor), + ); + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function redactMessage(message: string, redactor: MutableSecretRedactor | undefined): string { + return redactor?.redact(message) ?? message; +} From b8dfa86b48e18e5bf1fa299391c24d50affdbf8a Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 16/84] Implements provider secret resolution and environment management --- vscode/extension/src/providerSecrets.ts | 67 +++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 vscode/extension/src/providerSecrets.ts diff --git a/vscode/extension/src/providerSecrets.ts b/vscode/extension/src/providerSecrets.ts new file mode 100644 index 0000000..c5c2f32 --- /dev/null +++ b/vscode/extension/src/providerSecrets.ts @@ -0,0 +1,67 @@ +export const DEEPSEEK_API_KEY_SECRET_ID = "prole-coder.deepseek-api-key"; +export const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY"; + +export type ProviderSecretSource = "missing" | "process-env" | "secret-storage"; + +export interface DeepSeekSecretResolution { + readonly provider: "deepseek"; + readonly source: ProviderSecretSource; + readonly apiKey?: string; +} + +export interface ResolveDeepSeekApiKeyInput { + readonly secretValue?: string | undefined; + readonly processEnv?: Record | undefined; +} + +export function resolveDeepSeekApiKey(input: ResolveDeepSeekApiKeyInput): DeepSeekSecretResolution { + const secretValue = normalizedSecret(input.secretValue); + if (secretValue !== undefined) { + return { + provider: "deepseek", + source: "secret-storage", + apiKey: secretValue, + }; + } + + const envValue = normalizedSecret(input.processEnv?.[DEEPSEEK_API_KEY_ENV]); + if (envValue !== undefined) { + return { + provider: "deepseek", + source: "process-env", + apiKey: envValue, + }; + } + + return { + provider: "deepseek", + source: "missing", + }; +} + +export function deepSeekEnvOverride( + resolution: DeepSeekSecretResolution, +): Record { + return resolution.apiKey === undefined ? {} : { [DEEPSEEK_API_KEY_ENV]: resolution.apiKey }; +} + +export function providerSecretRedactionValues( + resolution: DeepSeekSecretResolution, +): readonly string[] { + return resolution.apiKey === undefined ? [] : [resolution.apiKey]; +} + +export function formatProviderSecretStatus(resolution: DeepSeekSecretResolution): string { + const source = + resolution.source === "secret-storage" + ? "VS Code SecretStorage" + : resolution.source === "process-env" + ? "process env" + : "missing"; + return `DeepSeek API key: ${source}`; +} + +function normalizedSecret(value: string | undefined): string | undefined { + const normalized = value?.trim(); + return normalized === undefined || normalized.length === 0 ? undefined : normalized; +} From fd9a324df025d2628035609ae6aa87621c6860c1 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 17/84] Implements mutable message redactor for sensitive data --- vscode/extension/src/redaction.ts | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 vscode/extension/src/redaction.ts diff --git a/vscode/extension/src/redaction.ts b/vscode/extension/src/redaction.ts new file mode 100644 index 0000000..b81ae57 --- /dev/null +++ b/vscode/extension/src/redaction.ts @@ -0,0 +1,36 @@ +export interface MessageRedactor { + redact(message: string): string; +} + +export class MutableSecretRedactor implements MessageRedactor { + private secretValues: readonly string[] = []; + + update(secretValues: readonly string[]): void { + this.secretValues = uniqueSecrets(secretValues); + } + + redact(message: string): string { + let redacted = message; + for (const secret of this.secretValues) { + redacted = redacted.replaceAll(secret, "[redacted]"); + } + return redacted; + } +} + +export const passthroughRedactor: MessageRedactor = { + redact(message) { + return message; + }, +}; + +function uniqueSecrets(secretValues: readonly string[]): readonly string[] { + const values = new Set(); + for (const value of secretValues) { + const normalized = value.trim(); + if (normalized.length > 0) { + values.add(normalized); + } + } + return [...values].sort((left, right) => right.length - left.length); +} From 9ffa7c5ccd040657c56b4c61b1b62dc6b92e5a89 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 18/84] Adds process environment support to RPC server manager --- vscode/extension/src/rpcServer.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/vscode/extension/src/rpcServer.ts b/vscode/extension/src/rpcServer.ts index 1a6a86c..4ccee60 100644 --- a/vscode/extension/src/rpcServer.ts +++ b/vscode/extension/src/rpcServer.ts @@ -70,6 +70,7 @@ export interface RpcServerConfiguration { export interface RpcSpawnOptions { readonly cwd: string; + readonly env?: Record; } export interface RpcWritable { @@ -105,6 +106,7 @@ export interface RpcServerManagerOptions { readonly launch: RpcServerLaunchConfig; readonly workspace: RpcServerWorkspace; readonly extensionVersion: string; + readonly processEnv?: Record; readonly processFactory?: RpcProcessFactory; readonly notifier?: RpcServerNotifier; } @@ -149,6 +151,10 @@ export const nodeRpcProcessFactory: RpcProcessFactory = { spawn(command, args, options) { return spawn(command, [...args], { cwd: options.cwd, + env: { + ...process.env, + ...options.env, + }, stdio: "pipe", windowsHide: true, }); @@ -183,6 +189,7 @@ export class RpcServerManager implements DisposableLike { private readonly notifier: RpcServerNotifier | undefined; private readonly eventHandlers = new Set<(event: AgentEventEnvelope) => void>(); private readonly pendingRequests = new Map>(); + private processEnv: Record; private child: RpcChildProcess | undefined; private startPromise: Promise | undefined; @@ -202,6 +209,7 @@ export class RpcServerManager implements DisposableLike { this.extensionVersion = options.extensionVersion; this.processFactory = options.processFactory ?? nodeRpcProcessFactory; this.notifier = options.notifier; + this.processEnv = { ...(options.processEnv ?? {}) }; } get status(): RpcServerStatus { @@ -220,6 +228,10 @@ export class RpcServerManager implements DisposableLike { return this.launch; } + setProcessEnv(env: Record): void { + this.processEnv = { ...env }; + } + start(): Promise { if (this.readyState !== undefined && this.currentStatus === "ready") { return Promise.resolve(this.readyState); @@ -245,6 +257,7 @@ export class RpcServerManager implements DisposableLike { try { child = this.processFactory.spawn(this.launch.command, this.launch.args, { cwd: this.workspace.root, + env: this.processEnv, }); } catch (error) { const spawnError = asError(error); From 2f51375ae6f0c9d948732a7760a5ea5948e1f0c8 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 19/84] Updates VS Code test host to verify new commands and improve cancel test --- vscode/extension/test/electron/index.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/vscode/extension/test/electron/index.ts b/vscode/extension/test/electron/index.ts index a394f66..eff4c66 100644 --- a/vscode/extension/test/electron/index.ts +++ b/vscode/extension/test/electron/index.ts @@ -6,6 +6,15 @@ import * as vscode from "vscode"; const extensionId = "prole-coder.prole-coder-vscode"; const TEST_CHAT_MESSAGE_COMMAND = "prole-coder.test.chatMessage"; const TEST_CHAT_STATE_COMMAND = "prole-coder.test.chatState"; +const CONTRIBUTED_COMMANDS = [ + "prole-coder.openChat", + "prole-coder.openSettings", + "prole-coder.configureDeepSeekApiKey", + "prole-coder.clearDeepSeekApiKey", + "prole-coder.showProviderStatus", + "prole-coder.generateCommitMessage", + "prole-coder.generatePrDescription", +] as const; export async function run(): Promise { const extension = vscode.extensions.getExtension(extensionId); @@ -21,7 +30,9 @@ export async function run(): Promise { await vscode.commands.executeCommand("prole-coder.chat.focus"); const commands = await vscode.commands.getCommands(true); - assert.equal(commands.includes("prole-coder.openChat"), true); + for (const command of CONTRIBUTED_COMMANDS) { + assert.equal(commands.includes(command), true); + } assert.equal(commands.includes(TEST_CHAT_MESSAGE_COMMAND), true); assert.equal(commands.includes(TEST_CHAT_STATE_COMMAND), true); await vscode.commands.executeCommand("prole-coder.openChat"); @@ -109,7 +120,9 @@ async function exerciseChatCancel(): Promise { const canceled = await waitFor("canceled chat turn", async () => { const current = await chatState(); - return current.submission.status === "canceled" && current.submission.runId === "run-cancel-1" + return current.submission.status === "canceled" && + current.submission.runId === "run-cancel-1" && + current.timeline.items.some((item) => item.type === "run.canceled") ? current : undefined; }); From 8dabe8fb38c17191e9b77adad4da0f959d0d45a2 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 20/84] Adds unit tests for Git workflow commands --- vscode/extension/test/gitWorkflow.test.ts | 315 ++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 vscode/extension/test/gitWorkflow.test.ts diff --git a/vscode/extension/test/gitWorkflow.test.ts b/vscode/extension/test/gitWorkflow.test.ts new file mode 100644 index 0000000..463e9d8 --- /dev/null +++ b/vscode/extension/test/gitWorkflow.test.ts @@ -0,0 +1,315 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import type { SendTurnParams, SendTurnResult } from "@prole-coder/protocol" with { + "resolution-mode": "import", +}; + +import { + generateCommitMessage, + generatePrDescription, + type GitWorkflowAgent, + type GitWorkflowRepository, + type GitWorkflowRepositoryProvider, +} from "../src/gitWorkflow.js"; +import type { AgentEventEnvelope, DisposableLike } from "../src/rpcServer.js"; + +test("generateCommitMessage sends staged diff to the agent and writes the SCM input box", async () => { + const repository = new FakeRepository({ + stagedDiff: "diff --git a/src/lib.rs b/src/lib.rs\n+new code\n", + }); + const agent = new FakeAgent("feat: update library"); + const window = new FakeGitWindow(); + + await generateCommitMessage({ + repositories: repositoryProvider(repository), + window, + agent, + }); + + assert.equal(repository.inputBox.value, "feat: update library"); + assert.equal(agent.sendTurns[0]?.mode, "ask"); + assert.ok(agent.sendTurns[0]?.message.includes("Conventional Commit")); + assert.ok(agent.sendTurns[0]?.attachments?.[0]?.text?.includes("diff --git")); + assert.equal(window.infos.at(-1), "Commit message generated in Source Control."); +}); + +test("generateCommitMessage asks before using unstaged changes", async () => { + const repository = new FakeRepository({ + stagedDiff: "", + unstagedDiff: "diff --git a/README.md b/README.md\n+docs\n", + }); + const agent = new FakeAgent("docs: update readme"); + const window = new FakeGitWindow(["Use Unstaged"]); + + await generateCommitMessage({ + repositories: repositoryProvider(repository), + window, + agent, + }); + + assert.equal(repository.inputBox.value, "docs: update readme"); + assert.ok(agent.sendTurns[0]?.attachments?.[0]?.text?.includes("README.md")); +}); + +test("generateCommitMessage redacts generation failures", async () => { + const repository = new FakeRepository({ + stagedDiff: "diff --git a/src/lib.rs b/src/lib.rs\n+new code\n", + }); + const window = new FakeGitWindow(); + + await generateCommitMessage({ + repositories: repositoryProvider(repository), + window, + agent: new FailingAgent(new Error("provider failed with fixture-secret")), + redactor: { + redact(message) { + return message.replaceAll("fixture-secret", "[redacted]"); + }, + }, + }); + + assert.equal(window.warnings.at(-1), "Failed to generate commit message: provider failed with [redacted]"); +}); + +test("generateCommitMessage keeps the first terminal agent event", async () => { + const repository = new FakeRepository({ + stagedDiff: "diff --git a/src/lib.rs b/src/lib.rs\n+new code\n", + }); + const window = new FakeGitWindow(); + + await generateCommitMessage({ + repositories: repositoryProvider(repository), + window, + agent: new DuplicateTerminalAgent(), + }); + + assert.equal(repository.inputBox.value, "feat: first result"); + assert.deepEqual(window.warnings, []); +}); + +test("generatePrDescription uses upstream branch context and opens markdown without creating a PR", async () => { + const repository = new FakeRepository({ + branch: "feature/api-key", + upstream: "origin/main", + diffStat: " README.md | 2 ++", + branchDiff: "diff --git a/README.md b/README.md\n+summary\n", + commitSummary: "abc123 Add API key UX", + }); + const agent = new FakeAgent("# Add API key UX\n\n## Summary\n- Adds configuration"); + const sink = new FakeMarkdownSink(); + + await generatePrDescription({ + repositories: repositoryProvider(repository), + window: new FakeGitWindow(), + agent, + markdownSink: sink, + }); + + assert.ok(agent.sendTurns[0]?.attachments?.[0]?.text?.includes("Base: origin/main")); + assert.ok(agent.sendTurns[0]?.attachments?.[0]?.text?.includes("abc123 Add API key UX")); + assert.equal(sink.markdowns[0]?.title, "ProleCoder PR Description"); + assert.ok(sink.copied.at(-1)?.includes("## Summary")); +}); + +test("generatePrDescription falls back to main when no upstream exists", async () => { + const repository = new FakeRepository({ + branch: "feature/no-upstream", + refs: new Set(["main"]), + }); + const agent = new FakeAgent("# PR\n"); + const sink = new FakeMarkdownSink(); + + await generatePrDescription({ + repositories: repositoryProvider(repository), + window: new FakeGitWindow(), + agent, + markdownSink: sink, + }); + + assert.ok(agent.sendTurns[0]?.attachments?.[0]?.text?.includes("Base: main")); +}); + +class FakeRepository implements GitWorkflowRepository { + readonly label = "repo"; + readonly rootPath = "C:/workspace/repo"; + readonly inputBox = { value: "" }; + readonly branch?: string | undefined; + readonly upstream?: string | undefined; + private readonly stagedDiffValue: string; + private readonly unstagedDiffValue: string; + private readonly diffStatValue: string; + private readonly branchDiffValue: string; + private readonly commitSummaryValue: string; + private readonly refs: Set; + + constructor(options: { + readonly branch?: string; + readonly upstream?: string; + readonly stagedDiff?: string; + readonly unstagedDiff?: string; + readonly diffStat?: string; + readonly branchDiff?: string; + readonly commitSummary?: string; + readonly refs?: Set; + } = {}) { + this.branch = options.branch; + this.upstream = options.upstream; + this.stagedDiffValue = options.stagedDiff ?? ""; + this.unstagedDiffValue = options.unstagedDiff ?? ""; + this.diffStatValue = options.diffStat ?? ""; + this.branchDiffValue = options.branchDiff ?? ""; + this.commitSummaryValue = options.commitSummary ?? ""; + this.refs = options.refs ?? new Set(); + } + + async stagedDiff(): Promise { + return this.stagedDiffValue; + } + + async unstagedDiff(): Promise { + return this.unstagedDiffValue; + } + + async diffStat(): Promise { + return this.diffStatValue; + } + + async branchDiff(): Promise { + return this.branchDiffValue; + } + + async commitSummary(): Promise { + return this.commitSummaryValue; + } + + async refExists(ref: string): Promise { + return this.refs.has(ref); + } +} + +class FakeGitWindow { + readonly infos: string[] = []; + readonly warnings: string[] = []; + + constructor(private readonly warningSelections: string[] = []) {} + + showInformationMessage(message: string): void { + this.infos.push(message); + } + + showWarningMessage(message: string): string | undefined { + this.warnings.push(message); + return this.warningSelections.shift(); + } +} + +class FakeAgent implements GitWorkflowAgent { + readonly sendTurns: SendTurnParams[] = []; + private readonly handlers = new Set<(event: AgentEventEnvelope) => void>(); + + constructor(private readonly responseText: string) {} + + onEvent(handler: (event: AgentEventEnvelope) => void): DisposableLike { + this.handlers.add(handler); + return { + dispose: () => { + this.handlers.delete(handler); + }, + }; + } + + async sendTurn(params: SendTurnParams): Promise { + this.sendTurns.push(params); + queueMicrotask(() => { + this.emit(agentEvent(1, "assistant.delta", { text: this.responseText })); + this.emit(agentEvent(2, "run.completed", { summary: "done" })); + }); + return { + runId: "run_git_1", + turnId: "turn_git_1", + accepted: true, + }; + } + + private emit(event: AgentEventEnvelope): void { + for (const handler of this.handlers) { + handler(event); + } + } +} + +class FailingAgent implements GitWorkflowAgent { + constructor(private readonly error: Error) {} + + onEvent(): DisposableLike { + return { dispose: () => undefined }; + } + + async sendTurn(): Promise { + throw this.error; + } +} + +class DuplicateTerminalAgent implements GitWorkflowAgent { + private readonly handlers = new Set<(event: AgentEventEnvelope) => void>(); + + onEvent(handler: (event: AgentEventEnvelope) => void): DisposableLike { + this.handlers.add(handler); + return { + dispose: () => { + this.handlers.delete(handler); + }, + }; + } + + async sendTurn(): Promise { + this.emit(agentEvent(1, "assistant.delta", { text: "feat: first result" })); + this.emit(agentEvent(2, "run.completed", { summary: "done" })); + this.emit(agentEvent(3, "assistant.delta", { text: "\nshould be ignored" })); + this.emit(agentEvent(4, "run.failed", { message: "late failure" })); + return { + runId: "run_git_1", + turnId: "turn_git_1", + accepted: true, + }; + } + + private emit(event: AgentEventEnvelope): void { + for (const handler of this.handlers) { + handler(event); + } + } +} + +class FakeMarkdownSink { + readonly markdowns: Array<{ readonly content: string; readonly title: string }> = []; + readonly copied: string[] = []; + + showMarkdown(content: string, title: string): void { + this.markdowns.push({ content, title }); + } + + copyToClipboard(content: string): void { + this.copied.push(content); + } +} + +function repositoryProvider(repository: GitWorkflowRepository): GitWorkflowRepositoryProvider { + return { + async repositories() { + return [repository]; + }, + }; +} + +function agentEvent(seq: number, type: string, payload: unknown): AgentEventEnvelope { + return { + seq, + time: "1970-01-01T00:00:00.000Z", + type, + runId: "run_git_1", + turnId: "turn_git_1", + payload, + }; +} From b63cb08ba9e5fd5f2f723300114b64da5385da5e Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:43 +0800 Subject: [PATCH 21/84] Adds unit tests for notifier redaction --- vscode/extension/test/notifier.test.ts | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 vscode/extension/test/notifier.test.ts diff --git a/vscode/extension/test/notifier.test.ts b/vscode/extension/test/notifier.test.ts new file mode 100644 index 0000000..204d925 --- /dev/null +++ b/vscode/extension/test/notifier.test.ts @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { createExtensionNotifier } from "../src/notifier.js"; +import { MutableSecretRedactor } from "../src/redaction.js"; + +test("extension notifier redacts secret values before logging and showing toasts", () => { + const logger = new FakeLogger(); + const window = new FakeWindow(); + const redactor = new MutableSecretRedactor(); + redactor.update(["fixture-secret"]); + + const notifier = createExtensionNotifier(logger, window, redactor); + notifier.info("using fixture-secret"); + notifier.warn("warn fixture-secret"); + notifier.error("error fixture-secret"); + + assert.deepEqual(logger.infos, ["using [redacted]"]); + assert.deepEqual(logger.warnings, ["warn [redacted]"]); + assert.deepEqual(logger.errors, ["error [redacted]"]); + assert.deepEqual(window.infos, ["using [redacted]"]); + assert.deepEqual(window.warnings, ["warn [redacted]", "error [redacted]"]); +}); + +class FakeLogger { + readonly infos: string[] = []; + readonly warnings: string[] = []; + readonly errors: string[] = []; + + info(message: string): void { + this.infos.push(message); + } + + warn(message: string): void { + this.warnings.push(message); + } + + error(message: string): void { + this.errors.push(message); + } +} + +class FakeWindow { + readonly infos: string[] = []; + readonly warnings: string[] = []; + + showInformationMessage(message: string): void { + this.infos.push(message); + } + + showWarningMessage(message: string): void { + this.warnings.push(message); + } +} From 72517ee134de67dc7032005e9c77c6e454d7ff17 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:44 +0800 Subject: [PATCH 22/84] Adds unit tests for provider secret commands --- .../test/providerSecretCommands.test.ts | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 vscode/extension/test/providerSecretCommands.test.ts diff --git a/vscode/extension/test/providerSecretCommands.test.ts b/vscode/extension/test/providerSecretCommands.test.ts new file mode 100644 index 0000000..b0881af --- /dev/null +++ b/vscode/extension/test/providerSecretCommands.test.ts @@ -0,0 +1,185 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + CLEAR_DEEPSEEK_API_KEY_COMMAND, + CONFIGURE_DEEPSEEK_API_KEY_COMMAND, + SHOW_PROVIDER_STATUS_COMMAND, + registerProviderSecretCommands, +} from "../src/providerSecretCommands.js"; +import { DEEPSEEK_API_KEY_ENV, DEEPSEEK_API_KEY_SECRET_ID } from "../src/providerSecrets.js"; +import { MutableSecretRedactor } from "../src/redaction.js"; + +test("configure DeepSeek API key stores SecretStorage value, updates env, and restarts idle RPC", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([" fixture-secret-value "]); + const secrets = new FakeSecrets(); + const redactor = new MutableSecretRedactor(); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + processEnv: { + [DEEPSEEK_API_KEY_ENV]: "env-secret-value", + }, + redactor, + rpcServer: rpc, + isRpcIdle: () => true, + }); + + await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + + assert.equal(await secrets.get(DEEPSEEK_API_KEY_SECRET_ID), "fixture-secret-value"); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "fixture-secret-value", + }); + assert.equal(rpc.stopCount, 1); + assert.equal(rpc.startCount, 1); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: VS Code SecretStorage"); + assert.equal(redactor.redact("key fixture-secret-value"), "key [redacted]"); +}); + +test("clear DeepSeek API key falls back to process env without restarting active RPC", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([]); + const secrets = new FakeSecrets({ + [DEEPSEEK_API_KEY_SECRET_ID]: "fixture-secret-value", + }); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + processEnv: { + [DEEPSEEK_API_KEY_ENV]: "env-secret-value", + }, + rpcServer: rpc, + isRpcIdle: () => false, + }); + + await commands.run(CLEAR_DEEPSEEK_API_KEY_COMMAND); + + assert.equal(await secrets.get(DEEPSEEK_API_KEY_SECRET_ID), undefined); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "env-secret-value", + }); + assert.equal(rpc.stopCount, 0); + assert.equal(rpc.startCount, 0); + assert.ok(window.infos.some((message) => message.includes("current turn finishes"))); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: process env"); +}); + +test("show provider status reports missing configuration without exposing keys", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([]); + + registerProviderSecretCommands({ + commands, + window, + secrets: new FakeSecrets(), + processEnv: {}, + }); + + await commands.run(SHOW_PROVIDER_STATUS_COMMAND); + + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: missing"); +}); + +test("configure DeepSeek API key redacts restart failures", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow(["fixture-secret-value"]); + const redactor = new MutableSecretRedactor(); + const rpc = new FakeRpcServer("ready", new Error("failed with fixture-secret-value")); + + registerProviderSecretCommands({ + commands, + window, + secrets: new FakeSecrets(), + redactor, + rpcServer: rpc, + isRpcIdle: () => true, + }); + + await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + + assert.equal(window.warnings.at(-1), "DeepSeek API key updated, but RPC restart failed: failed with [redacted]"); +}); + +class FakeCommands { + private readonly callbacks = new Map unknown>(); + + registerCommand(command: string, callback: () => unknown) { + this.callbacks.set(command, callback); + return { dispose: () => undefined }; + } + + async run(command: string): Promise { + const callback = this.callbacks.get(command); + assert.ok(callback, `${command} should be registered`); + await callback(); + } +} + +class FakeSecretWindow { + readonly infos: string[] = []; + readonly warnings: string[] = []; + + constructor(private readonly inputValues: string[]) {} + + showInputBox(): string | undefined { + return this.inputValues.shift(); + } + + showInformationMessage(message: string): void { + this.infos.push(message); + } + + showWarningMessage(message: string): void { + this.warnings.push(message); + } +} + +class FakeSecrets { + constructor(private readonly values: Record = {}) {} + + async get(key: string): Promise { + return this.values[key]; + } + + async store(key: string, value: string): Promise { + this.values[key] = value; + } + + async delete(key: string): Promise { + delete this.values[key]; + } +} + +class FakeRpcServer { + processEnv: Record = {}; + startCount = 0; + stopCount = 0; + + constructor( + readonly status: string, + private readonly startError?: Error, + ) {} + + setProcessEnv(env: Record): void { + this.processEnv = env; + } + + async start(): Promise { + this.startCount += 1; + if (this.startError !== undefined) { + throw this.startError; + } + } + + stop(): void { + this.stopCount += 1; + } +} From 13ba1a572555ae8fbec111100a34a381fd4c2f93 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:44 +0800 Subject: [PATCH 23/84] Adds unit tests for provider secret resolution --- vscode/extension/test/providerSecrets.test.ts | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 vscode/extension/test/providerSecrets.test.ts diff --git a/vscode/extension/test/providerSecrets.test.ts b/vscode/extension/test/providerSecrets.test.ts new file mode 100644 index 0000000..8ec18b1 --- /dev/null +++ b/vscode/extension/test/providerSecrets.test.ts @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + DEEPSEEK_API_KEY_ENV, + deepSeekEnvOverride, + formatProviderSecretStatus, + providerSecretRedactionValues, + resolveDeepSeekApiKey, +} from "../src/providerSecrets.js"; + +test("resolveDeepSeekApiKey prefers SecretStorage over process env", () => { + const resolution = resolveDeepSeekApiKey({ + secretValue: " secret-storage-key ", + processEnv: { + [DEEPSEEK_API_KEY_ENV]: "env-key", + }, + }); + + assert.equal(resolution.source, "secret-storage"); + assert.equal(resolution.apiKey, "secret-storage-key"); + assert.deepEqual(deepSeekEnvOverride(resolution), { + [DEEPSEEK_API_KEY_ENV]: "secret-storage-key", + }); + assert.deepEqual(providerSecretRedactionValues(resolution), ["secret-storage-key"]); + assert.equal(formatProviderSecretStatus(resolution), "DeepSeek API key: VS Code SecretStorage"); +}); + +test("resolveDeepSeekApiKey falls back to process env and reports missing", () => { + const envResolution = resolveDeepSeekApiKey({ + processEnv: { + [DEEPSEEK_API_KEY_ENV]: " env-key ", + }, + }); + + assert.equal(envResolution.source, "process-env"); + assert.equal(envResolution.apiKey, "env-key"); + assert.equal(formatProviderSecretStatus(envResolution), "DeepSeek API key: process env"); + + const missingResolution = resolveDeepSeekApiKey({ + secretValue: " ", + processEnv: { + [DEEPSEEK_API_KEY_ENV]: "", + }, + }); + + assert.equal(missingResolution.source, "missing"); + assert.deepEqual(deepSeekEnvOverride(missingResolution), {}); + assert.deepEqual(providerSecretRedactionValues(missingResolution), []); + assert.equal(formatProviderSecretStatus(missingResolution), "DeepSeek API key: missing"); +}); From b13acfcb0266e78a49987f1e8222748a35bb0de4 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 15:56:44 +0800 Subject: [PATCH 24/84] Adds unit test for RPC server process environment forwarding --- vscode/extension/test/rpcServer.test.ts | 41 +++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/vscode/extension/test/rpcServer.test.ts b/vscode/extension/test/rpcServer.test.ts index 0c1b50e..8e18aa0 100644 --- a/vscode/extension/test/rpcServer.test.ts +++ b/vscode/extension/test/rpcServer.test.ts @@ -63,6 +63,47 @@ test("RPC server manager spawns the configured command and initializes the works assert.equal(ready.capabilities.supportsEventBatching, true); }); +test("RPC server manager forwards configured process environment overrides", async () => { + const factory = new FakeProcessFactory(); + const manager = new RpcServerManager({ + launch: { + command: "prole", + args: ["rpc"], + autoStart: true, + }, + workspace: { + root: "C:/workspace/project", + trusted: true, + }, + extensionVersion: "0.1.0", + processFactory: factory, + processEnv: { + DEEPSEEK_API_KEY: "stored-key", + }, + }); + + const readyPromise = manager.start(); + const child = factory.lastChild(); + child.stdout.pushJson(initializeResponse(child.initializeRequest().id)); + await readyPromise; + + assert.deepEqual(factory.lastOptions?.env, { + DEEPSEEK_API_KEY: "stored-key", + }); + + manager.stop(); + manager.setProcessEnv({ DEEPSEEK_API_KEY: "rotated-key" }); + + const restarted = manager.start(); + const restartedChild = factory.lastChild(); + restartedChild.stdout.pushJson(initializeResponse(restartedChild.initializeRequest().id)); + await restarted; + + assert.deepEqual(factory.lastOptions?.env, { + DEEPSEEK_API_KEY: "rotated-key", + }); +}); + test("RPC server manager forwards agent.event notifications", async () => { const factory = new FakeProcessFactory(); const manager = rpcManagerWithFactory(factory); From 4d5063062aa86146405fa77f8abb58a5ebd11fd5 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 21:37:31 +0800 Subject: [PATCH 25/84] Adds DeepSeek model selector and Sidebar Key/Model buttons --- vscode/extension/package.json | 12 +++ vscode/extension/src/extension.ts | 11 ++- .../extension/src/providerConfigurationUx.ts | 6 ++ .../extension/src/providerSecretCommands.ts | 99 ++++++++++++++++++- vscode/extension/src/providerSecrets.ts | 59 ++++++++++- vscode/extension/src/rpcServer.ts | 20 +++- vscode/extension/test/electron/index.ts | 1 + .../test/providerSecretCommands.test.ts | 67 ++++++++++++- vscode/extension/test/providerSecrets.test.ts | 33 +++++++ vscode/extension/test/rpcServer.test.ts | 3 + 10 files changed, 297 insertions(+), 14 deletions(-) create mode 100644 vscode/extension/src/providerConfigurationUx.ts diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 8d0292a..bf10343 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -19,6 +19,7 @@ "onCommand:prole-coder.configureDeepSeekApiKey", "onCommand:prole-coder.clearDeepSeekApiKey", "onCommand:prole-coder.showProviderStatus", + "onCommand:prole-coder.selectDeepSeekModel", "onCommand:prole-coder.generateCommitMessage", "onCommand:prole-coder.generatePrDescription", "onView:prole-coder.chat", @@ -47,6 +48,10 @@ "command": "prole-coder.showProviderStatus", "title": "ProleCoder: Show Provider Status" }, + { + "command": "prole-coder.selectDeepSeekModel", + "title": "ProleCoder: Select DeepSeek Model" + }, { "command": "prole-coder.generateCommitMessage", "title": "ProleCoder: Generate Commit Message" @@ -145,6 +150,13 @@ "minimum": 1000, "maximum": 100000, "description": "Maximum prefix and suffix characters sent to the RPC server for a FIM preview." + }, + "prole-coder.provider.model": { + "type": "string", + "default": "", + "enum": ["", "deepseek-v4-pro", "deepseek-v4-flash"], + "enumItemLabels": ["Provider default", "DeepSeek V4 Pro", "DeepSeek V4 Flash"], + "description": "Optional DeepSeek model id used for chat and agent turns. Leave empty to use the provider default or DEEPSEEK_MODEL from the environment." } } } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index b1137e1..9091272 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -25,6 +25,7 @@ import { deepSeekEnvOverride, providerSecretRedactionValues, resolveDeepSeekApiKey, + resolveDeepSeekModel, } from "./providerSecrets"; import { MutableSecretRedactor } from "./redaction"; import { RpcServerManager, readRpcServerLaunchConfig } from "./rpcServer"; @@ -38,11 +39,17 @@ export async function activate(context: vscode.ExtensionContext): Promise secretValue: await context.secrets.get(DEEPSEEK_API_KEY_SECRET_ID), processEnv: process.env, }); + const providerConfiguration = vscode.workspace.getConfiguration("prole-coder.provider"); + const configuredModel = providerConfiguration.get("model", ""); + const initialModelId = resolveDeepSeekModel({ + configuredModel: typeof configuredModel === "string" ? configuredModel : "", + processEnv: process.env, + }); secretRedactor.update(providerSecretRedactionValues(initialSecretStatus)); const notifier = createExtensionNotifier(logger, vscode.window, secretRedactor); context.subscriptions.push(outputChannel); - const rpcServer = createRpcServerManager(context, notifier, deepSeekEnvOverride(initialSecretStatus)); + const rpcServer = createRpcServerManager(context, notifier, deepSeekEnvOverride(initialSecretStatus, initialModelId)); const chatView = new ProleChatViewProvider(context.extensionUri, rpcServer, workspaceRoot, logger, secretRedactor); const chatParticipant = registerProleChatParticipant(context, rpcServer, workspaceRoot, logger, secretRedactor); const openChat = registerOpenChatCommand( @@ -79,6 +86,8 @@ export async function activate(context: vscode.ExtensionContext): Promise redactor: secretRedactor, rpcServer, isRpcIdle: () => chatView.isIdle(), + providerConfiguration, + configurationTarget: vscode.ConfigurationTarget.Global, }); const gitRepositoryProvider = createVscodeGitRepositoryProvider(); const markdownSink = createVscodeMarkdownSink(); diff --git a/vscode/extension/src/providerConfigurationUx.ts b/vscode/extension/src/providerConfigurationUx.ts new file mode 100644 index 0000000..42a3ff5 --- /dev/null +++ b/vscode/extension/src/providerConfigurationUx.ts @@ -0,0 +1,6 @@ +export const CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL = "Configure API Key"; + +export function isDeepSeekApiKeyRequiredMessage(message: string): boolean { + const normalized = message.toLowerCase(); + return normalized.includes("deepseek") && normalized.includes("deepseek_api_key") && normalized.includes("required"); +} diff --git a/vscode/extension/src/providerSecretCommands.ts b/vscode/extension/src/providerSecretCommands.ts index 360ac26..a82b027 100644 --- a/vscode/extension/src/providerSecretCommands.ts +++ b/vscode/extension/src/providerSecretCommands.ts @@ -1,9 +1,13 @@ import { DEEPSEEK_API_KEY_SECRET_ID, + DEEPSEEK_MODEL_OPTIONS, deepSeekEnvOverride, + formatDeepSeekModelStatus, formatProviderSecretStatus, + normalizeDeepSeekModelId, providerSecretRedactionValues, resolveDeepSeekApiKey, + resolveDeepSeekModel, type DeepSeekSecretResolution, } from "./providerSecrets"; import type { MutableSecretRedactor } from "./redaction"; @@ -11,6 +15,7 @@ import type { MutableSecretRedactor } from "./redaction"; export const CONFIGURE_DEEPSEEK_API_KEY_COMMAND = "prole-coder.configureDeepSeekApiKey"; export const CLEAR_DEEPSEEK_API_KEY_COMMAND = "prole-coder.clearDeepSeekApiKey"; export const SHOW_PROVIDER_STATUS_COMMAND = "prole-coder.showProviderStatus"; +export const SELECT_DEEPSEEK_MODEL_COMMAND = "prole-coder.selectDeepSeekModel"; export interface CommandRegistry { registerCommand(command: string, callback: () => unknown): DisposableLike; @@ -33,10 +38,29 @@ export interface SecretWindowMessenger { readonly prompt: string; readonly title: string; }): string | undefined | PromiseLike; + showQuickPick( + items: readonly T[], + options: { + readonly ignoreFocusOut: true; + readonly placeHolder: string; + readonly title: string; + }, + ): T | undefined | PromiseLike; showInformationMessage(message: string): unknown; showWarningMessage(message: string): unknown; } +export interface SecretQuickPickItem { + readonly label: string; + readonly description?: string; + readonly detail?: string; +} + +export interface ProviderConfigurationLike { + get(section: string, defaultValue: T): T; + update(section: string, value: unknown, configurationTarget?: unknown): PromiseLike | void; +} + export interface ProviderSecretRpcServer { readonly status: string; setProcessEnv(env: Record): void; @@ -52,10 +76,13 @@ export interface RegisterProviderSecretCommandOptions { readonly redactor?: MutableSecretRedactor; readonly rpcServer?: ProviderSecretRpcServer | undefined; readonly isRpcIdle?: () => boolean; + readonly providerConfiguration?: ProviderConfigurationLike | undefined; + readonly configurationTarget?: unknown; } export interface ProviderSecretRuntimeState { status: DeepSeekSecretResolution; + modelId?: string | undefined; } export function registerProviderSecretCommands( @@ -63,6 +90,7 @@ export function registerProviderSecretCommands( ): readonly DisposableLike[] { const state: ProviderSecretRuntimeState = { status: resolveDeepSeekApiKey({ processEnv: options.processEnv }), + modelId: currentDeepSeekModel(options), }; void refreshProviderSecretState(options, state); @@ -97,13 +125,40 @@ export function registerProviderSecretCommands( }), options.commands.registerCommand(SHOW_PROVIDER_STATUS_COMMAND, async () => { const status = await refreshProviderSecretState(options, state); - return options.window.showInformationMessage(formatProviderSecretStatus(status)); + return options.window.showInformationMessage( + `${formatProviderSecretStatus(status)}; ${formatDeepSeekModelStatus(state.modelId)}`, + ); + }), + options.commands.registerCommand(SELECT_DEEPSEEK_MODEL_COMMAND, async () => { + const item = await options.window.showQuickPick(deepSeekModelPickItems(state.modelId), { + title: "Select DeepSeek Model", + placeHolder: "Choose the DeepSeek model used for chat and agent turns.", + ignoreFocusOut: true, + }); + if (item === undefined) { + return undefined; + } + + const modelId = normalizeDeepSeekModelId(item.modelId); + if (modelId === undefined) { + return options.window.showWarningMessage("DeepSeek model was not changed."); + } + + state.modelId = modelId; + await options.providerConfiguration?.update("model", modelId, options.configurationTarget); + const status = await refreshProviderSecretState(options, state); + await restartRpcAfterProviderChange(options, "DeepSeek model updated.", "DeepSeek model updated"); + options.window.showInformationMessage(formatDeepSeekModelStatus(modelId)); + return status; }), ]; } export async function refreshProviderSecretState( - options: Pick, + options: Pick< + RegisterProviderSecretCommandOptions, + "processEnv" | "providerConfiguration" | "redactor" | "rpcServer" | "secrets" + >, state?: ProviderSecretRuntimeState, ): Promise { const secretValue = await options.secrets.get(DEEPSEEK_API_KEY_SECRET_ID); @@ -111,17 +166,20 @@ export async function refreshProviderSecretState( secretValue, processEnv: options.processEnv, }); - options.rpcServer?.setProcessEnv(deepSeekEnvOverride(status)); + const modelId = state?.modelId ?? currentDeepSeekModel(options); + options.rpcServer?.setProcessEnv(deepSeekEnvOverride(status, modelId)); options.redactor?.update(providerSecretRedactionValues(status)); if (state !== undefined) { state.status = status; + state.modelId = modelId; } return status; } -async function restartRpcAfterSecretChange( +async function restartRpcAfterProviderChange( options: Pick, prefix: string, + failurePrefix: string, ): Promise { const rpcServer = options.rpcServer; if (rpcServer === undefined || rpcServer.status === "stopped" || rpcServer.status === "failed") { @@ -143,11 +201,42 @@ async function restartRpcAfterSecretChange( await rpcServer.start(); } catch (error) { options.window.showWarningMessage( - redactMessage(`DeepSeek API key updated, but RPC restart failed: ${errorMessage(error)}`, options.redactor), + redactMessage(`${failurePrefix}, but RPC restart failed: ${errorMessage(error)}`, options.redactor), ); } } +async function restartRpcAfterSecretChange( + options: Pick, + prefix: string, +): Promise { + await restartRpcAfterProviderChange(options, prefix, "DeepSeek API key updated"); +} + +function currentDeepSeekModel( + options: Pick, +): string | undefined { + const configuredModel = options.providerConfiguration?.get("model", ""); + return resolveDeepSeekModel({ + configuredModel: typeof configuredModel === "string" ? configuredModel : "", + processEnv: options.processEnv, + }); +} + +interface DeepSeekModelPickItem extends SecretQuickPickItem { + readonly modelId: string; +} + +function deepSeekModelPickItems(currentModelId: string | undefined): readonly DeepSeekModelPickItem[] { + const current = normalizeDeepSeekModelId(currentModelId); + return DEEPSEEK_MODEL_OPTIONS.map((model) => ({ + label: model.displayName, + description: model.id === current ? `${model.id} (current)` : model.id, + detail: model.detail, + modelId: model.id, + })); +} + function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } diff --git a/vscode/extension/src/providerSecrets.ts b/vscode/extension/src/providerSecrets.ts index c5c2f32..d5410c5 100644 --- a/vscode/extension/src/providerSecrets.ts +++ b/vscode/extension/src/providerSecrets.ts @@ -1,5 +1,25 @@ export const DEEPSEEK_API_KEY_SECRET_ID = "prole-coder.deepseek-api-key"; export const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY"; +export const DEEPSEEK_MODEL_ENV = "DEEPSEEK_MODEL"; + +export interface DeepSeekModelOption { + readonly id: string; + readonly displayName: string; + readonly detail: string; +} + +export const DEEPSEEK_MODEL_OPTIONS: readonly DeepSeekModelOption[] = [ + { + id: "deepseek-v4-pro", + displayName: "DeepSeek V4 Pro", + detail: "1M context, 384K max output", + }, + { + id: "deepseek-v4-flash", + displayName: "DeepSeek V4 Flash", + detail: "1M context, 384K max output", + }, +]; export type ProviderSecretSource = "missing" | "process-env" | "secret-storage"; @@ -14,6 +34,11 @@ export interface ResolveDeepSeekApiKeyInput { readonly processEnv?: Record | undefined; } +export interface ResolveDeepSeekModelInput { + readonly configuredModel?: string | undefined; + readonly processEnv?: Record | undefined; +} + export function resolveDeepSeekApiKey(input: ResolveDeepSeekApiKeyInput): DeepSeekSecretResolution { const secretValue = normalizedSecret(input.secretValue); if (secretValue !== undefined) { @@ -41,8 +66,17 @@ export function resolveDeepSeekApiKey(input: ResolveDeepSeekApiKeyInput): DeepSe export function deepSeekEnvOverride( resolution: DeepSeekSecretResolution, + modelId?: string | undefined, ): Record { - return resolution.apiKey === undefined ? {} : { [DEEPSEEK_API_KEY_ENV]: resolution.apiKey }; + const env: Record = {}; + const normalizedModelId = normalizeDeepSeekModelId(modelId); + if (resolution.apiKey !== undefined) { + env[DEEPSEEK_API_KEY_ENV] = resolution.apiKey; + } + if (normalizedModelId !== undefined) { + env[DEEPSEEK_MODEL_ENV] = normalizedModelId; + } + return env; } export function providerSecretRedactionValues( @@ -61,6 +95,29 @@ export function formatProviderSecretStatus(resolution: DeepSeekSecretResolution) return `DeepSeek API key: ${source}`; } +export function resolveDeepSeekModel(input: ResolveDeepSeekModelInput): string | undefined { + return ( + normalizeDeepSeekModelId(input.configuredModel) ?? + normalizeDeepSeekModelId(input.processEnv?.[DEEPSEEK_MODEL_ENV]) + ); +} + +export function normalizeDeepSeekModelId(value: string | undefined): string | undefined { + const normalized = value?.trim(); + return normalized === undefined || normalized.length === 0 ? undefined : normalized; +} + +export function formatDeepSeekModelStatus(modelId: string | undefined): string { + const normalized = normalizeDeepSeekModelId(modelId); + if (normalized === undefined) { + return "DeepSeek model: provider default"; + } + const option = DEEPSEEK_MODEL_OPTIONS.find((model) => model.id === normalized); + return option === undefined + ? `DeepSeek model: ${normalized}` + : `DeepSeek model: ${option.displayName} (${option.id})`; +} + function normalizedSecret(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized === undefined || normalized.length === 0 ? undefined : normalized; diff --git a/vscode/extension/src/rpcServer.ts b/vscode/extension/src/rpcServer.ts index 4ccee60..1a66716 100644 --- a/vscode/extension/src/rpcServer.ts +++ b/vscode/extension/src/rpcServer.ts @@ -273,8 +273,8 @@ export class RpcServerManager implements DisposableLike { this.child = child; child.stdout.on("data", (chunk) => this.handleStdoutData(chunk)); child.stderr.on("data", (chunk) => this.handleStderrData(chunk)); - child.on("exit", (code, signal) => this.handleExit(code, signal)); - child.on("error", (error) => this.handleProcessError(error)); + child.on("exit", (code, signal) => this.handleExit(child, code, signal)); + child.on("error", (error) => this.handleProcessError(child, error)); this.startPromise = new Promise((resolve, reject) => { this.resolveStart = resolve; @@ -506,7 +506,15 @@ export class RpcServerManager implements DisposableLike { this.stderrTail = `${this.stderrTail}${chunk.toString()}`.slice(-4096); } - private handleExit(code: number | null, signal: NodeJS.Signals | null): void { + private handleExit( + exitedChild: RpcChildProcess, + code: number | null, + signal: NodeJS.Signals | null, + ): void { + if (this.child !== exitedChild) { + return; + } + const wasIntentional = this.intentionalStop; this.child = undefined; this.readyState = undefined; @@ -537,7 +545,11 @@ export class RpcServerManager implements DisposableLike { } } - private handleProcessError(error: Error): void { + private handleProcessError(processChild: RpcChildProcess, error: Error): void { + if (this.child !== processChild) { + return; + } + if (this.currentStatus === "starting") { this.failStarting(error); return; diff --git a/vscode/extension/test/electron/index.ts b/vscode/extension/test/electron/index.ts index eff4c66..40c935f 100644 --- a/vscode/extension/test/electron/index.ts +++ b/vscode/extension/test/electron/index.ts @@ -12,6 +12,7 @@ const CONTRIBUTED_COMMANDS = [ "prole-coder.configureDeepSeekApiKey", "prole-coder.clearDeepSeekApiKey", "prole-coder.showProviderStatus", + "prole-coder.selectDeepSeekModel", "prole-coder.generateCommitMessage", "prole-coder.generatePrDescription", ] as const; diff --git a/vscode/extension/test/providerSecretCommands.test.ts b/vscode/extension/test/providerSecretCommands.test.ts index b0881af..8e4c6a7 100644 --- a/vscode/extension/test/providerSecretCommands.test.ts +++ b/vscode/extension/test/providerSecretCommands.test.ts @@ -4,10 +4,15 @@ import test from "node:test"; import { CLEAR_DEEPSEEK_API_KEY_COMMAND, CONFIGURE_DEEPSEEK_API_KEY_COMMAND, + SELECT_DEEPSEEK_MODEL_COMMAND, SHOW_PROVIDER_STATUS_COMMAND, registerProviderSecretCommands, } from "../src/providerSecretCommands.js"; -import { DEEPSEEK_API_KEY_ENV, DEEPSEEK_API_KEY_SECRET_ID } from "../src/providerSecrets.js"; +import { + DEEPSEEK_API_KEY_ENV, + DEEPSEEK_API_KEY_SECRET_ID, + DEEPSEEK_MODEL_ENV, +} from "../src/providerSecrets.js"; import { MutableSecretRedactor } from "../src/redaction.js"; test("configure DeepSeek API key stores SecretStorage value, updates env, and restarts idle RPC", async () => { @@ -85,7 +90,39 @@ test("show provider status reports missing configuration without exposing keys", await commands.run(SHOW_PROVIDER_STATUS_COMMAND); - assert.deepEqual(window.infos.at(-1), "DeepSeek API key: missing"); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: missing; DeepSeek model: provider default"); +}); + +test("select DeepSeek model stores configuration, updates env, and restarts idle RPC", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([], ["DeepSeek V4 Flash"]); + const secrets = new FakeSecrets({ + [DEEPSEEK_API_KEY_SECRET_ID]: "fixture-secret-value", + }); + const configuration = new FakeProviderConfiguration({ + model: "deepseek-v4-pro", + }); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + providerConfiguration: configuration, + rpcServer: rpc, + isRpcIdle: () => true, + }); + + await commands.run(SELECT_DEEPSEEK_MODEL_COMMAND); + + assert.equal(configuration.values["model"], "deepseek-v4-flash"); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "fixture-secret-value", + [DEEPSEEK_MODEL_ENV]: "deepseek-v4-flash", + }); + assert.equal(rpc.stopCount, 1); + assert.equal(rpc.startCount, 1); + assert.deepEqual(window.infos.at(-1), "DeepSeek model: DeepSeek V4 Flash (deepseek-v4-flash)"); }); test("configure DeepSeek API key redacts restart failures", async () => { @@ -127,12 +164,23 @@ class FakeSecretWindow { readonly infos: string[] = []; readonly warnings: string[] = []; - constructor(private readonly inputValues: string[]) {} + constructor( + private readonly inputValues: string[], + private readonly quickPickLabels: string[] = [], + ) {} showInputBox(): string | undefined { return this.inputValues.shift(); } + showQuickPick(items: readonly T[]): T | undefined { + const label = this.quickPickLabels.shift(); + if (label === undefined) { + return undefined; + } + return items.find((item) => item.label === label); + } + showInformationMessage(message: string): void { this.infos.push(message); } @@ -158,6 +206,19 @@ class FakeSecrets { } } +class FakeProviderConfiguration { + constructor(readonly values: Record = {}) {} + + get(section: string, defaultValue: T): T { + const value = this.values[section]; + return value === undefined ? defaultValue : (value as T); + } + + update(section: string, value: unknown): void { + this.values[section] = value; + } +} + class FakeRpcServer { processEnv: Record = {}; startCount = 0; diff --git a/vscode/extension/test/providerSecrets.test.ts b/vscode/extension/test/providerSecrets.test.ts index 8ec18b1..4902955 100644 --- a/vscode/extension/test/providerSecrets.test.ts +++ b/vscode/extension/test/providerSecrets.test.ts @@ -3,10 +3,13 @@ import test from "node:test"; import { DEEPSEEK_API_KEY_ENV, + DEEPSEEK_MODEL_ENV, deepSeekEnvOverride, + formatDeepSeekModelStatus, formatProviderSecretStatus, providerSecretRedactionValues, resolveDeepSeekApiKey, + resolveDeepSeekModel, } from "../src/providerSecrets.js"; test("resolveDeepSeekApiKey prefers SecretStorage over process env", () => { @@ -22,6 +25,10 @@ test("resolveDeepSeekApiKey prefers SecretStorage over process env", () => { assert.deepEqual(deepSeekEnvOverride(resolution), { [DEEPSEEK_API_KEY_ENV]: "secret-storage-key", }); + assert.deepEqual(deepSeekEnvOverride(resolution, " deepseek-v4-flash "), { + [DEEPSEEK_API_KEY_ENV]: "secret-storage-key", + [DEEPSEEK_MODEL_ENV]: "deepseek-v4-flash", + }); assert.deepEqual(providerSecretRedactionValues(resolution), ["secret-storage-key"]); assert.equal(formatProviderSecretStatus(resolution), "DeepSeek API key: VS Code SecretStorage"); }); @@ -49,3 +56,29 @@ test("resolveDeepSeekApiKey falls back to process env and reports missing", () = assert.deepEqual(providerSecretRedactionValues(missingResolution), []); assert.equal(formatProviderSecretStatus(missingResolution), "DeepSeek API key: missing"); }); + +test("resolveDeepSeekModel prefers configured model over process env", () => { + assert.equal( + resolveDeepSeekModel({ + configuredModel: " deepseek-v4-pro ", + processEnv: { + [DEEPSEEK_MODEL_ENV]: "deepseek-v4-flash", + }, + }), + "deepseek-v4-pro", + ); + assert.equal( + resolveDeepSeekModel({ + configuredModel: "", + processEnv: { + [DEEPSEEK_MODEL_ENV]: " deepseek-v4-flash ", + }, + }), + "deepseek-v4-flash", + ); + assert.equal(resolveDeepSeekModel({ configuredModel: " ", processEnv: {} }), undefined); + assert.equal( + formatDeepSeekModelStatus("deepseek-v4-pro"), + "DeepSeek model: DeepSeek V4 Pro (deepseek-v4-pro)", + ); +}); diff --git a/vscode/extension/test/rpcServer.test.ts b/vscode/extension/test/rpcServer.test.ts index 8e18aa0..a071839 100644 --- a/vscode/extension/test/rpcServer.test.ts +++ b/vscode/extension/test/rpcServer.test.ts @@ -96,12 +96,15 @@ test("RPC server manager forwards configured process environment overrides", asy const restarted = manager.start(); const restartedChild = factory.lastChild(); + child.exit(null, "SIGTERM"); restartedChild.stdout.pushJson(initializeResponse(restartedChild.initializeRequest().id)); await restarted; assert.deepEqual(factory.lastOptions?.env, { DEEPSEEK_API_KEY: "rotated-key", }); + assert.equal(restartedChild.killed, false); + assert.equal(manager.status, "ready"); }); test("RPC server manager forwards agent.event notifications", async () => { From 2f869ebce0fc1a291584da60d3a7f9f2bf768935 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 21:37:31 +0800 Subject: [PATCH 26/84] Adds automatic API key configuration prompt on missing key errors --- vscode/extension/src/chatParticipant.ts | 10 +- vscode/extension/src/chatView.ts | 150 +++++++++++++++++++++++- 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/vscode/extension/src/chatParticipant.ts b/vscode/extension/src/chatParticipant.ts index d0566f2..2db4d7b 100644 --- a/vscode/extension/src/chatParticipant.ts +++ b/vscode/extension/src/chatParticipant.ts @@ -16,6 +16,8 @@ import { } from "./chatParticipantCore"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; +import { isDeepSeekApiKeyRequiredMessage } from "./providerConfigurationUx"; +import { CONFIGURE_DEEPSEEK_API_KEY_COMMAND } from "./providerSecretCommands"; import type { MessageRedactor } from "./redaction"; export function registerProleChatParticipant( @@ -27,13 +29,13 @@ export function registerProleChatParticipant( ): vscode.Disposable { const participant = vscode.chat.createChatParticipant( CHAT_PARTICIPANT_ID, - (request, chatContext, response, token) => { + async (request, chatContext, response, token) => { const automaticContext = automaticContextAttachmentFromMessages( messagesFromChatHistory(chatContext.history), ); const diagnostics = collectDiagnosticAttachments(workspaceRoot); const attachments = mergeTurnAttachments(automaticContext, diagnostics); - return runChatParticipantTurn({ + const result = await runChatParticipantTurn({ ...(rpcClient === undefined ? {} : { rpcClient }), request: { prompt: request.prompt, @@ -45,6 +47,10 @@ export function registerProleChatParticipant( ...(redactor === undefined ? {} : { redactor }), token, }); + if (isDeepSeekApiKeyRequiredMessage(result.errorDetails?.message ?? "")) { + await vscode.commands.executeCommand(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + } + return result; }, ); participant.iconPath = vscode.Uri.joinPath(context.extensionUri, "media", "prole-coder-view.svg"); diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 1d59bba..2dfe45c 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -27,6 +27,14 @@ import { } from "./contextViz"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; +import { + CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + isDeepSeekApiKeyRequiredMessage, +} from "./providerConfigurationUx"; +import { + CONFIGURE_DEEPSEEK_API_KEY_COMMAND, + SELECT_DEEPSEEK_MODEL_COMMAND, +} from "./providerSecretCommands"; import type { MessageRedactor } from "./redaction"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; import { @@ -90,6 +98,11 @@ type ExtensionToWebviewMessage = type ChatSubmissionStatus = "idle" | "sending" | "running" | "completed" | "failed" | "canceled"; type TerminalSubmissionStatus = Extract; +export interface ChatSubmissionAction { + readonly type: "configureDeepSeekApiKey"; + readonly label: string; +} + export interface ChatSubmissionSnapshot { readonly busy: boolean; readonly status: ChatSubmissionStatus; @@ -98,6 +111,7 @@ export interface ChatSubmissionSnapshot { readonly turnId?: string; readonly error?: string; readonly canceling?: boolean; + readonly action?: ChatSubmissionAction; } interface TerminalRunState { @@ -220,6 +234,16 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } + if (isConfigureDeepSeekApiKeyMessage(message)) { + await this.configureDeepSeekApiKey(); + return; + } + + if (isSelectDeepSeekModelMessage(message)) { + await this.selectDeepSeekModel(); + return; + } + if (!isRecord(message) || message["type"] !== "submitTurn") { return; } @@ -285,13 +309,18 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } catch (error) { const messageText = `Failed to send turn: ${errorMessage(error)}`; const redacted = this.redact(messageText); + const missingApiKey = isDeepSeekApiKeyRequiredMessage(messageText); this.logger?.error(redacted); this.setSubmission({ ...idleSubmission(), status: "failed", message: redacted, error: redacted, + ...(missingApiKey ? { action: configureDeepSeekApiKeyAction() } : {}), }); + if (missingApiKey) { + await this.configureDeepSeekApiKey(); + } } } @@ -486,6 +515,14 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private redact(message: string): string { return this.redactor?.redact(message) ?? message; } + + private async configureDeepSeekApiKey(): Promise { + await vscode.commands.executeCommand(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + } + + private async selectDeepSeekModel(): Promise { + await vscode.commands.executeCommand(SELECT_DEEPSEEK_MODEL_COMMAND); + } } function renderChatViewHtml( @@ -913,6 +950,7 @@ function renderChatViewHtml( .mode:focus, .send:focus, .cancel:focus, + .provider-action:focus, .refresh-runs:focus, .run-entry:focus, .context-tab:focus { @@ -923,6 +961,7 @@ function renderChatViewHtml( .composer-row { display: flex; align-items: center; + flex-wrap: wrap; gap: 8px; min-width: 0; } @@ -959,10 +998,26 @@ function renderChatViewHtml( font-weight: 500; } + .provider-action { + flex: 0 0 auto; + min-width: 58px; + height: 28px; + padding: 0 8px; + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border: 0; + font: var(--vscode-font-size) var(--vscode-font-family); + font-weight: 500; + } + .cancel:hover:enabled { background: var(--vscode-button-secondaryHoverBackground); } + .provider-action:hover:enabled { + background: var(--vscode-button-secondaryHoverBackground); + } + .send:disabled, .cancel:disabled, .prompt:disabled, @@ -972,14 +1027,39 @@ function renderChatViewHtml( .submission { min-width: 0; - flex: 1; + flex: 1 1 100%; color: var(--vscode-descriptionForeground); + display: flex; + align-items: center; + gap: 6px; font-size: 12px; overflow: hidden; + white-space: nowrap; + } + + .submission-message { + min-width: 0; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .submission-action { + flex: 0 0 auto; + border: 0; + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + border-radius: 3px; + cursor: pointer; + font-size: 11px; + line-height: 18px; + padding: 0 8px; + } + + .submission-action:hover { + background: var(--vscode-button-hoverBackground); + } + .submission.failed { color: var(--vscode-editorError-foreground); } @@ -1019,6 +1099,8 @@ function renderChatViewHtml(
+ +
@@ -1044,6 +1126,8 @@ function renderChatViewHtml( const composer = document.getElementById("composer"); const promptInput = document.getElementById("prompt"); const modeInput = document.getElementById("mode"); + const apiKeyButton = document.getElementById("api-key"); + const modelButton = document.getElementById("model"); const sendButton = document.getElementById("send"); const cancelButton = document.getElementById("cancel"); const submissionRoot = document.getElementById("submission"); @@ -1078,6 +1162,14 @@ function renderChatViewHtml( vscodeApi.postMessage({ type: "refreshRuns" }); }); + apiKeyButton.addEventListener("click", () => { + vscodeApi.postMessage({ type: "configureDeepSeekApiKey" }); + }); + + modelButton.addEventListener("click", () => { + vscodeApi.postMessage({ type: "selectDeepSeekModel" }); + }); + cancelButton.addEventListener("click", () => { const runId = cancelButton.dataset.runId; if (typeof runId === "string" && runId.length > 0) { @@ -1448,13 +1540,52 @@ function renderChatViewHtml( cancelButton.dataset.runId = runId; submissionRoot.className = "submission " + status; const submissionMessage = typeof state.message === "string" ? state.message : ""; - submissionRoot.textContent = submissionMessage; submissionRoot.title = typeof state.error === "string" ? state.error : submissionMessage; + submissionRoot.replaceChildren(); + const message = document.createElement("span"); + message.className = "submission-message"; + message.textContent = submissionMessage; + submissionRoot.append(message); + const actionButton = renderSubmissionAction(state.action); + if (actionButton) { + submissionRoot.append(actionButton); + } if (status === "running") { promptInput.value = ""; } } + function renderSubmissionAction(action) { + const definition = submissionActionDefinition(action); + if (definition === undefined) { + return undefined; + } + + const button = document.createElement("button"); + button.type = "button"; + button.className = "submission-action"; + button.textContent = typeof action.label === "string" ? action.label : definition.fallbackLabel; + button.addEventListener("click", () => { + vscodeApi.postMessage({ type: definition.messageType }); + }); + return button; + } + + function submissionActionDefinition(action) { + if (!action || typeof action !== "object") { + return undefined; + } + + if (action.type === "configureDeepSeekApiKey") { + return { + fallbackLabel: "Configure API Key", + messageType: "configureDeepSeekApiKey", + }; + } + + return undefined; + } + function setComposerBusy(busy, cancelable) { promptInput.disabled = busy; modeInput.disabled = busy; @@ -1511,6 +1642,13 @@ function idleSubmission(): ChatSubmissionSnapshot { }; } +function configureDeepSeekApiKeyAction(): ChatSubmissionAction { + return { + type: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }; +} + function terminalSubmission( runId: string, turnId: string | undefined, @@ -1568,6 +1706,14 @@ function cancelRunIdFromMessage(message: unknown): string | undefined { return typeof runId === "string" && runId.length > 0 ? runId : undefined; } +function isConfigureDeepSeekApiKeyMessage(message: unknown): boolean { + return isRecord(message) && message["type"] === "configureDeepSeekApiKey"; +} + +function isSelectDeepSeekModelMessage(message: unknown): boolean { + return isRecord(message) && message["type"] === "selectDeepSeekModel"; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } From de2ad3515ddb3b797f66ee630639b2e4de95e470 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 21:37:31 +0800 Subject: [PATCH 27/84] Fixes shell tool to accept '.' as workspace root cwd --- crates/agent-core/src/tool_execution.rs | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/agent-core/src/tool_execution.rs b/crates/agent-core/src/tool_execution.rs index 6cf6b5f..cad054e 100644 --- a/crates/agent-core/src/tool_execution.rs +++ b/crates/agent-core/src/tool_execution.rs @@ -535,6 +535,10 @@ impl WorkspaceToolExecutor { fn resolve_workspace_path(&self, relative: &str) -> Result { let normalized = normalize_workspace_relative_path(relative)?; reject_sensitive_path(&normalized)?; + if normalized == "." { + return Ok(self.root.clone()); + } + let path = self.root.join(Path::new(&normalized)); let parent = path.parent().unwrap_or(&self.root); let canonical_parent = @@ -556,6 +560,10 @@ impl WorkspaceToolExecutor { relative: &str, ) -> Result { let path = self.resolve_workspace_path(relative)?; + if path == self.root { + return Ok(self.root.clone()); + } + let canonical = fs::canonicalize(&path).map_err(|source| ToolExecutionError::Io { path: path.clone(), source, @@ -1960,6 +1968,28 @@ mod tests { assert!(result.stdout.contains("hello")); } + #[test] + fn shell_accepts_dot_cwd_as_workspace_root() { + let workspace = TestWorkspace::new("tool-execution"); + let tools = WorkspaceToolExecutor::new(workspace.path()).expect("workspace should open"); + + #[cfg(windows)] + let command = "Write-Output hello"; + #[cfg(not(windows))] + let command = "printf hello"; + + let result = tools + .shell(ShellArgs { + command: command.to_owned(), + cwd: Some(".".to_owned()), + timeout_ms: Some(10_000), + }) + .expect("dot cwd should resolve to the workspace root"); + + assert_eq!(result.status, ToolStatus::Ok); + assert!(result.stdout.contains("hello")); + } + #[test] fn shell_cancels_running_command() { let workspace = TestWorkspace::new("tool-execution"); From c1be814248dee7a44d1a1b53e47d005547f90d54 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 21:37:31 +0800 Subject: [PATCH 28/84] Refactors RPC test helpers to support agent.eventBatch events --- crates/agent-rpc/src/lib.rs | 122 ++++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 53 deletions(-) diff --git a/crates/agent-rpc/src/lib.rs b/crates/agent-rpc/src/lib.rs index f0271df..b3730bf 100644 --- a/crates/agent-rpc/src/lib.rs +++ b/crates/agent-rpc/src/lib.rs @@ -3690,9 +3690,9 @@ mod tests { output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["runId"] == "run_real_rpc" + line_has_agent_event(line, |event| { + event["type"] == "run.completed" && event["runId"] == "run_real_rpc" + }) }, RPC_TEST_TIMEOUT, ); @@ -3730,12 +3730,12 @@ mod tests { assert_eq!(lines[1]["result"]["turnId"], "turn_1"); let turn_response_index = line_index(&lines, |line| line["id"] == "turn_1"); let run_started_index = line_index(&lines, |line| { - line["method"] == "agent.event" && line["params"]["type"] == "run.started" + line_has_agent_event(line, |event| event["type"] == "run.started") }); assert!(turn_response_index < run_started_index); - assert!(lines.iter().any(|line| line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["payload"]["summary"] == "RPC final answer")); + let events = agent_event_values(&lines); + assert!(events.iter().any(|event| event["type"] == "run.completed" + && event["payload"]["summary"] == "RPC final answer")); assert!(lines.iter().any(|line| { line["id"] == "resume_1" && line["result"]["nextSeq"] @@ -3807,18 +3807,18 @@ mod tests { output.wait_for_line(|line| line["id"] == "turn_1", RPC_TEST_TIMEOUT); assert!( - !output.lines().iter().any(|line| { - line["method"] == "agent.event" && line["params"]["type"] == "run.completed" - }), + !agent_event_values(&output.lines()) + .iter() + .any(|event| event["type"] == "run.completed"), "sendTurn response must be written before the blocked provider completes" ); gate.release(); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["runId"] == "run_rpc_early_accept" + line_has_agent_event(line, |event| { + event["type"] == "run.completed" && event["runId"] == "run_rpc_early_accept" + }) }, RPC_TEST_TIMEOUT, ); @@ -3858,9 +3858,9 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalRequired" - && line["params"]["runId"] == "run_rpc_approval" + line_has_agent_event(line, |event| { + event["type"] == "tool.approvalRequired" && event["runId"] == "run_rpc_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -3878,9 +3878,9 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["runId"] == "run_rpc_approval" + line_has_agent_event(line, |event| { + event["type"] == "run.completed" && event["runId"] == "run_rpc_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -3891,19 +3891,24 @@ mod tests { assert_eq!(workspace.read("README.md"), "new\n"); let lines = output.lines(); + let events = agent_event_values(&lines); let approval_required_index = line_index(&lines, |line| { - line["method"] == "agent.event" && line["params"]["type"] == "tool.approvalRequired" + line_has_agent_event(line, |event| event["type"] == "tool.approvalRequired") }); let approve_response_index = line_index(&lines, |line| line["id"] == "approve_1"); let approval_resolved_index = line_index(&lines, |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalResolved" - && line["params"]["payload"]["decision"] == "approved" + line_has_agent_event(line, |event| { + event["type"] == "tool.approvalResolved" + && event["payload"]["decision"] == "approved" + }) }); assert!(approval_required_index < approve_response_index); assert!(approve_response_index < approval_resolved_index); - let approval_payload = &lines[approval_required_index]["params"]["payload"]; + let approval_payload = &events + .iter() + .find(|event| event["type"] == "tool.approvalRequired") + .expect("approval required event should exist")["payload"]; assert_eq!(approval_payload["approvalId"], "approval_1_1"); assert_eq!(approval_payload["toolCallId"], "call_patch"); assert_eq!(approval_payload["toolName"], "apply_patch"); @@ -3918,10 +3923,9 @@ mod tests { lines[approve_response_index]["result"]["persist"], "session" ); - assert!(lines.iter().any(|line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["payload"]["changedFiles"] == json!(["README.md"]) + assert!(events.iter().any(|event| { + event["type"] == "run.completed" + && event["payload"]["changedFiles"] == json!(["README.md"]) })); } @@ -3955,9 +3959,10 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalRequired" - && line["params"]["runId"] == "run_rpc_rejected_approval" + line_has_agent_event(line, |event| { + event["type"] == "tool.approvalRequired" + && event["runId"] == "run_rpc_rejected_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -3975,9 +3980,9 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.failed" - && line["params"]["runId"] == "run_rpc_rejected_approval" + line_has_agent_event(line, |event| { + event["type"] == "run.failed" && event["runId"] == "run_rpc_rejected_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -3988,20 +3993,15 @@ mod tests { assert_eq!(workspace.read("README.md"), "old\n"); let lines = output.lines(); + let events = agent_event_values(&lines); let reject_response_index = line_index(&lines, |line| line["id"] == "reject_1"); assert_eq!(lines[reject_response_index]["result"]["state"], "rejected"); assert_eq!(lines[reject_response_index]["result"]["reason"], "not now"); - assert!(lines.iter().any(|line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalResolved" - && line["params"]["payload"]["decision"] == "rejected" - })); - assert!(lines.iter().any(|line| { - line["method"] == "agent.event" && line["params"]["type"] == "run.failed" - })); - assert!(!lines.iter().any(|line| { - line["method"] == "agent.event" && line["params"]["type"] == "tool.started" + assert!(events.iter().any(|event| { + event["type"] == "tool.approvalResolved" && event["payload"]["decision"] == "rejected" })); + assert!(events.iter().any(|event| event["type"] == "run.failed")); + assert!(!events.iter().any(|event| event["type"] == "tool.started")); } #[test] @@ -4034,9 +4034,10 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalRequired" - && line["params"]["runId"] == "run_rpc_canceled_approval" + line_has_agent_event(line, |event| { + event["type"] == "tool.approvalRequired" + && event["runId"] == "run_rpc_canceled_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -4054,9 +4055,9 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.canceled" - && line["params"]["runId"] == "run_rpc_canceled_approval" + line_has_agent_event(line, |event| { + event["type"] == "run.canceled" && event["runId"] == "run_rpc_canceled_approval" + }) }, RPC_TEST_TIMEOUT, ); @@ -4158,9 +4159,10 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "tool.approvalRequired" - && line["params"]["runId"] == "run_rpc_active_disconnect" + line_has_agent_event(line, |event| { + event["type"] == "tool.approvalRequired" + && event["runId"] == "run_rpc_active_disconnect" + }) }, RPC_TEST_TIMEOUT, ); @@ -4628,6 +4630,20 @@ mod tests { events } + fn line_has_agent_event(line: &Value, predicate: impl Fn(&Value) -> bool) -> bool { + if line["method"] == "agent.event" { + return predicate(&line["params"]); + } + + if line["method"] == "agent.eventBatch" + && let Some(events) = line["params"]["events"].as_array() + { + return events.iter().any(predicate); + } + + false + } + fn line_index(lines: &[Value], predicate: impl Fn(&Value) -> bool) -> usize { lines .iter() From 7971647407a943f0406084a047e9c97556fb47e4 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Sun, 31 May 2026 21:37:31 +0800 Subject: [PATCH 29/84] Updates documentation to reflect Phase 5 progress and P5-12 task --- README.md | 13 +++++++------ docs/phase-tasks.md | 11 ++++++----- docs/roadmap.md | 9 +++++---- docs/security-model.md | 2 +- docs/testing.md | 8 ++++---- docs/vscode-extension.md | 14 ++++++++------ 6 files changed, 31 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0703570..df6359b 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线、Phase 4 的 VS Code 深度集成以及 Phase 5:VS Code Codex-like UX 与开发工作流均已完成。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 配置 DeepSeek API key、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key 配置和 Git 工作流均已完成。下一步进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 配置 DeepSeek API key、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-12 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -699,20 +699,21 @@ extension.ts - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 -- [x] P5-6:DeepSeek API key SecretStorage 与 provider status:插件提供 Configure/Clear API Key 和 provider status 入口;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 -- [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;配置/清除 key 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 +- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口,status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +- [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并保留修复按钮;配置/清除 key 或切换 model 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 - [x] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream 和 staged diff;git CLI 只作受控 fallback 采集 unstaged diff、diff stat、branch diff、commit summary 和 ref 检测,cwd 来自 repository root。 - [x] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 - [x] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版只提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 - [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 +- [ ] P5-12:结构化 provider 配置错误码与恢复动作:将缺 API key 等配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 验收标准: - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- 主审批弹窗保持简单;完整错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 条目已全部标记为 `[x]`,README 可把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 +- 主审批弹窗保持简单;Sidebar composer 提供 Key/Model 直接入口,完整错误诊断可在 `Output > ProleCoder` 查看。 +- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-12 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 @@ -734,7 +735,7 @@ extension.ts ## 安全与注意事项 -- API Key 只能从环境变量、系统密钥链或用户配置读取,不能进入 run log。 +- API Key 只能从环境变量、VS Code SecretStorage 或系统密钥链读取,不能进入 run log。 - 默认排除 `.env`、密钥、证书、浏览器配置、包管理器 token 和大型二进制文件。 - 运行命令前展示 cwd、命令、环境变量差异和风险等级。 - Windows、Linux、macOS 的沙箱能力不同,必须分别实现和测试。 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 0897a8b..922493b 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4、Phase 5 已完成。Phase 4 保留 14 项 VS Code 深度集成能力,Phase 5 记录 VS Code Codex-like UX 与开发工作流;下一阶段进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-11 已完成,P5-12 结构化 provider 配置错误码仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -102,14 +102,14 @@ | [x] | P4-7:Problems 面板诊断进入 Context Builder | `README.md`、`docs/vscode-extension.md`、`docs/context-capsule.md` | 已完成:VS Code 发送 turn 时采集 Problems 快照,并通过 `agent.sendTurn.attachments` 的 diagnostic attachment 注入;前端按协议 attachment 上限裁剪、优先保留 error,Core/Context Builder 继续负责 attachment 校验和 token 预算。 | | [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批弹窗不再暴露复杂持久化选项。 | | [x] | P4-9:审批持久化存储 | `docs/approval-model.md`、`docs/tool-system.md`、`docs/vscode-extension.md` | 已完成:RPC pending queue 支持 session 内存复用和 workspace `.prole-coder/approvals.v1.json` 存储;继续在 Core 和 RPC 层禁止 network/destructive 风险持久化。 | -| [x] | P4-10:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 仍只由 RPC server 环境读取。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P4-10:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 不进入 VS Code settings。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | P4-11:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code modal 提供 selected hunk quick pick;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | | [x] | P4-12:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | | [x] | P4-13:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:已完成。P5-1 到 P5-11 已全部完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key 配置、错误恢复、只读 Git context 和 GitLens-like commit / PR 文案生成工作流。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-11 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context 和 GitLens-like commit / PR 文案生成工作流;P5-12 结构化 provider 配置错误码与恢复动作未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -118,12 +118,13 @@ | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | -| [x] | P5-6:DeepSeek API key SecretStorage 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Show Provider Status` 命令;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`,不改变 CLI env 路径。 | -| [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;API key 配置/清除后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | +| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;provider status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | +| [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并在 Sidebar 错误状态保留修复按钮;API key 配置/清除或 model 切换后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | | [x] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | | [x] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | | [x] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | +| [ ] | P5-12:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码;VS Code Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口,避免依赖后端英文错误消息。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 83ccde0..2afa7fa 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # 路线图 -状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列、Phase 4 VS Code 深度集成和 Phase 5 VS Code Codex-like UX 与开发工作流已完成。 +状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列和 Phase 4 VS Code 深度集成已完成;Phase 5 VS Code Codex-like UX 与开发工作流进行中。 本文档把 README 中的大阶段拆成更可执行的优先级。README 保留项目入口和高层计划;这里记录跨模块的落地顺序、取舍和验收重点。具体任务的阶段、状态和来源统一登记在 `docs/phase-tasks.md`,阶段条目标记完成前应同步检查并更新该索引。 @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 已完成 VS Code Codex-like UX 与开发工作流。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内 API key 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成。TUI 随后进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage/provider status、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛。 +- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage/provider status、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 @@ -119,6 +119,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P5-2 后 VS Code 主审批弹窗保持 Approve / Reject。 - 审批持久化存储已支持 session/workspace,继续禁止 network/destructive 风险持久化。 - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 +- P5-12:provider 配置失败应从前端字符串匹配升级为 RPC 结构化错误码,例如缺少 API key 时返回可恢复的错误码,供 VS Code/TUI 统一展示配置动作。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 @@ -132,7 +133,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目和 Phase 5 的 11 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;README 可把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-12 未完成,README 不能把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/security-model.md b/docs/security-model.md index 29fc422..c60029d 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -7,7 +7,7 @@ ## 边界 - API Key 不得进入 run log。 -- VS Code 插件中的 DeepSeek API key 存入 SecretStorage,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 `DEEPSEEK_API_KEY`。 +- VS Code 插件中的 DeepSeek API key 存入 SecretStorage,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 `DEEPSEEK_API_KEY`。DeepSeek model ID 不是密钥,可通过 VS Code 普通设置保存,并在 child env 中覆盖 `DEEPSEEK_MODEL`。 - `.env` 和本地状态必须被 git 忽略。 - tool call 执行前必须校验 schema。 - 写入应通过 patch application。 diff --git a/docs/testing.md b/docs/testing.md index e790f1f..fad4872 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -79,13 +79,13 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 -Phase 5 P5-6 到 P5-11 的 API key 与 Git 工作流已补齐以下确定性覆盖: +Phase 5 P5-6 到 P5-11 的 API key/model 与 Git 工作流已补齐以下确定性覆盖: -- `providerSecrets.test.ts` 覆盖 SecretStorage 优先、process env fallback、missing status、child env 覆盖值和 redaction source。 +- `providerSecrets.test.ts` 覆盖 SecretStorage 优先、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 - `notifier.test.ts` 覆盖 Output Channel/toast message 统一脱敏 SecretStorage/env key。 -- `providerSecretCommands.test.ts` 覆盖 Configure/Clear 命令、provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 +- `providerSecretCommands.test.ts` 覆盖 Configure/Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 -- `rpcServer.test.ts` 覆盖 RPC child env 注入和 key 轮换后重启使用新 env;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 +- `rpcServer.test.ts` 覆盖 RPC child env 注入和 key 轮换后重启使用新 env;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 ## 新增测试的协作要求 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index dc23d86..1993da2 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流已完成。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key 配置、统一 redaction 和 Git 工作流均已实现。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction 和 Git 工作流均已实现;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -69,7 +69,8 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server { "prole-coder.rpc.autoStart": true, "prole-coder.rpc.command": "prole", - "prole-coder.rpc.args": ["rpc"] + "prole-coder.rpc.args": ["rpc"], + "prole-coder.provider.model": "deepseek-v4-pro" } ``` @@ -82,7 +83,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server } ``` -配置不保存 API Key。DeepSeek API Key 仍应由 Rust CLI/RPC server 按既有规则从环境变量或被忽略的本地 `.secrets/` 文件读取。 +配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage,或由 CLI/RPC server 继续按既有规则从环境变量读取;DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 ## MVP 分层 @@ -114,7 +115,7 @@ Phase 3 P0 验收标准: - `tool.approvalRequired` 触发 VS Code modal,approve/reject 能回传到 `agent.approve` / `agent.reject`。已完成真实 RPC pending queue 接入;`apply_patch` 首版支持 selected hunk quick pick 并通过 `agent.approve.hunks` 回传。 - Sidebar Chat 能通过 `agent.listRuns` 展示最近 run,并用 `agent.resume` 回放历史事件。已完成首版 Run List / resume 接入。 - Sidebar Chat 能把 `context.built` 渲染为 Context Capsule 面板,展示 token 分段、来源和 manifest/cache/estimator metadata。已完成首版 Context Capsule 可视化。 -- `ProleCoder: Open Settings` 能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key。 +- `ProleCoder: Open Settings` 能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key,DeepSeek model ID 作为非敏感配置保存。 - Inline completion 首版通过 `agent.previewFim` 请求 RPC server 的 FIM preview,只有 server capability 明确标记 `supportsFim` 的模型会被使用。 Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: @@ -140,12 +141,13 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 -6. P5-6:DeepSeek API key SecretStorage 与 provider status,已完成:插件内配置/清除 key 与查看 provider status;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY`。 -7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key,API key 配置后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 +6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status,provider status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口,Sidebar 错误状态保留 Configure API Key 修复按钮;API key 配置或 model 切换后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 8. P5-8:Git context 只读采集与大 diff attachment 管线,已完成:优先使用 VS Code Git API,git CLI 仅作受控 fallback,commit/PR 命令把 diff context 作为 `explicit_content` attachment 进入 Context Capsule 管线。 9. P5-9:Generate Commit Message,已完成:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit;staged 为空时才询问是否使用 unstaged diff。 10. P5-10:Generate PR Description,已完成:根据 upstream/main/master/用户选择的 base、diff/stat 和 commit summary 生成 PR title/body markdown,用带标题的 untitled markdown 预览承载结果,不自动创建 PR。 11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 +12. P5-12:结构化 provider 配置错误码与恢复动作,未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 4139a4852b63dac7e93ffb51613d0da5ec387c33 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Mon, 1 Jun 2026 21:25:57 +0800 Subject: [PATCH 30/84] Introduces multi-key management for DeepSeek API keys in SecretStorage --- README.md | 4 +- docs/phase-tasks.md | 2 +- docs/roadmap.md | 4 +- docs/security-model.md | 2 +- docs/testing.md | 4 +- docs/vscode-extension.md | 4 +- vscode/extension/src/extension.ts | 22 +- .../extension/src/providerSecretCommands.ts | 315 ++++++++++++++++-- vscode/extension/src/providerSecrets.ts | 177 +++++++++- .../test/providerSecretCommands.test.ts | 183 +++++++++- vscode/extension/test/providerSecrets.test.ts | 104 ++++++ 11 files changed, 778 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index df6359b..b3daae0 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 配置 DeepSeek API key、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-12 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-12 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -699,7 +699,7 @@ extension.ts - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 -- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口,status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 - [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并保留修复按钮;配置/清除 key 或切换 model 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 - [x] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream 和 staged diff;git CLI 只作受控 fallback 采集 unstaged diff、diff stat、branch diff、commit summary 和 ref 检测,cwd 来自 repository root。 - [x] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 922493b..067809d 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -118,7 +118,7 @@ | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | -| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;provider status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | +| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;API key 配置入口升级为 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | | [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并在 Sidebar 错误状态保留修复按钮;API key 配置/清除或 model 切换后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | | [x] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | | [x] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | diff --git a/docs/roadmap.md b/docs/roadmap.md index 2afa7fa..ceac835 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage/provider status、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 +- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 diff --git a/docs/security-model.md b/docs/security-model.md index c60029d..5d6a464 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -7,7 +7,7 @@ ## 边界 - API Key 不得进入 run log。 -- VS Code 插件中的 DeepSeek API key 存入 SecretStorage,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 `DEEPSEEK_API_KEY`。DeepSeek model ID 不是密钥,可通过 VS Code 普通设置保存,并在 child env 中覆盖 `DEEPSEEK_MODEL`。 +- VS Code 插件中的 DeepSeek API key 存入 SecretStorage;多 key 管理器只展示 alias 与 masked key,支持选择 active key 和修改 alias,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 active `DEEPSEEK_API_KEY`。DeepSeek model ID 不是密钥,可通过 VS Code 普通设置保存,并在 child env 中覆盖 `DEEPSEEK_MODEL`。 - `.env` 和本地状态必须被 git 忽略。 - tool call 执行前必须校验 schema。 - 写入应通过 patch application。 diff --git a/docs/testing.md b/docs/testing.md index fad4872..759a4ba 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -81,9 +81,9 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte Phase 5 P5-6 到 P5-11 的 API key/model 与 Git 工作流已补齐以下确定性覆盖: -- `providerSecrets.test.ts` 覆盖 SecretStorage 优先、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 +- `providerSecrets.test.ts` 覆盖 SecretStorage 多 key store 解析、写入前显式校验、active key 选择、masked key 展示、所有 stored key redaction、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 - `notifier.test.ts` 覆盖 Output Channel/toast message 统一脱敏 SecretStorage/env key。 -- `providerSecretCommands.test.ts` 覆盖 Configure/Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 +- `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 - `rpcServer.test.ts` 覆盖 RPC child env 注入和 key 轮换后重启使用新 env;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 1993da2..b5f7671 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -83,7 +83,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server } ``` -配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage,或由 CLI/RPC server 继续按既有规则从环境变量读取;DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 +配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage 的多 key 管理器,或由 CLI/RPC server 继续按既有规则从环境变量读取;Key 管理器展示 alias 与 masked key,支持添加 key+alias、选择 active key 和改 alias。DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 ## MVP 分层 @@ -141,7 +141,7 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 -6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status,provider status 同时显示 key 来源与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口,Sidebar 错误状态保留 Configure API Key 修复按钮;API key 配置或 model 切换后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 8. P5-8:Git context 只读采集与大 diff attachment 管线,已完成:优先使用 VS Code Git API,git CLI 仅作受控 fallback,commit/PR 命令把 diff context 作为 `explicit_content` attachment 进入 Context Capsule 管线。 9. P5-9:Generate Commit Message,已完成:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit;staged 为空时才询问是否使用 unstaged diff。 diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 9091272..e1c5c6e 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -19,8 +19,13 @@ import { import { createVscodeGitRepositoryProvider, createVscodeMarkdownSink } from "./gitWorkflowVscode"; import { createOutputLogger } from "./logging"; import { createExtensionNotifier, type ExtensionNotifier } from "./notifier"; -import { registerProviderSecretCommands } from "./providerSecretCommands"; import { + registerProviderSecretCommands, + type SecretQuickPickController, + type SecretQuickPickItem, +} from "./providerSecretCommands"; +import { + DEEPSEEK_API_KEY_STORE_SECRET_ID, DEEPSEEK_API_KEY_SECRET_ID, deepSeekEnvOverride, providerSecretRedactionValues, @@ -37,6 +42,7 @@ export async function activate(context: vscode.ExtensionContext): Promise const secretRedactor = new MutableSecretRedactor(); const initialSecretStatus = resolveDeepSeekApiKey({ secretValue: await context.secrets.get(DEEPSEEK_API_KEY_SECRET_ID), + keyStoreValue: await context.secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID), processEnv: process.env, }); const providerConfiguration = vscode.workspace.getConfiguration("prole-coder.provider"); @@ -78,9 +84,17 @@ export async function activate(context: vscode.ExtensionContext): Promise retainContextWhenHidden: true, }, }); + const secretWindow = { + showInputBox: vscode.window.showInputBox.bind(vscode.window), + showQuickPick: vscode.window.showQuickPick.bind(vscode.window), + createQuickPick: (): SecretQuickPickController => + vscode.window.createQuickPick() as unknown as SecretQuickPickController, + showInformationMessage: vscode.window.showInformationMessage.bind(vscode.window), + showWarningMessage: vscode.window.showWarningMessage.bind(vscode.window), + }; const providerSecretCommands = registerProviderSecretCommands({ commands: vscode.commands, - window: vscode.window, + window: secretWindow, secrets: context.secrets, processEnv: process.env, redactor: secretRedactor, @@ -88,6 +102,10 @@ export async function activate(context: vscode.ExtensionContext): Promise isRpcIdle: () => chatView.isIdle(), providerConfiguration, configurationTarget: vscode.ConfigurationTarget.Global, + renameAliasButton: { + iconPath: new vscode.ThemeIcon("edit"), + tooltip: "Rename alias", + }, }); const gitRepositoryProvider = createVscodeGitRepositoryProvider(); const markdownSink = createVscodeMarkdownSink(); diff --git a/vscode/extension/src/providerSecretCommands.ts b/vscode/extension/src/providerSecretCommands.ts index a82b027..df4310e 100644 --- a/vscode/extension/src/providerSecretCommands.ts +++ b/vscode/extension/src/providerSecretCommands.ts @@ -1,13 +1,23 @@ +import { randomUUID } from "node:crypto"; + import { + DEEPSEEK_API_KEY_STORE_SECRET_ID, DEEPSEEK_API_KEY_SECRET_ID, DEEPSEEK_MODEL_OPTIONS, deepSeekEnvOverride, formatDeepSeekModelStatus, formatProviderSecretStatus, + maskDeepSeekApiKey, + normalizeDeepSeekKeyAlias, normalizeDeepSeekModelId, + parseDeepSeekApiKeyStore, providerSecretRedactionValues, resolveDeepSeekApiKey, resolveDeepSeekModel, + selectedDeepSeekApiKeyEntry, + serializeDeepSeekApiKeyStore, + type DeepSeekApiKeyEntry, + type DeepSeekApiKeyStore, type DeepSeekSecretResolution, } from "./providerSecrets"; import type { MutableSecretRedactor } from "./redaction"; @@ -34,9 +44,11 @@ export interface SecretStorageLike { export interface SecretWindowMessenger { showInputBox(options: { readonly ignoreFocusOut: true; - readonly password: true; + readonly password?: boolean; + readonly placeHolder?: string; readonly prompt: string; readonly title: string; + readonly value?: string; }): string | undefined | PromiseLike; showQuickPick( items: readonly T[], @@ -46,14 +58,41 @@ export interface SecretWindowMessenger { readonly title: string; }, ): T | undefined | PromiseLike; + createQuickPick(): SecretQuickPickController; showInformationMessage(message: string): unknown; showWarningMessage(message: string): unknown; } +export interface SecretQuickInputButton { + readonly iconPath?: unknown; + readonly tooltip?: string; +} + export interface SecretQuickPickItem { readonly label: string; readonly description?: string; readonly detail?: string; + readonly buttons?: readonly SecretQuickInputButton[]; + readonly alwaysShow?: boolean; +} + +export interface SecretQuickPickItemButtonEvent { + readonly button: SecretQuickInputButton; + readonly item: T; +} + +export interface SecretQuickPickController extends DisposableLike { + title: string | undefined; + placeholder: string | undefined; + ignoreFocusOut: boolean; + matchOnDescription: boolean; + items: readonly T[]; + readonly selectedItems: readonly T[]; + onDidAccept(callback: () => unknown): DisposableLike; + onDidHide(callback: () => unknown): DisposableLike; + onDidTriggerItemButton(callback: (event: SecretQuickPickItemButtonEvent) => unknown): DisposableLike; + show(): void; + hide(): void; } export interface ProviderConfigurationLike { @@ -78,6 +117,7 @@ export interface RegisterProviderSecretCommandOptions { readonly isRpcIdle?: () => boolean; readonly providerConfiguration?: ProviderConfigurationLike | undefined; readonly configurationTarget?: unknown; + readonly renameAliasButton?: SecretQuickInputButton | undefined; } export interface ProviderSecretRuntimeState { @@ -97,28 +137,10 @@ export function registerProviderSecretCommands( return [ options.commands.registerCommand(CONFIGURE_DEEPSEEK_API_KEY_COMMAND, async () => { - const value = await options.window.showInputBox({ - title: "Configure DeepSeek API Key", - prompt: "Enter the DeepSeek API key for ProleCoder. It is stored in VS Code SecretStorage.", - password: true, - ignoreFocusOut: true, - }); - if (value === undefined) { - return undefined; - } - - const normalized = value.trim(); - if (normalized.length === 0) { - return options.window.showWarningMessage("DeepSeek API key was not changed."); - } - - await options.secrets.store(DEEPSEEK_API_KEY_SECRET_ID, normalized); - const status = await refreshProviderSecretState(options, state); - await restartRpcAfterSecretChange(options, "DeepSeek API key saved."); - return options.window.showInformationMessage(formatProviderSecretStatus(status)); + return showDeepSeekApiKeyManager(options, state); }), options.commands.registerCommand(CLEAR_DEEPSEEK_API_KEY_COMMAND, async () => { - await options.secrets.delete(DEEPSEEK_API_KEY_SECRET_ID); + await clearSelectedDeepSeekApiKey(options); const status = await refreshProviderSecretState(options, state); await restartRpcAfterSecretChange(options, "DeepSeek API key cleared."); return options.window.showInformationMessage(formatProviderSecretStatus(status)); @@ -161,9 +183,13 @@ export async function refreshProviderSecretState( >, state?: ProviderSecretRuntimeState, ): Promise { - const secretValue = await options.secrets.get(DEEPSEEK_API_KEY_SECRET_ID); + const [secretValue, keyStoreValue] = await Promise.all([ + options.secrets.get(DEEPSEEK_API_KEY_SECRET_ID), + options.secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID), + ]); const status = resolveDeepSeekApiKey({ secretValue, + keyStoreValue, processEnv: options.processEnv, }); const modelId = state?.modelId ?? currentDeepSeekModel(options); @@ -213,6 +239,251 @@ async function restartRpcAfterSecretChange( await restartRpcAfterProviderChange(options, prefix, "DeepSeek API key updated"); } +async function showDeepSeekApiKeyManager( + options: RegisterProviderSecretCommandOptions, + state: ProviderSecretRuntimeState, +): Promise { + let store = await readDeepSeekApiKeyStore(options.secrets); + const quickPick = options.window.createQuickPick(); + quickPick.title = "Select DeepSeek API Key"; + quickPick.placeholder = "Choose a key, add a new one, or use the edit button to rename an alias."; + quickPick.ignoreFocusOut = true; + quickPick.matchOnDescription = true; + quickPick.items = deepSeekApiKeyPickItems(store, options.renameAliasButton); + + return new Promise((resolve) => { + let finished = false; + let actionInProgress = false; + const disposables: DisposableLike[] = []; + + const finish = (value: DeepSeekSecretResolution | undefined): void => { + if (finished) { + return; + } + finished = true; + for (const disposable of disposables) { + disposable.dispose(); + } + quickPick.dispose(); + resolve(value); + }; + + disposables.push( + quickPick.onDidHide(() => { + if (!actionInProgress) { + finish(undefined); + } + }), + quickPick.onDidAccept(() => { + const item = quickPick.selectedItems[0]; + if (item === undefined) { + return; + } + actionInProgress = true; + quickPick.hide(); + void (async () => { + if (item.kind === "add") { + finish(await addDeepSeekApiKey(options, state, store)); + return; + } + + store = { + ...store, + selectedKeyId: item.keyId, + }; + await saveDeepSeekApiKeyStore(options.secrets, store); + const status = await refreshProviderSecretState(options, state); + await restartRpcAfterSecretChange(options, "DeepSeek API key selected."); + options.window.showInformationMessage(formatProviderSecretStatus(status)); + finish(status); + })(); + }), + quickPick.onDidTriggerItemButton((event) => { + if (event.item.kind !== "key") { + return; + } + + actionInProgress = true; + quickPick.hide(); + void (async () => { + finish(await renameDeepSeekApiKeyAlias(options, state, store, event.item.keyId)); + })(); + }), + ); + + quickPick.show(); + }); +} + +async function addDeepSeekApiKey( + options: RegisterProviderSecretCommandOptions, + state: ProviderSecretRuntimeState, + store: DeepSeekApiKeyStore, +): Promise { + const aliasValue = await options.window.showInputBox({ + title: "Add DeepSeek API Key", + prompt: "Enter an alias for this key.", + placeHolder: "Personal account", + value: `DeepSeek key ${store.entries.length + 1}`, + ignoreFocusOut: true, + }); + if (aliasValue === undefined) { + return undefined; + } + + const apiKeyValue = await options.window.showInputBox({ + title: "Add DeepSeek API Key", + prompt: "Enter the DeepSeek API key. It is stored in VS Code SecretStorage.", + password: true, + ignoreFocusOut: true, + }); + if (apiKeyValue === undefined) { + return undefined; + } + + const apiKey = apiKeyValue.trim(); + if (apiKey.length === 0) { + options.window.showWarningMessage("DeepSeek API key was not changed."); + return undefined; + } + + const entry: DeepSeekApiKeyEntry = { + id: randomUUID(), + alias: normalizeDeepSeekKeyAlias(aliasValue, `DeepSeek key ${store.entries.length + 1}`), + apiKey, + }; + const nextStore: DeepSeekApiKeyStore = { + selectedKeyId: entry.id, + entries: [...store.entries, entry], + }; + await saveDeepSeekApiKeyStore(options.secrets, nextStore); + const status = await refreshProviderSecretState(options, state); + await restartRpcAfterSecretChange(options, "DeepSeek API key saved."); + options.window.showInformationMessage(formatProviderSecretStatus(status)); + return status; +} + +async function renameDeepSeekApiKeyAlias( + options: RegisterProviderSecretCommandOptions, + state: ProviderSecretRuntimeState, + store: DeepSeekApiKeyStore, + keyId: string, +): Promise { + const entry = store.entries.find((candidate) => candidate.id === keyId); + if (entry === undefined) { + return undefined; + } + + const aliasValue = await options.window.showInputBox({ + title: "Rename DeepSeek API Key", + prompt: "Enter a new alias for this key.", + value: entry.alias, + ignoreFocusOut: true, + }); + if (aliasValue === undefined) { + return undefined; + } + + const alias = normalizeDeepSeekKeyAlias(aliasValue, entry.alias); + const nextStore: DeepSeekApiKeyStore = { + selectedKeyId: store.selectedKeyId, + entries: store.entries.map((candidate) => (candidate.id === keyId ? { ...candidate, alias } : candidate)), + }; + await saveDeepSeekApiKeyStore(options.secrets, nextStore); + const status = await refreshProviderSecretState(options, state); + options.window.showInformationMessage(`DeepSeek API key alias updated: ${alias}`); + return status; +} + +async function clearSelectedDeepSeekApiKey( + options: Pick, +): Promise { + const store = await readDeepSeekApiKeyStore(options.secrets); + const selected = selectedDeepSeekApiKeyEntry(store); + if (selected === undefined) { + await options.secrets.delete(DEEPSEEK_API_KEY_SECRET_ID); + await options.secrets.delete(DEEPSEEK_API_KEY_STORE_SECRET_ID); + return; + } + + const entries = store.entries.filter((entry) => entry.id !== selected.id); + await saveDeepSeekApiKeyStore(options.secrets, { + selectedKeyId: entries[0]?.id, + entries, + }); +} + +async function readDeepSeekApiKeyStore( + secrets: SecretStorageLike, +): Promise { + const [keyStoreValue, legacySecretValue] = await Promise.all([ + secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID), + secrets.get(DEEPSEEK_API_KEY_SECRET_ID), + ]); + const store = parseDeepSeekApiKeyStore(keyStoreValue); + if (store.entries.length > 0) { + return store; + } + + const legacySecret = legacySecretValue?.trim(); + if (legacySecret === undefined || legacySecret.length === 0) { + return store; + } + + return { + selectedKeyId: "legacy", + entries: [ + { + id: "legacy", + alias: "Default key", + apiKey: legacySecret, + }, + ], + }; +} + +async function saveDeepSeekApiKeyStore( + secrets: SecretStorageLike, + store: DeepSeekApiKeyStore, +): Promise { + if (store.entries.length === 0) { + await secrets.delete(DEEPSEEK_API_KEY_STORE_SECRET_ID); + } else { + await secrets.store(DEEPSEEK_API_KEY_STORE_SECRET_ID, serializeDeepSeekApiKeyStore(store)); + } + await secrets.delete(DEEPSEEK_API_KEY_SECRET_ID); +} + +interface DeepSeekApiKeyPickItem extends SecretQuickPickItem { + readonly kind: "add" | "key"; + readonly keyId: string; +} + +function deepSeekApiKeyPickItems( + store: DeepSeekApiKeyStore, + renameAliasButton: SecretQuickInputButton | undefined, +): readonly DeepSeekApiKeyPickItem[] { + const selected = selectedDeepSeekApiKeyEntry(store); + const keyItems = store.entries.map((entry) => ({ + label: entry.alias, + description: `${maskDeepSeekApiKey(entry.apiKey)}${entry.id === selected?.id ? " (current)" : ""}`, + detail: "Stored in VS Code SecretStorage", + buttons: renameAliasButton === undefined ? [] : [renameAliasButton], + kind: "key" as const, + keyId: entry.id, + })); + return [ + ...keyItems, + { + label: "+ Add DeepSeek API Key", + description: "Store a new key and alias in VS Code SecretStorage", + alwaysShow: true, + kind: "add", + keyId: "__add__", + }, + ]; +} + function currentDeepSeekModel( options: Pick, ): string | undefined { diff --git a/vscode/extension/src/providerSecrets.ts b/vscode/extension/src/providerSecrets.ts index d5410c5..36f44ea 100644 --- a/vscode/extension/src/providerSecrets.ts +++ b/vscode/extension/src/providerSecrets.ts @@ -1,4 +1,5 @@ export const DEEPSEEK_API_KEY_SECRET_ID = "prole-coder.deepseek-api-key"; +export const DEEPSEEK_API_KEY_STORE_SECRET_ID = "prole-coder.deepseek-api-keys.v1"; export const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY"; export const DEEPSEEK_MODEL_ENV = "DEEPSEEK_MODEL"; @@ -27,10 +28,13 @@ export interface DeepSeekSecretResolution { readonly provider: "deepseek"; readonly source: ProviderSecretSource; readonly apiKey?: string; + readonly keyAlias?: string; + readonly redactionValues?: readonly string[]; } export interface ResolveDeepSeekApiKeyInput { readonly secretValue?: string | undefined; + readonly keyStoreValue?: string | undefined; readonly processEnv?: Record | undefined; } @@ -39,13 +43,42 @@ export interface ResolveDeepSeekModelInput { readonly processEnv?: Record | undefined; } +export interface DeepSeekApiKeyEntry { + readonly id: string; + readonly alias: string; + readonly apiKey: string; +} + +export interface DeepSeekApiKeyStore { + readonly selectedKeyId?: string | undefined; + readonly entries: readonly DeepSeekApiKeyEntry[]; +} + export function resolveDeepSeekApiKey(input: ResolveDeepSeekApiKeyInput): DeepSeekSecretResolution { - const secretValue = normalizedSecret(input.secretValue); - if (secretValue !== undefined) { + const keyStore = parseDeepSeekApiKeyStore(input.keyStoreValue); + const selectedKey = selectedDeepSeekApiKeyEntry(keyStore); + const legacySecretValue = normalizedSecret(input.secretValue); + const storedRedactionValues = uniqueStrings([ + ...keyStore.entries.map((entry) => entry.apiKey), + ...(legacySecretValue === undefined ? [] : [legacySecretValue]), + ]); + + if (selectedKey !== undefined) { + return { + provider: "deepseek", + source: "secret-storage", + apiKey: selectedKey.apiKey, + keyAlias: selectedKey.alias, + redactionValues: storedRedactionValues, + }; + } + + if (legacySecretValue !== undefined) { return { provider: "deepseek", source: "secret-storage", - apiKey: secretValue, + apiKey: legacySecretValue, + redactionValues: storedRedactionValues, }; } @@ -55,6 +88,7 @@ export function resolveDeepSeekApiKey(input: ResolveDeepSeekApiKeyInput): DeepSe provider: "deepseek", source: "process-env", apiKey: envValue, + redactionValues: [envValue], }; } @@ -82,7 +116,10 @@ export function deepSeekEnvOverride( export function providerSecretRedactionValues( resolution: DeepSeekSecretResolution, ): readonly string[] { - return resolution.apiKey === undefined ? [] : [resolution.apiKey]; + return uniqueStrings([ + ...(resolution.redactionValues ?? []), + ...(resolution.apiKey === undefined ? [] : [resolution.apiKey]), + ]); } export function formatProviderSecretStatus(resolution: DeepSeekSecretResolution): string { @@ -92,7 +129,7 @@ export function formatProviderSecretStatus(resolution: DeepSeekSecretResolution) : resolution.source === "process-env" ? "process env" : "missing"; - return `DeepSeek API key: ${source}`; + return resolution.keyAlias === undefined ? `DeepSeek API key: ${source}` : `DeepSeek API key: ${source} (${resolution.keyAlias})`; } export function resolveDeepSeekModel(input: ResolveDeepSeekModelInput): string | undefined { @@ -118,7 +155,137 @@ export function formatDeepSeekModelStatus(modelId: string | undefined): string { : `DeepSeek model: ${option.displayName} (${option.id})`; } +export function parseDeepSeekApiKeyStore(value: string | undefined): DeepSeekApiKeyStore { + if (value === undefined || value.trim().length === 0) { + return emptyDeepSeekApiKeyStore(); + } + + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + return emptyDeepSeekApiKeyStore(); + } + + if (!isRecord(parsed) || parsed["version"] !== 1 || !Array.isArray(parsed["keys"])) { + return emptyDeepSeekApiKeyStore(); + } + + const entries: DeepSeekApiKeyEntry[] = []; + const seenIds = new Set(); + for (const item of parsed["keys"]) { + if (!isRecord(item)) { + continue; + } + + const id = normalizeId(item["id"]); + const apiKey = typeof item["apiKey"] === "string" ? normalizedSecret(item["apiKey"]) : undefined; + if (id === undefined || apiKey === undefined || seenIds.has(id)) { + continue; + } + + seenIds.add(id); + entries.push({ + id, + alias: normalizeDeepSeekKeyAlias(typeof item["alias"] === "string" ? item["alias"] : undefined, "DeepSeek API key"), + apiKey, + }); + } + + return { + selectedKeyId: normalizeId(parsed["selectedKeyId"]), + entries, + }; +} + +export function serializeDeepSeekApiKeyStore(store: DeepSeekApiKeyStore): string { + const entries = validatedDeepSeekApiKeyStoreEntries(store.entries); + return JSON.stringify({ + version: 1, + selectedKeyId: normalizeId(store.selectedKeyId), + keys: entries, + }); +} + +export function selectedDeepSeekApiKeyEntry( + store: DeepSeekApiKeyStore, +): DeepSeekApiKeyEntry | undefined { + return store.entries.find((entry) => entry.id === store.selectedKeyId) ?? store.entries[0]; +} + +export function maskDeepSeekApiKey(value: string): string { + const normalized = normalizedSecret(value); + if (normalized === undefined) { + return ""; + } + if (normalized.length <= 8) { + return "********"; + } + return `${normalized.slice(0, 4)}********${normalized.slice(-4)}`; +} + +export function normalizeDeepSeekKeyAlias(value: string | undefined, fallback: string): string { + const normalized = value?.trim().replace(/\s+/g, " "); + return normalized === undefined || normalized.length === 0 ? fallback : normalized.slice(0, 80); +} + +export function emptyDeepSeekApiKeyStore(): DeepSeekApiKeyStore { + return { + entries: [], + }; +} + function normalizedSecret(value: string | undefined): string | undefined { const normalized = value?.trim(); return normalized === undefined || normalized.length === 0 ? undefined : normalized; } + +function normalizeId(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + const normalized = value.trim(); + return normalized.length === 0 ? undefined : normalized; +} + +function validatedDeepSeekApiKeyStoreEntries( + entries: readonly DeepSeekApiKeyEntry[], +): readonly DeepSeekApiKeyEntry[] { + const seenIds = new Set(); + return entries.map((entry, index) => { + if (!isRecord(entry)) { + throw new Error(`DeepSeek API key store entry ${index + 1} must be an object.`); + } + + const id = normalizeId(entry["id"]); + if (id === undefined) { + throw new Error(`DeepSeek API key store entry ${index + 1} must have a non-empty id.`); + } + if (seenIds.has(id)) { + throw new Error(`DeepSeek API key store entry id "${id}" must be unique.`); + } + + const apiKey = typeof entry["apiKey"] === "string" ? normalizedSecret(entry["apiKey"]) : undefined; + if (apiKey === undefined) { + throw new Error(`DeepSeek API key store entry "${id}" must have a non-empty apiKey.`); + } + + seenIds.add(id); + return { + id, + alias: normalizeDeepSeekKeyAlias( + typeof entry["alias"] === "string" ? entry["alias"] : undefined, + "DeepSeek API key", + ), + apiKey, + }; + }); +} + +function uniqueStrings(values: readonly string[]): readonly string[] { + return Array.from(new Set(values.map((value) => value.trim()).filter((value) => value.length > 0))); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/vscode/extension/test/providerSecretCommands.test.ts b/vscode/extension/test/providerSecretCommands.test.ts index 8e4c6a7..1c1bbb2 100644 --- a/vscode/extension/test/providerSecretCommands.test.ts +++ b/vscode/extension/test/providerSecretCommands.test.ts @@ -7,17 +7,24 @@ import { SELECT_DEEPSEEK_MODEL_COMMAND, SHOW_PROVIDER_STATUS_COMMAND, registerProviderSecretCommands, + type SecretQuickInputButton, + type SecretQuickPickItemButtonEvent, } from "../src/providerSecretCommands.js"; import { DEEPSEEK_API_KEY_ENV, + DEEPSEEK_API_KEY_STORE_SECRET_ID, DEEPSEEK_API_KEY_SECRET_ID, DEEPSEEK_MODEL_ENV, + parseDeepSeekApiKeyStore, + serializeDeepSeekApiKeyStore, } from "../src/providerSecrets.js"; import { MutableSecretRedactor } from "../src/redaction.js"; test("configure DeepSeek API key stores SecretStorage value, updates env, and restarts idle RPC", async () => { const commands = new FakeCommands(); - const window = new FakeSecretWindow([" fixture-secret-value "]); + const window = new FakeSecretWindow(["Work", " fixture-secret-value "], [], [ + { label: "+ Add DeepSeek API Key" }, + ]); const secrets = new FakeSecrets(); const redactor = new MutableSecretRedactor(); const rpc = new FakeRpcServer("ready"); @@ -32,20 +39,107 @@ test("configure DeepSeek API key stores SecretStorage value, updates env, and re redactor, rpcServer: rpc, isRpcIdle: () => true, + renameAliasButton: FakeSecretWindow.renameButton, }); await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); - assert.equal(await secrets.get(DEEPSEEK_API_KEY_SECRET_ID), "fixture-secret-value"); + const keyStore = parseDeepSeekApiKeyStore(await secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID)); + assert.equal(keyStore.entries.length, 1); + assert.equal(keyStore.entries[0]?.alias, "Work"); + assert.equal(keyStore.entries[0]?.apiKey, "fixture-secret-value"); + assert.equal(keyStore.selectedKeyId, keyStore.entries[0]?.id); + assert.equal(await secrets.get(DEEPSEEK_API_KEY_SECRET_ID), undefined); assert.deepEqual(rpc.processEnv, { [DEEPSEEK_API_KEY_ENV]: "fixture-secret-value", }); assert.equal(rpc.stopCount, 1); assert.equal(rpc.startCount, 1); - assert.deepEqual(window.infos.at(-1), "DeepSeek API key: VS Code SecretStorage"); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: VS Code SecretStorage (Work)"); assert.equal(redactor.redact("key fixture-secret-value"), "key [redacted]"); }); +test("configure DeepSeek API key selects an existing stored key", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([], [], [{ label: "Personal" }]); + const secrets = new FakeSecrets({ + [DEEPSEEK_API_KEY_STORE_SECRET_ID]: serializeDeepSeekApiKeyStore({ + selectedKeyId: "work", + entries: [ + { + id: "work", + alias: "Work", + apiKey: "work-secret-value", + }, + { + id: "personal", + alias: "Personal", + apiKey: "personal-secret-value", + }, + ], + }), + }); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + rpcServer: rpc, + isRpcIdle: () => true, + renameAliasButton: FakeSecretWindow.renameButton, + }); + + await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + + const keyStore = parseDeepSeekApiKeyStore(await secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID)); + assert.equal(keyStore.selectedKeyId, "personal"); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "personal-secret-value", + }); + assert.equal(rpc.stopCount, 1); + assert.equal(rpc.startCount, 1); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key: VS Code SecretStorage (Personal)"); +}); + +test("configure DeepSeek API key renames an existing key alias without restarting RPC", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow(["Team"], [], [{ label: "Work", button: true }]); + const secrets = new FakeSecrets({ + [DEEPSEEK_API_KEY_STORE_SECRET_ID]: serializeDeepSeekApiKeyStore({ + selectedKeyId: "work", + entries: [ + { + id: "work", + alias: "Work", + apiKey: "work-secret-value", + }, + ], + }), + }); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + rpcServer: rpc, + isRpcIdle: () => true, + renameAliasButton: FakeSecretWindow.renameButton, + }); + + await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + + const keyStore = parseDeepSeekApiKeyStore(await secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID)); + assert.equal(keyStore.entries[0]?.alias, "Team"); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "work-secret-value", + }); + assert.equal(rpc.stopCount, 0); + assert.equal(rpc.startCount, 0); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key alias updated: Team"); +}); + test("clear DeepSeek API key falls back to process env without restarting active RPC", async () => { const commands = new FakeCommands(); const window = new FakeSecretWindow([]); @@ -127,7 +221,9 @@ test("select DeepSeek model stores configuration, updates env, and restarts idle test("configure DeepSeek API key redacts restart failures", async () => { const commands = new FakeCommands(); - const window = new FakeSecretWindow(["fixture-secret-value"]); + const window = new FakeSecretWindow(["Key", "fixture-secret-value"], [], [ + { label: "+ Add DeepSeek API Key" }, + ]); const redactor = new MutableSecretRedactor(); const rpc = new FakeRpcServer("ready", new Error("failed with fixture-secret-value")); @@ -138,6 +234,7 @@ test("configure DeepSeek API key redacts restart failures", async () => { redactor, rpcServer: rpc, isRpcIdle: () => true, + renameAliasButton: FakeSecretWindow.renameButton, }); await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); @@ -161,12 +258,15 @@ class FakeCommands { } class FakeSecretWindow { + static readonly renameButton = { tooltip: "Rename alias" }; + readonly infos: string[] = []; readonly warnings: string[] = []; constructor( private readonly inputValues: string[], private readonly quickPickLabels: string[] = [], + private readonly quickPickInteractions: FakeQuickPickInteraction[] = [], ) {} showInputBox(): string | undefined { @@ -181,6 +281,10 @@ class FakeSecretWindow { return items.find((item) => item.label === label); } + createQuickPick(): FakeQuickPick { + return new FakeQuickPick(this.quickPickInteractions); + } + showInformationMessage(message: string): void { this.infos.push(message); } @@ -190,6 +294,77 @@ class FakeSecretWindow { } } +interface FakeQuickPickInteraction { + readonly label: string; + readonly button?: boolean; +} + +class FakeQuickPick { + title: string | undefined; + placeholder: string | undefined; + ignoreFocusOut = false; + matchOnDescription = false; + items: readonly T[] = []; + selectedItems: readonly T[] = []; + + private readonly acceptCallbacks: Array<() => unknown> = []; + private readonly hideCallbacks: Array<() => unknown> = []; + private readonly buttonCallbacks: Array<(event: SecretQuickPickItemButtonEvent) => unknown> = []; + + constructor(private readonly interactions: FakeQuickPickInteraction[]) {} + + onDidAccept(callback: () => unknown): { dispose(): void } { + this.acceptCallbacks.push(callback); + return { dispose: () => undefined }; + } + + onDidHide(callback: () => unknown): { dispose(): void } { + this.hideCallbacks.push(callback); + return { dispose: () => undefined }; + } + + onDidTriggerItemButton(callback: (event: SecretQuickPickItemButtonEvent) => unknown): { dispose(): void } { + this.buttonCallbacks.push(callback); + return { dispose: () => undefined }; + } + + show(): void { + const interaction = this.interactions.shift(); + queueMicrotask(() => { + if (interaction === undefined) { + this.hide(); + return; + } + + const item = this.items.find((candidate) => candidate.label === interaction.label); + assert.ok(item, `${interaction.label} should exist in quick pick`); + if (interaction.button === true) { + const button = item.buttons?.[0]; + assert.ok(button, `${interaction.label} should have a button`); + for (const callback of this.buttonCallbacks) { + callback({ button, item }); + } + return; + } + + this.selectedItems = [item]; + for (const callback of this.acceptCallbacks) { + callback(); + } + }); + } + + hide(): void { + for (const callback of this.hideCallbacks) { + callback(); + } + } + + dispose(): void { + return undefined; + } +} + class FakeSecrets { constructor(private readonly values: Record = {}) {} diff --git a/vscode/extension/test/providerSecrets.test.ts b/vscode/extension/test/providerSecrets.test.ts index 4902955..b76992c 100644 --- a/vscode/extension/test/providerSecrets.test.ts +++ b/vscode/extension/test/providerSecrets.test.ts @@ -3,13 +3,17 @@ import test from "node:test"; import { DEEPSEEK_API_KEY_ENV, + DEEPSEEK_API_KEY_STORE_SECRET_ID, DEEPSEEK_MODEL_ENV, deepSeekEnvOverride, formatDeepSeekModelStatus, formatProviderSecretStatus, + maskDeepSeekApiKey, + parseDeepSeekApiKeyStore, providerSecretRedactionValues, resolveDeepSeekApiKey, resolveDeepSeekModel, + serializeDeepSeekApiKeyStore, } from "../src/providerSecrets.js"; test("resolveDeepSeekApiKey prefers SecretStorage over process env", () => { @@ -33,6 +37,106 @@ test("resolveDeepSeekApiKey prefers SecretStorage over process env", () => { assert.equal(formatProviderSecretStatus(resolution), "DeepSeek API key: VS Code SecretStorage"); }); +test("resolveDeepSeekApiKey selects stored key aliases and redacts every stored key", () => { + const keyStoreValue = serializeDeepSeekApiKeyStore({ + selectedKeyId: "work", + entries: [ + { + id: "personal", + alias: "Personal", + apiKey: "personal-secret-value", + }, + { + id: "work", + alias: "Work", + apiKey: "work-secret-value", + }, + ], + }); + + const store = parseDeepSeekApiKeyStore(keyStoreValue); + assert.equal(store.entries.length, 2); + assert.equal(maskDeepSeekApiKey("work-secret-value"), "work********alue"); + + const resolution = resolveDeepSeekApiKey({ + keyStoreValue, + secretValue: "legacy-secret-value", + processEnv: { + [DEEPSEEK_API_KEY_ENV]: "env-secret-value", + }, + }); + + assert.equal(resolution.source, "secret-storage"); + assert.equal(resolution.apiKey, "work-secret-value"); + assert.equal(resolution.keyAlias, "Work"); + assert.equal(formatProviderSecretStatus(resolution), "DeepSeek API key: VS Code SecretStorage (Work)"); + assert.deepEqual(providerSecretRedactionValues(resolution), [ + "personal-secret-value", + "work-secret-value", + "legacy-secret-value", + ]); + assert.deepEqual(deepSeekEnvOverride(resolution), { + [DEEPSEEK_API_KEY_ENV]: "work-secret-value", + }); + assert.ok(keyStoreValue.includes(DEEPSEEK_API_KEY_STORE_SECRET_ID) === false); +}); + +test("serializeDeepSeekApiKeyStore validates entries before writing", () => { + assert.throws( + () => + serializeDeepSeekApiKeyStore({ + entries: [ + { + id: "empty", + alias: "Empty", + apiKey: " ", + }, + ], + }), + /non-empty apiKey/, + ); + assert.throws( + () => + serializeDeepSeekApiKeyStore({ + entries: [ + { + id: "duplicate", + alias: "First", + apiKey: "first-secret-value", + }, + { + id: "duplicate", + alias: "Second", + apiKey: "second-secret-value", + }, + ], + }), + /must be unique/, + ); + + const serialized = serializeDeepSeekApiKeyStore({ + selectedKeyId: " work ", + entries: [ + { + id: " work ", + alias: " Work Key ", + apiKey: " work-secret-value ", + }, + ], + }); + + assert.deepEqual(parseDeepSeekApiKeyStore(serialized), { + selectedKeyId: "work", + entries: [ + { + id: "work", + alias: "Work Key", + apiKey: "work-secret-value", + }, + ], + }); +}); + test("resolveDeepSeekApiKey falls back to process env and reports missing", () => { const envResolution = resolveDeepSeekApiKey({ processEnv: { From 58a4879ef3ad994b586b84856723b01e75d90a18 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:49 +0800 Subject: [PATCH 31/84] Enables system proxy support and improves HTTP error diagnostics --- Cargo.lock | 68 ++++++++++ crates/agent-core/Cargo.toml | 2 +- .../agent-core/src/provider/deepseek_api.rs | 123 ++++++++++++++++-- docs/deepseek-api-adapter.md | 3 +- 4 files changed, 182 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fde29fa..170db15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,6 +104,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -387,9 +403,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -1038,6 +1056,27 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "thiserror" version = "2.0.18" @@ -1390,6 +1429,35 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/crates/agent-core/Cargo.toml b/crates/agent-core/Cargo.toml index 7153305..b807870 100644 --- a/crates/agent-core/Cargo.toml +++ b/crates/agent-core/Cargo.toml @@ -12,7 +12,7 @@ path = "src/lib.rs" async-stream = "0.3" futures-util = "0.3" ignore = "0.4" -reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream", "system-proxy"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" diff --git a/crates/agent-core/src/provider/deepseek_api.rs b/crates/agent-core/src/provider/deepseek_api.rs index 347645d..c86595e 100644 --- a/crates/agent-core/src/provider/deepseek_api.rs +++ b/crates/agent-core/src/provider/deepseek_api.rs @@ -1,4 +1,6 @@ -use std::{collections::BTreeMap, env, fmt, pin::Pin, str, time::Duration}; +use std::{ + collections::BTreeMap, env, error::Error as StdError, fmt, pin::Pin, str, time::Duration, +}; use futures_util::{Stream, StreamExt}; use reqwest::StatusCode; @@ -40,8 +42,12 @@ pub enum DeepSeekApiError { ToolChoiceUnsupportedWithThinking, #[error("non-stream chat completion call received a streaming request")] StreamingRequestInNonStreamCall, - #[error("DeepSeek API request failed: {0}")] - Http(#[from] reqwest::Error), + #[error("DeepSeek API request failed: {details}")] + Http { + details: String, + #[source] + source: reqwest::Error, + }, #[error("DeepSeek API returned HTTP {status}: {body}")] Api { status: StatusCode, body: String }, #[error("DeepSeek API JSON response is invalid: {source}; body: {body}")] @@ -60,6 +66,15 @@ pub enum DeepSeekApiError { IncompleteStreamEvent { buffered_bytes: usize }, } +impl DeepSeekApiError { + fn http(source: reqwest::Error) -> Self { + Self::Http { + details: format_reqwest_error(&source), + source, + } + } +} + pub type ChatCompletionStream = Pin> + Send>>; @@ -214,7 +229,8 @@ impl DeepSeekApiAdapter { pub fn new(config: DeepSeekApiConfig) -> Result { let client = reqwest::Client::builder() .timeout(config.timeout()) - .build()?; + .build() + .map_err(DeepSeekApiError::http)?; Ok(Self { client, config }) } @@ -245,7 +261,8 @@ impl DeepSeekApiAdapter { .bearer_auth(&self.config.api_key) .json(&request) .send() - .await?; + .await + .map_err(DeepSeekApiError::http)?; decode_chat_completion_response(response).await } @@ -263,7 +280,8 @@ impl DeepSeekApiAdapter { .bearer_auth(&self.config.api_key) .json(&request) .send() - .await?; + .await + .map_err(DeepSeekApiError::http)?; decode_chat_completion_stream(response).await } @@ -279,7 +297,8 @@ impl DeepSeekApiAdapter { .bearer_auth(&self.config.api_key) .json(&request) .send() - .await?; + .await + .map_err(DeepSeekApiError::http)?; decode_fim_completion_response(response).await } @@ -289,7 +308,7 @@ async fn decode_chat_completion_response( response: reqwest::Response, ) -> Result { let status = response.status(); - let body = response.text().await?; + let body = response.text().await.map_err(DeepSeekApiError::http)?; if !status.is_success() { return Err(DeepSeekApiError::Api { status, body }); @@ -302,7 +321,7 @@ async fn decode_fim_completion_response( response: reqwest::Response, ) -> Result { let status = response.status(); - let body = response.text().await?; + let body = response.text().await.map_err(DeepSeekApiError::http)?; if !status.is_success() { return Err(DeepSeekApiError::Api { status, body }); } @@ -315,7 +334,7 @@ async fn decode_chat_completion_stream( ) -> Result { let status = response.status(); if !status.is_success() { - let body = response.text().await?; + let body = response.text().await.map_err(DeepSeekApiError::http)?; return Err(DeepSeekApiError::Api { status, body }); } @@ -325,7 +344,7 @@ async fn decode_chat_completion_stream( let mut parser = SseEventParser::new(); while let Some(chunk) = byte_stream.next().await { - let chunk = chunk?; + let chunk = chunk.map_err(DeepSeekApiError::http)?; for event in parser.push_bytes(&chunk)? { let done = event == StreamEvent::Done; yield event; @@ -342,6 +361,76 @@ async fn decode_chat_completion_stream( })) } +fn format_reqwest_error(error: &reqwest::Error) -> String { + let mut details = Vec::new(); + details.push(redact_url_credentials(&error.to_string())); + + let mut kinds = Vec::new(); + if error.is_timeout() { + kinds.push("timeout"); + } + if error.is_connect() { + kinds.push("connect"); + } + if error.is_request() { + kinds.push("request"); + } + if error.is_body() { + kinds.push("body"); + } + if error.is_decode() { + kinds.push("decode"); + } + if !kinds.is_empty() { + details.push(format!("kind: {}", kinds.join("/"))); + } + if let Some(status) = error.status() { + details.push(format!("status: {status}")); + } + + let mut source = StdError::source(error); + while let Some(cause) = source { + let cause_message = redact_url_credentials(&cause.to_string()); + if !cause_message.is_empty() && !details.iter().any(|detail| detail == &cause_message) { + details.push(format!("caused by: {cause_message}")); + } + source = StdError::source(cause); + } + + details.join("; ") +} + +fn redact_url_credentials(message: &str) -> String { + let mut result = String::with_capacity(message.len()); + let mut rest = message; + + while let Some(scheme_index) = rest.find("://") { + let authority_start = scheme_index + "://".len(); + result.push_str(&rest[..authority_start]); + let after_scheme = &rest[authority_start..]; + let authority_end = after_scheme + .find(|character: char| { + character == '/' + || character == '\\' + || character == '?' + || character == '#' + || character.is_whitespace() + }) + .unwrap_or(after_scheme.len()); + let authority = &after_scheme[..authority_end]; + if let Some((_, host)) = authority.rsplit_once('@') { + result.push_str("[redacted]@"); + result.push_str(host); + } else { + result.push_str(authority); + } + rest = &after_scheme[authority_end..]; + } + + result.push_str(rest); + result +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ChatCompletionRequest { pub model: DeepSeekModelId, @@ -1120,7 +1209,7 @@ mod tests { ChatToolCallAccumulator, ChatToolCallAccumulatorError, ChatToolCallDelta, ChatToolType, DeepSeekApiConfig, DeepSeekApiError, DeepSeekModelId, FimCompletionRequest, ReasoningEffort, SseEventParser, StreamEvent, StreamOptions, ThinkingConfig, ToolChoice, - parse_stream_event_block, + parse_stream_event_block, redact_url_credentials, }; #[test] @@ -1171,6 +1260,16 @@ mod tests { ); } + #[test] + fn request_error_formatting_redacts_url_credentials() { + assert_eq!( + redact_url_credentials( + "proxy failed at https://user:secret@example.test:8443/path and http://plain.test" + ), + "proxy failed at https://[redacted]@example.test:8443/path and http://plain.test" + ); + } + #[test] fn fim_request_serializes_prefix_suffix_and_non_streaming_default() { let request = FimCompletionRequest::new( diff --git a/docs/deepseek-api-adapter.md b/docs/deepseek-api-adapter.md index 7122842..d8e3b66 100644 --- a/docs/deepseek-api-adapter.md +++ b/docs/deepseek-api-adapter.md @@ -153,7 +153,7 @@ adapter 使用显式错误枚举: - DeepSeek 返回非 2xx 状态。 - JSON 响应或 SSE data 解析失败。 -非 2xx 响应保留 HTTP status 和响应 body,便于上层生成用户可读错误。API Key 不进入错误消息。 +HTTP client 启用 reqwest `system-proxy`,让 CLI、RPC server 和 VS Code 插件启动的子进程都尽量复用系统代理配置。发送失败时错误消息会保留 timeout/connect/request/body/decode 等分类和底层 source chain;如果 source 中包含带用户名密码的 URL,会先脱敏。非 2xx 响应保留 HTTP status 和响应 body,便于上层生成用户可读错误。API Key 不进入错误消息。 ## reasoning_content 规则 @@ -170,6 +170,7 @@ adapter 只负责序列化和反序列化 `reasoning_content` 字段。是否需 - 配置 Debug 不泄露 API Key。 - base URL 带路径时 endpoint 拼接正确。 +- HTTP 错误格式化不会泄露 URL 用户名密码。 - thinking request 序列化。 - `thinking.type = disabled` 时不会发送 `reasoning_effort`。 - `tool_choice` 与 thinking mode 的不兼容组合会在本地校验失败。 From f80d011cff58c74647f547ffefe9dbee15d64e01 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:49 +0800 Subject: [PATCH 32/84] Adds run deletion support to RunLogStore --- crates/agent-core/src/run_log.rs | 98 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/crates/agent-core/src/run_log.rs b/crates/agent-core/src/run_log.rs index 0c29003..3b3b7e0 100644 --- a/crates/agent-core/src/run_log.rs +++ b/crates/agent-core/src/run_log.rs @@ -151,6 +151,19 @@ impl RunLogStore { read_summary(&run_id, &self.summary_path(&run_id)?) } + pub fn delete_run(&self, run_id: impl Into) -> Result<(), RunLogError> { + let run_id = validate_id("run id", run_id.into())?; + let run_dir = self.run_dir(&run_id)?; + if !run_dir.is_dir() { + return Err(RunLogError::RunNotFound { run_id }); + } + + fs::remove_dir_all(&run_dir).map_err(|source| RunLogError::Io { + path: run_dir, + source, + }) + } + pub fn list_run_summaries(&self) -> Result, RunLogError> { if !self.runs_dir.exists() { return Ok(Vec::new()); @@ -451,8 +464,15 @@ impl RunSummary { match event.event_type.as_str() { "run.started" => { - self.started_at_unix_ms = event.time_unix_ms; + if self.event_count == 1 { + self.started_at_unix_ms = event.time_unix_ms; + } self.updated_at_unix_ms = event.time_unix_ms; + self.status = RunSummaryStatus::Running; + self.completed_at_unix_ms = None; + self.summary = None; + self.changed_files.clear(); + self.verification_status = None; self.mode = string_field(&event.payload, "mode"); } "turn.started" => { @@ -1226,6 +1246,60 @@ mod tests { assert_eq!(summary.verification_status.as_deref(), Some("passed")); } + #[test] + fn run_log_summary_resets_terminal_fields_when_run_reopens_for_next_turn() { + let workspace = TestWorkspace::new("run-log"); + let store = RunLogStore::new(workspace.path()).expect("store should open"); + let mut run = store + .create_run("run_multi_turn_summary") + .expect("run should be created"); + + run.append_at(100, "run.started", None, json!({ "mode": "ask" })) + .expect("first run started should append"); + run.append_at( + 110, + "turn.started", + Some("turn_1".to_owned()), + json!({ "turnId": "turn_1", "userTask": "First task" }), + ) + .expect("first turn started should append"); + run.append_at( + 120, + "run.completed", + Some("turn_1".to_owned()), + json!({ + "summary": "First summary.", + "changedFiles": ["README.md"], + "verificationStatus": "passed" + }), + ) + .expect("first completion should append"); + run.append_at(200, "run.started", None, json!({ "mode": "edit" })) + .expect("second run started should append"); + run.append_at( + 210, + "turn.started", + Some("turn_2".to_owned()), + json!({ "turnId": "turn_2", "userTask": "Second task" }), + ) + .expect("second turn started should append"); + + let summary = store + .load_run_summary("run_multi_turn_summary") + .expect("summary should load"); + assert_eq!(summary.title, "Second task"); + assert_eq!(summary.status, RunSummaryStatus::Running); + assert_eq!(summary.started_at_unix_ms, 100); + assert_eq!(summary.updated_at_unix_ms, 210); + assert_eq!(summary.completed_at_unix_ms, None); + assert_eq!(summary.last_seq, 5); + assert_eq!(summary.event_count, 5); + assert_eq!(summary.mode.as_deref(), Some("edit")); + assert_eq!(summary.summary, None); + assert!(summary.changed_files.is_empty()); + assert_eq!(summary.verification_status, None); + } + #[test] fn run_log_lists_summaries_by_recent_update_without_scanning_events() { let workspace = TestWorkspace::new("run-log"); @@ -1259,6 +1333,28 @@ mod tests { assert_eq!(summaries[1].updated_at_unix_ms, 100); } + #[test] + fn run_log_deletes_run_directory_safely() { + let workspace = TestWorkspace::new("run-log"); + let store = RunLogStore::new(workspace.path()).expect("store should open"); + store + .create_run("run_delete") + .expect("run should be created"); + + store + .delete_run("run_delete") + .expect("run should be deleted"); + + assert!(matches!( + store.load_run("run_delete"), + Err(RunLogError::RunNotFound { .. }) + )); + assert!(matches!( + store.delete_run("../outside"), + Err(RunLogError::InvalidIdentifier { .. }) + )); + } + #[test] fn serialized_run_log_serializes_concurrent_appenders() { let workspace = TestWorkspace::new("run-log"); From 9b2233fbdbf36562d9f33919d272d3691b129e68 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:49 +0800 Subject: [PATCH 33/84] Injects final response summary instruction into turn context --- crates/agent-core/src/turn_loop.rs | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/crates/agent-core/src/turn_loop.rs b/crates/agent-core/src/turn_loop.rs index 595fa0d..32e6035 100644 --- a/crates/agent-core/src/turn_loop.rs +++ b/crates/agent-core/src/turn_loop.rs @@ -34,6 +34,11 @@ const DEFAULT_MAX_ATTACHMENTS: usize = 32; const DEFAULT_MAX_ATTACHMENT_BYTES: u64 = 256 * 1024; const SHELL_APPROVAL_OUTPUT_SUMMARY_MAX_LINES: usize = 8; const SHELL_APPROVAL_OUTPUT_SUMMARY_MAX_BYTES: usize = 2 * 1024; +const FINAL_RESPONSE_SUMMARY_INSTRUCTION: &str = concat!( + "When the task is complete, make the final assistant message a concise work summary for the user. ", + "Mention what changed, important files, verification or tests, and any blockers. ", + "Do not dump raw tool logs, JSON-RPC events, or intermediate provider/tool chatter; those details are recorded in ProleCoder Output." +); #[derive(Debug)] pub struct AgentTurnLoop { @@ -416,6 +421,10 @@ where "stable workspace manifest summary", )); } + builder.add_item(ContextItem::project_rules( + FINAL_RESPONSE_SUMMARY_INSTRUCTION, + "default final response summary contract", + )); builder.add_item(ContextItem::user_task(input.user_task.clone())); for item in &input.context_items { builder.add_item(item.clone()); @@ -2365,6 +2374,31 @@ mod tests { } } + #[tokio::test] + async fn turn_loop_injects_final_response_summary_contract() { + let workspace = TestWorkspace::new("turn-loop"); + let store = RunLogStore::new(workspace.path()).expect("run log store should open"); + let mut run = store + .create_run("run_turn_final_summary_contract") + .expect("run should be created"); + let provider = + ScriptedProvider::new(vec![TurnProviderResponse::final_text("Work summary.")]); + let mut loop_runner = + AgentTurnLoop::new(workspace.path(), provider).expect("turn loop should initialize"); + + loop_runner + .run_turn(AgentTurnInput::new("turn_1", "Do a small task"), &mut run) + .await + .expect("turn should complete"); + + let prompt = loop_runner.provider.requests[0].messages[0] + .content + .as_deref() + .expect("provider prompt should include context"); + assert!(prompt.contains("final assistant message")); + assert!(prompt.contains("Do not dump raw tool logs")); + } + #[tokio::test] async fn turn_loop_rejects_duplicate_attachments_before_provider_call() { let workspace = TestWorkspace::new("turn-loop"); From de95ead13e5f7a05c8b8a9d14996adda02973145 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:49 +0800 Subject: [PATCH 34/84] Implements agent.deleteRun RPC method with multi-turn support --- crates/agent-rpc/src/lib.rs | 260 +++++++++++++++++++++++++++++++++--- 1 file changed, 244 insertions(+), 16 deletions(-) diff --git a/crates/agent-rpc/src/lib.rs b/crates/agent-rpc/src/lib.rs index b3730bf..80439eb 100644 --- a/crates/agent-rpc/src/lib.rs +++ b/crates/agent-rpc/src/lib.rs @@ -56,6 +56,7 @@ pub const REJECT_METHOD: RpcMethod = RpcMethod::new("reject"); pub const CANCEL_METHOD: RpcMethod = RpcMethod::new("cancel"); pub const RESUME_METHOD: RpcMethod = RpcMethod::new("resume"); pub const LIST_RUNS_METHOD: RpcMethod = RpcMethod::new("listRuns"); +pub const DELETE_RUN_METHOD: RpcMethod = RpcMethod::new("deleteRun"); pub const FIM_PREVIEW_METHOD: RpcMethod = RpcMethod::new("previewFim"); pub const EVENT_METHOD: RpcMethod = RpcMethod::new("event"); pub const EVENT_BATCH_METHOD: RpcMethod = RpcMethod::new("eventBatch"); @@ -414,6 +415,19 @@ pub struct ListRunsResult { pub runs: Vec, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteRunParams { + pub run_id: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteRunResult { + pub run_id: String, + pub deleted: bool, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcRunSummary { @@ -645,6 +659,11 @@ pub trait AgentRpcRequestHandler { params: ListRunsParams, ) -> Result, AgentRpcHandlerError>; + fn delete_run( + &mut self, + params: DeleteRunParams, + ) -> Result, AgentRpcHandlerError>; + fn preview_fim( &mut self, params: FimPreviewParams, @@ -787,13 +806,13 @@ where Some(run_id) => run_id, None => generate_id("run")?, }; - let turn_id = "turn_1".to_owned(); let provider = self.provider_factory.create_provider(¶ms)?; let run_log = self .workspace()? .store - .create_run(run_id.clone()) + .open_or_create_run(run_id.clone()) .map_err(map_run_log_error)?; + let turn_id = next_turn_id(&run_log).map_err(map_run_log_error)?; let attachments = params .attachments .iter() @@ -993,6 +1012,32 @@ where Ok(AgentRpcHandlerOutput::new(ListRunsResult { runs })) } + fn delete_run( + &mut self, + params: DeleteRunParams, + ) -> Result, AgentRpcHandlerError> { + self.drain_ready_active_run_events()?; + if self + .active_run + .as_ref() + .is_some_and(|active_run| active_run.run_id == params.run_id) + { + return Err(AgentRpcHandlerError::new( + RPC_RUN_ALREADY_ACTIVE, + format!("run `{}` is active and cannot be deleted", params.run_id), + )); + } + + self.workspace()? + .store + .delete_run(params.run_id.clone()) + .map_err(map_run_log_error)?; + Ok(AgentRpcHandlerOutput::new(DeleteRunResult { + run_id: params.run_id, + deleted: true, + })) + } + fn preview_fim( &mut self, params: FimPreviewParams, @@ -1117,6 +1162,18 @@ impl AgentTurnLoopRpcHandler { } } +fn next_turn_id(run_log: &RunLog) -> Result { + let turn_count = run_log + .load()? + .iter() + .filter(|event| event.event_type == "turn.started") + .count(); + let turn_number = turn_count + .checked_add(1) + .ok_or(RunLogError::SequenceOverflow)?; + Ok(format!("turn_{turn_number}")) +} + impl TryFrom<&RunSummary> for RpcRunSummary { type Error = AgentRpcHandlerError; @@ -2303,6 +2360,9 @@ where method if method == LIST_RUNS_METHOD.qualified_name() => { self.handle_list_runs(id, message.params, writer) } + method if method == DELETE_RUN_METHOD.qualified_name() => { + self.handle_delete_run(id, message.params, writer) + } method if method == FIM_PREVIEW_METHOD.qualified_name() => { self.handle_preview_fim(id, message.params, writer) } @@ -2532,6 +2592,32 @@ where } } + fn handle_delete_run( + &mut self, + id: Value, + params: Option, + writer: &mut W, + ) -> Result<(), AgentRpcError> + where + W: Write, + { + let params = match parse_params::( + params, + DELETE_RUN_METHOD.qualified_name().as_str(), + ) { + Ok(params) => params, + Err(error) => return write_error(writer, id, error), + }; + + match self.handler.delete_run(params) { + Ok(output) => { + write_json_line(writer, &JsonRpcResponse::new(id, output.result))?; + emit_run_log_events(writer, &output.events) + } + Err(error) => write_error(writer, id, error.into_error_object()), + } + } + fn handle_preview_fim( &mut self, id: Value, @@ -2898,7 +2984,7 @@ mod tests { use prole_coder_agent_core::{ approval::RiskLevel, provider::deepseek_api::ChatToolCall, - run_log::{RunLogEvent, RunLogStore}, + run_log::{RunLogError, RunLogEvent, RunLogStore, RunSummaryStatus}, test_helpers::TestWorkspace, turn_loop::{ AgentTurnInput, AgentTurnLoopConfig, ApprovalDecision, TurnApprovalRequest, @@ -2920,19 +3006,20 @@ mod tests { APPROVE_METHOD, ActiveRunSpawn, AgentInitializeParams, AgentInitializeResult, AgentRpcError, AgentRpcHandlerError, AgentRpcHandlerOutput, AgentRpcRequestHandler, AgentTurnLoopRpcHandler, ApproveParams, ApproveResult, CANCEL_METHOD, CancelParams, - CancelResult, EVENT_BATCH_METHOD, EVENT_METHOD, FIM_PREVIEW_METHOD, FimPreviewParams, - FimPreviewResult, INITIALIZE_METHOD, JSON_RPC_INTERNAL_ERROR, JSON_RPC_INVALID_PARAMS, - JSON_RPC_INVALID_REQUEST, JSON_RPC_METHOD_NOT_FOUND, JSON_RPC_PARSE_ERROR, - LIST_RUNS_METHOD, ListRunsParams, ListRunsResult, PROTOCOL_VERSION, REJECT_METHOD, - RESUME_METHOD, RPC_APPROVAL_DENIED, RPC_APPROVAL_NOT_FOUND, RPC_CONTEXT_BUDGET_EXCEEDED, - RPC_INTERNAL_INVARIANT, RPC_INVALID_TOOL_ARGUMENTS, RPC_PROVIDER_ERROR, - RPC_RUN_ALREADY_ACTIVE, RPC_RUN_CANCELED, RPC_RUN_NOT_FOUND, RPC_TOOL_EXECUTION_FAILED, - RPC_UNSUPPORTED_PROTOCOL, RPC_WORKSPACE_UNTRUSTED, RejectParams, RejectResult, - ResumeParams, ResumeResult, RpcApprovalPersistence, RpcApprovalQueue, RpcApprovalState, - RpcApprovedHunks, RpcRunState, RpcRunSummary, RpcRunSummaryStatus, RpcWorkspace, - SEND_TURN_METHOD, SendTurnParams, SendTurnResult, StdioEventBridge, - emit_live_run_log_events, format_unix_millis, run_log_event_to_notification, - run_log_events_to_batch_notification, run_stdio_request_loop, spawn_active_run, + CancelResult, DELETE_RUN_METHOD, DeleteRunParams, DeleteRunResult, EVENT_BATCH_METHOD, + EVENT_METHOD, FIM_PREVIEW_METHOD, FimPreviewParams, FimPreviewResult, INITIALIZE_METHOD, + JSON_RPC_INTERNAL_ERROR, JSON_RPC_INVALID_PARAMS, JSON_RPC_INVALID_REQUEST, + JSON_RPC_METHOD_NOT_FOUND, JSON_RPC_PARSE_ERROR, LIST_RUNS_METHOD, ListRunsParams, + ListRunsResult, PROTOCOL_VERSION, REJECT_METHOD, RESUME_METHOD, RPC_APPROVAL_DENIED, + RPC_APPROVAL_NOT_FOUND, RPC_CONTEXT_BUDGET_EXCEEDED, RPC_INTERNAL_INVARIANT, + RPC_INVALID_TOOL_ARGUMENTS, RPC_PROVIDER_ERROR, RPC_RUN_ALREADY_ACTIVE, RPC_RUN_CANCELED, + RPC_RUN_NOT_FOUND, RPC_TOOL_EXECUTION_FAILED, RPC_UNSUPPORTED_PROTOCOL, + RPC_WORKSPACE_UNTRUSTED, RejectParams, RejectResult, ResumeParams, ResumeResult, + RpcApprovalPersistence, RpcApprovalQueue, RpcApprovalState, RpcApprovedHunks, RpcRunState, + RpcRunSummary, RpcRunSummaryStatus, RpcWorkspace, SEND_TURN_METHOD, SendTurnParams, + SendTurnResult, StdioEventBridge, emit_live_run_log_events, format_unix_millis, + run_log_event_to_notification, run_log_events_to_batch_notification, + run_stdio_request_loop, spawn_active_run, }; #[test] @@ -2944,6 +3031,7 @@ mod tests { assert_eq!(CANCEL_METHOD.qualified_name(), "agent.cancel"); assert_eq!(RESUME_METHOD.qualified_name(), "agent.resume"); assert_eq!(LIST_RUNS_METHOD.qualified_name(), "agent.listRuns"); + assert_eq!(DELETE_RUN_METHOD.qualified_name(), "agent.deleteRun"); assert_eq!(FIM_PREVIEW_METHOD.qualified_name(), "agent.previewFim"); assert_eq!(EVENT_METHOD.qualified_name(), "agent.event"); assert_eq!(EVENT_BATCH_METHOD.qualified_name(), "agent.eventBatch"); @@ -3477,6 +3565,42 @@ mod tests { ); } + #[test] + fn request_loop_deletes_runs() { + let input = [ + json!({ + "jsonrpc": "2.0", + "id": "init_1", + "method": "agent.initialize", + "params": initialize_params() + }) + .to_string(), + json!({ + "jsonrpc": "2.0", + "id": "delete_1", + "method": "agent.deleteRun", + "params": { + "runId": "run_rpc" + } + }) + .to_string(), + ] + .join("\n"); + let mut output = Vec::new(); + + let handler = + run_stdio_request_loop(Cursor::new(input), &mut output, TestHandler::default()) + .expect("request loop should complete"); + + assert_eq!(handler.delete_runs.len(), 1); + assert_eq!(handler.delete_runs[0].run_id, "run_rpc"); + let lines = output_lines(output); + assert_eq!(lines.len(), 2); + assert_eq!(lines[1]["id"], "delete_1"); + assert_eq!(lines[1]["result"]["runId"], "run_rpc"); + assert_eq!(lines[1]["result"]["deleted"], true); + } + #[test] fn request_loop_handles_fim_preview_requests() { let input = [ @@ -3770,6 +3894,97 @@ mod tests { ); } + #[test] + fn turn_loop_rpc_handler_appends_multiple_turns_to_existing_run() { + let workspace = TestWorkspace::new("rpc"); + let mut handler = AgentTurnLoopRpcHandler::new(final_provider_factory); + handler + .initialize( + serde_json::from_value(initialize_params_for(workspace.path_str())) + .expect("initialize params should deserialize"), + ) + .expect("handler should initialize"); + + let first = handler + .send_turn(SendTurnParams { + run_id: Some("run_multi_turn".to_owned()), + message: "First task".to_owned(), + mode: super::RpcRunMode::Ask, + attachments: Vec::new(), + }) + .expect("first turn should run"); + let second = handler + .send_turn(SendTurnParams { + run_id: Some("run_multi_turn".to_owned()), + message: "Second task".to_owned(), + mode: super::RpcRunMode::Ask, + attachments: Vec::new(), + }) + .expect("second turn should append to the same run"); + + assert_eq!(first.result.turn_id, "turn_1"); + assert_eq!(second.result.turn_id, "turn_2"); + let store = RunLogStore::new(workspace.path()).expect("store should open"); + let events = store + .load_run("run_multi_turn") + .expect("run log should load"); + assert_eq!( + events + .iter() + .filter(|event| event.event_type == "run.started") + .count(), + 2 + ); + assert_eq!( + events + .iter() + .filter(|event| event.event_type == "turn.started") + .map(|event| event.turn_id.as_deref()) + .collect::>(), + vec![Some("turn_1"), Some("turn_2")] + ); + let summary = store + .load_run_summary("run_multi_turn") + .expect("summary should load"); + assert_eq!(summary.title, "Second task"); + assert_eq!(summary.status, RunSummaryStatus::Completed); + assert_eq!(summary.summary.as_deref(), Some("RPC final answer")); + } + + #[test] + fn turn_loop_rpc_handler_deletes_inactive_run_logs() { + let workspace = TestWorkspace::new("rpc"); + let mut handler = AgentTurnLoopRpcHandler::new(final_provider_factory); + handler + .initialize( + serde_json::from_value(initialize_params_for(workspace.path_str())) + .expect("initialize params should deserialize"), + ) + .expect("handler should initialize"); + handler + .send_turn(SendTurnParams { + run_id: Some("run_delete_rpc".to_owned()), + message: "Say hello".to_owned(), + mode: super::RpcRunMode::Ask, + attachments: Vec::new(), + }) + .expect("turn should complete before deletion"); + + let result = handler + .delete_run(DeleteRunParams { + run_id: "run_delete_rpc".to_owned(), + }) + .expect("inactive run should delete"); + + assert_eq!(result.result.run_id, "run_delete_rpc"); + assert!(result.result.deleted); + let store = RunLogStore::new(workspace.path()).expect("store should open"); + assert!(matches!( + store.load_run("run_delete_rpc"), + Err(RunLogError::RunNotFound { .. }) + )); + } + #[test] fn request_loop_send_turn_returns_before_provider_completion() { let workspace = TestWorkspace::new("rpc"); @@ -4790,6 +5005,7 @@ mod tests { cancellations: Vec, resumes: Vec, list_runs: Vec, + delete_runs: Vec, fim_previews: Vec, } @@ -4905,6 +5121,18 @@ mod tests { })) } + fn delete_run( + &mut self, + params: DeleteRunParams, + ) -> Result, AgentRpcHandlerError> { + let run_id = params.run_id.clone(); + self.delete_runs.push(params); + Ok(AgentRpcHandlerOutput::new(DeleteRunResult { + run_id, + deleted: true, + })) + } + fn preview_fim( &mut self, params: FimPreviewParams, From a8878cba2f2e0cef5f0c21b46ead1b0ee97a2c96 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 35/84] Updates documentation for P5-12 Sidebar continuous session and run deletion --- README.md | 11 ++++++----- docs/architecture.md | 2 +- docs/json-rpc-protocol.md | 22 ++++++++++++++++++++++ docs/phase-tasks.md | 9 +++++---- docs/roadmap.md | 9 +++++---- docs/rpc-server.md | 2 ++ docs/security-model.md | 2 +- docs/testing.md | 12 +++++++----- docs/vscode-extension.md | 16 +++++++++------- 9 files changed, 58 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index b3daae0..35f824d 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-12 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和过程事件默认折叠;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-13 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -699,21 +699,22 @@ extension.ts - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 -- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 修改 alias 和 trash 删除指定 key;status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 - [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并保留修复按钮;配置/清除 key 或切换 model 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 - [x] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream 和 staged diff;git CLI 只作受控 fallback 采集 unstaged diff、diff stat、branch diff、commit summary 和 ref 检测,cwd 来自 repository root。 - [x] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 - [x] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版只提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 - [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 -- [ ] P5-12:结构化 provider 配置错误码与恢复动作:将缺 API key 等配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 +- [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX:`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar resume 后继续同一会话发送多轮 turn;Run List 支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder` 便于 debug。 +- [ ] P5-13:结构化 provider 配置错误码与恢复动作:将缺 API key 等配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 验收标准: - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- 主审批弹窗保持简单;Sidebar composer 提供 Key/Model 直接入口,完整错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-12 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 +- 主审批弹窗保持简单;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 +- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-13 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/architecture.md b/docs/architecture.md index e74538b..de47b65 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -57,7 +57,7 @@ TypeScript workspace: - Rust workspace、TypeScript workspace、VS Code 插件骨架和共享协议包已建立。 - `agent-core` 已包含 provider adapter、流式解析、streaming tool call delta accumulator、`reasoning_content` 状态机、工具/审批基础类型、协作式取消 token、read/search/apply_patch/shell/git 基础执行层、基础 run log、基础 Context Builder、基础 Agent Turn Loop、async / streaming `TurnProvider` 边界和 `TurnEventSink` 实时事件出口。 -- `agent-rpc` 已实现 Run Log 事件到 `agent.event` JSON-RPC notification 的 stdio 桥接,`StdioEventBridge` 可直接作为 `TurnEventSink` 使用;同时已实现 `agent.initialize` / `agent.sendTurn` / `agent.approve` / `agent.reject` / `agent.cancel` / `agent.resume` / `agent.listRuns` 的双向 request loop、全双工 live event queue、真实 `AgentTurnLoopRpcHandler`、单 active run 的 RPC pending approval 等待队列和断连取消。 +- `agent-rpc` 已实现 Run Log 事件到 `agent.event` JSON-RPC notification 的 stdio 桥接,`StdioEventBridge` 可直接作为 `TurnEventSink` 使用;同时已实现 `agent.initialize` / `agent.sendTurn` / `agent.approve` / `agent.reject` / `agent.cancel` / `agent.resume` / `agent.listRuns` / `agent.deleteRun` 的双向 request loop、全双工 live event queue、真实 `AgentTurnLoopRpcHandler`、同一 run 多 turn 追加、单 active run 的 RPC pending approval 等待队列和断连取消。 - CLI 已实现 `run` 最小闭环,能直接调用 Agent Core、通过 DeepSeek streaming wrapper 驱动真实 provider,在 `--json` 模式下随着 run log 写入实时输出 JSON-RPC event,失败时输出 JSON-RPC error response,并支持 stdin/stderr 交互式审批;CLI `rpc` 子命令已能作为 stdio RPC 入口驱动真实 handler;VS Code 插件已接入 RPC server 启动监管、JSON-RPC request client、Sidebar Chat、事件渲染、真实审批回传、Native diff editor patch 预览、Run List / resume 和 Context Capsule 可视化;TUI 已有审批 prompt 状态机但仍未接入完整 ratatui 界面,优先级排在 VS Code 核心体验之后。 ## 后续增强 diff --git a/docs/json-rpc-protocol.md b/docs/json-rpc-protocol.md index 2ba7847..dd483fd 100644 --- a/docs/json-rpc-protocol.md +++ b/docs/json-rpc-protocol.md @@ -443,6 +443,28 @@ interface ListRunsResult { - `limit` 省略时返回全部已知 run;传入时只返回前 N 条。 - 当前 Rust request loop 已能解析 `agent.listRuns` 并分发给 handler;`AgentTurnLoopRpcHandler` 已能从 Run Log summary metadata 返回列表。 +### `agent.deleteRun` + +删除当前 workspace 内的本地 run log。 + +```ts +interface DeleteRunParams { + runId: string; +} + +interface DeleteRunResult { + runId: string; + deleted: true; +} +``` + +规则: + +- `agent.deleteRun` 仅删除通过 run id 校验后的 `.prole-coder/runs/` 目录;非法 id 不会解析为文件路径。 +- 如果本地 run log 不存在,返回 `E_RUN_NOT_FOUND`。 +- 如果该 run 正在当前 RPC handler 内 active,返回 `E_RUN_ALREADY_ACTIVE`,前端应先取消或等待 run 收口。 +- 当前 Rust request loop 已能解析 `agent.deleteRun` 并分发给 handler;`AgentTurnLoopRpcHandler` 已能删除 inactive run 并保持 list/resume 语义一致。 + ### `agent.previewFim` 请求一次 fill-in-the-middle completion preview。该方法用于编辑器 inline completion,不创建 run,也不写入 run log。 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 067809d..9eb0cf5 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-11 已完成,P5-12 结构化 provider 配置错误码仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-12 已完成,P5-13 结构化 provider 配置错误码仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -109,7 +109,7 @@ | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-11 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context 和 GitLens-like commit / PR 文案生成工作流;P5-12 结构化 provider 配置错误码与恢复动作未完成。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-12 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流,以及 Sidebar 连续会话 / Run 删除 / 折叠事件 UX;P5-13 结构化 provider 配置错误码与恢复动作未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -118,13 +118,14 @@ | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | -| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;API key 配置入口升级为 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | +| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;API key 配置入口升级为 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 按钮修改 alias 和 trash 按钮删除指定 key;删除非 active key 只刷新 SecretStorage/redaction,删除 active key 才更新 child env 并按 idle 状态重启 RPC;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | | [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并在 Sidebar 错误状态保留修复按钮;API key 配置/清除或 model 切换后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | | [x] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | | [x] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | | [x] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | -| [ ] | P5-12:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码;VS Code Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口,避免依赖后端英文错误消息。 | +| [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | +| [ ] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码;VS Code Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口,避免依赖后端英文错误消息。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index ceac835..0c448e3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-11 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox 和 PR markdown 生成;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-12 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-11 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description 以及测试/文档验收收敛;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 +- Phase 5 的 P5-1 到 P5-12 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status(含 add/select/rename/delete)、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description、Sidebar 连续会话、Run 删除、过程事件默认折叠以及测试/文档验收收敛;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 @@ -119,7 +119,8 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P5-2 后 VS Code 主审批弹窗保持 Approve / Reject。 - 审批持久化存储已支持 session/workspace,继续禁止 network/destructive 风险持久化。 - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 -- P5-12:provider 配置失败应从前端字符串匹配升级为 RPC 结构化错误码,例如缺少 API key 时返回可恢复的错误码,供 VS Code/TUI 统一展示配置动作。 +- P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 +- P5-13:provider 配置失败应从前端字符串匹配升级为 RPC 结构化错误码,例如缺少 API key 时返回可恢复的错误码,供 VS Code/TUI 统一展示配置动作。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 @@ -133,7 +134,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-12 未完成,README 不能把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-13 未完成,README 不能把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/rpc-server.md b/docs/rpc-server.md index 11a8b06..5731bb0 100644 --- a/docs/rpc-server.md +++ b/docs/rpc-server.md @@ -149,6 +149,7 @@ RPC 层不负责重新脱敏 payload。当前 Run Log 写入时已经调用基 - `agentRejectMethod` - `agentCancelMethod` - `agentListRunsMethod` +- `agentDeleteRunMethod` - `agentEventMethod` - `JsonRpcRequest` - `JsonRpcResponse` @@ -161,6 +162,7 @@ RPC 层不负责重新脱敏 payload。当前 Run Log 写入时已经调用基 - `CancelParams` / `CancelResult` - `ResumeParams` / `ResumeResult` - `ListRunsParams` / `ListRunsResult` / `RpcRunSummary` +- `DeleteRunParams` / `DeleteRunResult` - `AgentEventEnvelope` - `AgentEventNotification` diff --git a/docs/security-model.md b/docs/security-model.md index 5d6a464..98f1db0 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -7,7 +7,7 @@ ## 边界 - API Key 不得进入 run log。 -- VS Code 插件中的 DeepSeek API key 存入 SecretStorage;多 key 管理器只展示 alias 与 masked key,支持选择 active key 和修改 alias,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 active `DEEPSEEK_API_KEY`。DeepSeek model ID 不是密钥,可通过 VS Code 普通设置保存,并在 child env 中覆盖 `DEEPSEEK_MODEL`。 +- VS Code 插件中的 DeepSeek API key 存入 SecretStorage;多 key 管理器只展示 alias 与 masked key,支持选择 active key、修改 alias 和删除指定 key,优先级高于进程环境变量;传给 RPC 子进程时只通过 child env 覆盖 active `DEEPSEEK_API_KEY`。DeepSeek model ID 不是密钥,可通过 VS Code 普通设置保存,并在 child env 中覆盖 `DEEPSEEK_MODEL`。 - `.env` 和本地状态必须被 git 忽略。 - tool call 执行前必须校验 schema。 - 写入应通过 patch application。 diff --git a/docs/testing.md b/docs/testing.md index 759a4ba..5fa5a30 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -65,7 +65,7 @@ pnpm run vscode:test-electron - Problems diagnostics 被采集为 `agent.sendTurn.attachments` 的 diagnostic attachment。 - `tool.approvalRequired` 经过 test-only auto approval requester 回传为真实 `agent.approve`。 - Chat Cancel UI 边界通过真实 `agent.cancel` 请求收口。 -- Run List refresh 和 `agent.resume` replay 通过同一 `agent.event` 渲染路径更新 timeline。 +- Run List refresh、`agent.resume` replay 和 `agent.deleteRun` run 删除通过同一 typed RPC / `agent.event` 渲染路径更新 timeline。 test-only command 和 auto approval 同时要求 VS Code `ExtensionMode.Test` 以及 `PROLE_CODER_VSCODE_TEST=1` / `PROLE_CODER_VSCODE_TEST_AUTO_APPROVE=1` 环境变量,普通扩展激活不会注册这些测试入口。 @@ -79,13 +79,15 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 -Phase 5 P5-6 到 P5-11 的 API key/model 与 Git 工作流已补齐以下确定性覆盖: +Phase 5 P5-6 到 P5-12 的 API key/model、Git 工作流和 Sidebar 连续会话 UX 已补齐以下确定性覆盖: -- `providerSecrets.test.ts` 覆盖 SecretStorage 多 key store 解析、写入前显式校验、active key 选择、masked key 展示、所有 stored key redaction、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 +- `providerSecrets.test.ts` 覆盖 SecretStorage 多 key store 解析、malformed entry/重复 id/空 key 边界、写入前显式校验、active key 选择、masked key 展示、所有 stored key redaction、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 +- `providerConfigurationUx.test.ts` 覆盖缺少 `DEEPSEEK_API_KEY` 的临时字符串匹配入口,确认不会误判其他 provider 或普通 DeepSeek 请求错误;该逻辑后续由 P5-13 结构化错误码替换。 - `notifier.test.ts` 覆盖 Output Channel/toast message 统一脱敏 SecretStorage/env key。 -- `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 +- `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、trash 按钮删除非 active key、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 -- `rpcServer.test.ts` 覆盖 RPC child env 注入和 key 轮换后重启使用新 env;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 +- `rpcServer.test.ts` 覆盖 RPC child env 注入、key 轮换后重启使用新 env,以及 typed `agent.deleteRun` request;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 +- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 ## 新增测试的协作要求 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index b5f7671..80b3f27 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction 和 Git 工作流均已实现;P5-12 结构化 provider 配置错误码与恢复动作仍未完成。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除和折叠事件 UX 均已实现;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -45,10 +45,11 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server - 在 Activity Bar 暴露 ProleCoder view container 和 Chat view。 - 通过 `RpcServerManager.onEvent()` 订阅 live `agent.event`。 -- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item。 +- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item;tool/provider/request 等过程事件默认折叠,assistant 文本和 terminal summary 默认可见。 - 同一 run/turn 的连续 `assistant.delta` 会合并为一条 assistant 消息,避免流式输出刷屏。 -- 提供 prompt 输入、mode 选择和运行中 Cancel 按钮;通过 Webview `submitTurn` 消息调用 typed `RpcServerManager.sendTurn()`,发送时把 Problems 快照转换为 diagnostic attachments,并按协议 attachment 上限优先保留 error;accepted 后等待同一 run 的 terminal event 收口输入状态,Cancel 会调用 typed `RpcServerManager.cancel()`。 -- 失败状态会在 Sidebar Chat 中显示短消息,并把 sendTurn、Run List refresh/resume/cancel、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误写入 VS Code `Output > ProleCoder`。 +- 提供 prompt 输入、mode 选择和运行中 Cancel 按钮;通过 Webview `submitTurn` 消息调用 typed `RpcServerManager.sendTurn()`,发送时把 Problems 快照转换为 diagnostic attachments,并按协议 attachment 上限优先保留 error;如果当前已 resume/发送过 run,会复用该 `runId` 继续同一会话并由后端递增 `turn_N`;accepted 后等待同一 run 的 terminal event 收口输入状态,Cancel 会调用 typed `RpcServerManager.cancel()`。 +- Run List 支持 `agent.listRuns` / `agent.resume` / `agent.deleteRun`,可回放历史 run、继续多轮会话,也可删除 inactive run。 +- 失败状态会在 Sidebar Chat 中显示短消息,并把 sendTurn、Run List refresh/resume/delete/cancel、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误写入 VS Code `Output > ProleCoder`;Sidebar Chat 还会把完整 `agent.event` payload 写入 Output 便于 debug。 `vscode/extension/src/commands.ts` 还提供 `requestApproval`: @@ -83,7 +84,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server } ``` -配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage 的多 key 管理器,或由 CLI/RPC server 继续按既有规则从环境变量读取;Key 管理器展示 alias 与 masked key,支持添加 key+alias、选择 active key 和改 alias。DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 +配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage 的多 key 管理器,或由 CLI/RPC server 继续按既有规则从环境变量读取;Key 管理器展示 alias 与 masked key,支持添加 key+alias、选择 active key、改 alias 和删除指定 key。DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 ## MVP 分层 @@ -141,13 +142,14 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 -6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key 和行内 edit 按钮修改 alias;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 +6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 按钮修改 alias 和 trash 按钮删除指定 key;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口,Sidebar 错误状态保留 Configure API Key 修复按钮;API key 配置或 model 切换后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 8. P5-8:Git context 只读采集与大 diff attachment 管线,已完成:优先使用 VS Code Git API,git CLI 仅作受控 fallback,commit/PR 命令把 diff context 作为 `explicit_content` attachment 进入 Context Capsule 管线。 9. P5-9:Generate Commit Message,已完成:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit;staged 为空时才询问是否使用 unstaged diff。 10. P5-10:Generate PR Description,已完成:根据 upstream/main/master/用户选择的 base、diff/stat 和 commit summary 生成 PR title/body markdown,用带标题的 untitled markdown 预览承载结果,不自动创建 PR。 11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 -12. P5-12:结构化 provider 配置错误码与恢复动作,未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 +12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX,已完成:Sidebar resume 后继续同一 run 发送多轮 turn,Run List 可通过 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder`。 +13. P5-13:结构化 provider 配置错误码与恢复动作,未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 04a4451a1c4e7c8982bd0f0878bf08cc46f7b14b Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 36/84] Adds agent.deleteRun to TypeScript protocol package --- packages/protocol/src/index.ts | 10 ++++++++++ packages/protocol/test/index.test.ts | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 578eda9..7770bf7 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -9,6 +9,7 @@ export const agentApproveMethod = "agent.approve" as const; export const agentRejectMethod = "agent.reject" as const; export const agentCancelMethod = "agent.cancel" as const; export const agentListRunsMethod = "agent.listRuns" as const; +export const agentDeleteRunMethod = "agent.deleteRun" as const; export const agentPreviewFimMethod = "agent.previewFim" as const; export interface ProtocolErrorDefinition { @@ -510,6 +511,15 @@ export interface ListRunsResult { readonly runs: readonly RunSummary[]; } +export interface DeleteRunParams { + readonly runId: string; +} + +export interface DeleteRunResult { + readonly runId: string; + readonly deleted: true; +} + export interface ApproveParams { readonly approvalId: string; readonly persist?: ApprovalPersistence; diff --git a/packages/protocol/test/index.test.ts b/packages/protocol/test/index.test.ts index f887785..fd41d00 100644 --- a/packages/protocol/test/index.test.ts +++ b/packages/protocol/test/index.test.ts @@ -11,6 +11,7 @@ import { agentRejectMethod, agentCancelMethod, agentListRunsMethod, + agentDeleteRunMethod, agentEventBatchMethod, agentPreviewFimMethod, type ApprovalRequest, @@ -18,6 +19,8 @@ import { type ApproveResult, type CancelParams, type CancelResult, + type DeleteRunParams, + type DeleteRunResult, type AgentEventBatchParams, type FimPreviewParams, type FimPreviewResult, @@ -130,6 +133,7 @@ test("JSON-RPC method constants match protocol document", () => { assert.equal(agentRejectMethod, "agent.reject"); assert.equal(agentCancelMethod, "agent.cancel"); assert.equal(agentListRunsMethod, "agent.listRuns"); + assert.equal(agentDeleteRunMethod, "agent.deleteRun"); assert.equal(agentPreviewFimMethod, "agent.previewFim"); assert.equal(agentEventMethod, "agent.event"); assert.equal(agentEventBatchMethod, "agent.eventBatch"); @@ -332,6 +336,17 @@ test("run summary params and results use stable protocol fields", () => { assert.equal(result.runs[0]?.runId, "run_1"); assert.equal(result.runs[0]?.status, "completed"); assert.equal(result.runs[0]?.lastSeq, 8); + + const deleteParams = { + runId: "run_1", + } satisfies DeleteRunParams; + const deleteResult = { + runId: "run_1", + deleted: true, + } satisfies DeleteRunResult; + + assert.equal(deleteParams.runId, "run_1"); + assert.equal(deleteResult.deleted, true); }); test("attachments and provider completed payload use phase 2c and 2d fields", () => { From a65617af298646aff718988c6559b093f741316a Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 37/84] Implements collapsed process events and event logging in chat timeline --- vscode/extension/src/chatEvents.ts | 24 +++++++++++ vscode/extension/src/chatView.ts | 51 +++++++++++++++++++++++- vscode/extension/test/chatEvents.test.ts | 3 ++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/vscode/extension/src/chatEvents.ts b/vscode/extension/src/chatEvents.ts index c954149..c9497bf 100644 --- a/vscode/extension/src/chatEvents.ts +++ b/vscode/extension/src/chatEvents.ts @@ -28,6 +28,7 @@ export interface ChatTimelineItem { readonly title: string; readonly body?: string | undefined; readonly detail?: string | undefined; + readonly defaultCollapsed?: boolean; } export interface ChatTimelineSnapshot { @@ -169,6 +170,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem tone: "running", title: "Run started", body: joinParts([label("Mode", textField(payload, "mode")), label("Workspace", textField(payload, "workspaceRoot"))]), + defaultCollapsed: true, }; case "turn.started": return { @@ -177,6 +179,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem tone: "running", title: "Turn started", body: textField(payload, "userTask") ?? textField(payload, "prompt") ?? compactJson(event.payload), + defaultCollapsed: true, }; case "context.built": return { @@ -190,6 +193,20 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem label("Sources", arrayCount(payload, "includedSources")), label("Omitted", arrayCount(payload, "omittedSources")), ]), + defaultCollapsed: true, + }; + case "provider.requested": + return { + ...base, + kind: "provider", + tone: "running", + title: "Provider request", + body: joinParts([ + label("Iteration", valueText(payload, "iteration")), + label("Messages", valueText(payload, "messageCount")), + label("Reasoning", valueText(payload, "reasoningState")), + ]), + defaultCollapsed: true, }; case "provider.completed": return { @@ -203,6 +220,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem label("Duration", suffix(valueText(payload, "durationMs"), "ms")), label("Total tokens", nestedValueText(payload, "usage", "totalTokens")), ]), + defaultCollapsed: true, }; case "tool.requested": return { @@ -215,6 +233,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem label("Reasons", arrayText(payload, "riskReasons")), label("Args", valueText(payload, "argumentsPreview")), ]), + defaultCollapsed: true, }; case "tool.approvalRequired": return { @@ -230,6 +249,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem label("Command", textField(payload, "command")), label("Paths", arrayText(payload, "paths")), ]), + defaultCollapsed: true, }; case "tool.approvalResolved": { const decision = textField(payload, "decision") ?? "resolved"; @@ -239,6 +259,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem tone: approvalTone(decision), title: `Approval ${decision}`, body: joinParts([label("Tool", textField(payload, "toolName")), label("Reason", textField(payload, "reason"))]), + defaultCollapsed: true, }; } case "tool.started": @@ -248,6 +269,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem tone: "running", title: `Tool started: ${toolName(payload)}`, body: label("Call", textField(payload, "toolCallId")), + defaultCollapsed: true, }; case "tool.completed": { const status = textField(payload, "status"); @@ -261,6 +283,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem textField(payload, "summary"), label("Files", nestedValueText(payload, "result", "files")), ]), + defaultCollapsed: true, }; } case "run.completed": @@ -294,6 +317,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem tone: "neutral", title: event.type, body: compactJson(event.payload), + defaultCollapsed: true, }; } } diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 2dfe45c..61acbb9 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -876,6 +876,27 @@ function renderChatViewHtml( background: var(--vscode-input-background); } + details.item > summary { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 6px; + cursor: pointer; + list-style-position: inside; + } + + details.item > summary .meta, + details.item > summary .title { + min-width: 0; + } + + details.item > summary .meta { + grid-column: 1 / -1; + } + + details.item > summary .title { + grid-column: 1 / -1; + } + .item.running { border-left-color: var(--vscode-progressBar-background); } @@ -953,6 +974,7 @@ function renderChatViewHtml( .provider-action:focus, .refresh-runs:focus, .run-entry:focus, + .run-delete:focus, .context-tab:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; @@ -1594,7 +1616,8 @@ function renderChatViewHtml( } function renderItem(item) { - const article = document.createElement("article"); + const collapsed = item.defaultCollapsed === true; + const article = document.createElement(collapsed ? "details" : "article"); article.className = "item " + item.kind + " " + item.tone; const meta = document.createElement("div"); @@ -1611,7 +1634,13 @@ function renderChatViewHtml( title.className = "title"; title.textContent = item.title; - article.append(meta, title); + if (collapsed) { + const summary = document.createElement("summary"); + summary.append(meta, title); + article.append(summary); + } else { + article.append(meta, title); + } if (item.body) { const body = document.createElement("div"); body.className = "body"; @@ -1697,6 +1726,24 @@ function terminalMessage(event: AgentEventEnvelope, fallback: string): string { return typeof message === "string" && message.length > 0 ? message : fallback; } +function formatAgentEventLog(event: AgentEventEnvelope): string { + const turn = event.turnId === undefined ? "" : ` turn=${event.turnId}`; + return `agent.event #${event.seq} ${event.type} run=${event.runId}${turn} ${stringifyLogPayload(event.payload)}`; +} + +function stringifyLogPayload(payload: unknown): string { + try { + const serialized = JSON.stringify(payload); + return truncateLogText(serialized === undefined ? "undefined" : serialized); + } catch { + return truncateLogText(String(payload)); + } +} + +function truncateLogText(value: string): string { + return value.length <= 4096 ? value : `${value.slice(0, 4093)}...`; +} + function cancelRunIdFromMessage(message: unknown): string | undefined { if (!isRecord(message) || message["type"] !== "cancelTurn") { return undefined; diff --git a/vscode/extension/test/chatEvents.test.ts b/vscode/extension/test/chatEvents.test.ts index 2e7313f..0ae25fa 100644 --- a/vscode/extension/test/chatEvents.test.ts +++ b/vscode/extension/test/chatEvents.test.ts @@ -61,7 +61,9 @@ test("chat timeline renders tool lifecycle and terminal events", () => { ["Tool requested: shell", "Tool started: shell", "Tool completed: shell", "Run completed"], ); assert.equal(snapshot.items[2]?.tone, "success"); + assert.equal(snapshot.items[2]?.defaultCollapsed, true); assert.equal(snapshot.items[3]?.kind, "terminal"); + assert.equal(snapshot.items[3]?.defaultCollapsed, undefined); assert.equal(snapshot.latestStatus, "Completed"); }); @@ -128,6 +130,7 @@ test("chat timeline renders raw unknown events with compact payloads", () => { assert.equal(item.kind, "raw"); assert.equal(item.title, "custom.event"); + assert.equal(item.defaultCollapsed, true); assert.ok(item.body?.includes("unmapped")); }); From 8362109d1244873740f7a8e05c21aafcfff31d38 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 38/84] Implements multi-turn conversation and run deletion in Sidebar Chat --- vscode/extension/src/chatView.ts | 100 +++++++++++++++++- vscode/extension/src/extension.ts | 4 + vscode/extension/src/rpcServer.ts | 7 ++ vscode/extension/src/runHistory.ts | 19 ++++ .../test/fixtures/rpcFixtureServer.mjs | 28 ++++- 5 files changed, 152 insertions(+), 6 deletions(-) diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 61acbb9..d045589 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -4,6 +4,8 @@ import * as vscode from "vscode"; import type { CancelParams, CancelResult, + DeleteRunParams, + DeleteRunResult, ListRunsParams, ListRunsResult, ResumeParams, @@ -42,6 +44,7 @@ import { failedRunList, idleRunList, isRefreshRunsMessage, + deleteRunIdFromMessage, loadingRunList, readyRunList, resumeRunIdFromMessage, @@ -65,6 +68,7 @@ export interface ChatCancelClient { export interface ChatRunHistoryClient { listRuns(params?: ListRunsParams): Promise; resume(params: ResumeParams): Promise; + deleteRun(params: DeleteRunParams): Promise; } export type ChatRpcClient = ChatRpcEventSource & ChatTurnSender & ChatCancelClient & ChatRunHistoryClient; @@ -134,6 +138,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private submission: ChatSubmissionSnapshot = idleSubmission(); private runList: RunListSnapshot = idleRunList(); private contextViz: ContextVizSnapshot = emptyContextViz(); + private activeConversationRunId: string | undefined; private rpcSubscription: DisposableLike | undefined; private viewMessageSubscription: DisposableLike | undefined; private view: vscode.WebviewView | undefined; @@ -147,6 +152,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos ) { this.rpcClient = rpcClient; this.rpcSubscription = rpcClient?.onEvent((event) => { + this.logger?.info(this.redact(formatAgentEventLog(event))); this.timeline.append(event); const contextViz = contextVizFromEvent(event); if (contextViz !== undefined) { @@ -228,6 +234,12 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } + const deleteRunId = deleteRunIdFromMessage(message); + if (deleteRunId !== undefined) { + await this.deleteRun(deleteRunId); + return; + } + const cancelRunId = cancelRunIdFromMessage(message); if (cancelRunId !== undefined) { await this.cancelTurn(cancelRunId); @@ -290,9 +302,15 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos automaticContext, this.collectDiagnosticAttachments(), ); + const activeRunId = this.activeConversationRunId; + if (activeRunId !== undefined) { + this.terminalRuns.delete(activeRunId); + } + const params = sendTurnParams(parsed.value, attachments); const result = await this.rpcClient.sendTurn( - sendTurnParams(parsed.value, attachments), + activeRunId === undefined ? params : { ...params, runId: activeRunId }, ); + this.activeConversationRunId = result.runId; void this.refreshRuns("Refreshing runs..."); const terminal = this.terminalRuns.get(result.runId); this.setSubmission( @@ -382,7 +400,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.setRunList(loadingRunList(this.runList, message)); try { const result = await this.rpcClient.listRuns({ limit: RUN_LIST_LIMIT }); - this.setRunList(readyRunList(result, this.runList.selectedRunId)); + this.setRunList(readyRunList(result, this.activeConversationRunId ?? this.runList.selectedRunId)); } catch (error) { const messageText = `Failed to load runs: ${errorMessage(error)}`; const redacted = this.redact(messageText); @@ -411,6 +429,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.setRunList(loadingRunList(this.runList, "Replaying run...")); try { const result = await this.rpcClient.resume({ runId }); + this.activeConversationRunId = result.runId; const message = result.replayStarted ? `Replaying ${result.runId} through seq ${result.nextSeq - 1}.` : `No events to replay for ${result.runId}.`; @@ -423,6 +442,41 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } } + private async deleteRun(runId: string): Promise { + if (this.rpcClient === undefined) { + this.setRunList( + failedRunList("Open a trusted workspace before deleting a run.", this.runList), + ); + return; + } + + if (this.submission.busy) { + this.setRunList(failedRunList("A turn is already running.", this.runList)); + return; + } + + this.setRunList(loadingRunList(this.runList, "Deleting run...")); + try { + const result = await this.rpcClient.deleteRun({ runId }); + const runs = this.runList.runs.filter((run) => run.runId !== result.runId); + if (this.activeConversationRunId === result.runId) { + this.activeConversationRunId = undefined; + this.timeline.clear(); + this.terminalRuns.delete(result.runId); + this.setSubmission(idleSubmission()); + this.setContextViz(emptyContextViz()); + this.postSnapshot(); + } + this.setRunList(readyRunList({ runs }, undefined, `Deleted ${result.runId}.`)); + void this.refreshRuns("Refreshing runs..."); + } catch (error) { + const messageText = `Failed to delete run: ${errorMessage(error)}`; + const redacted = this.redact(messageText); + this.logger?.error(redacted); + this.setRunList(failedRunList(redacted, this.runList)); + } + } + private async cancelTurn(runId: string): Promise { if (this.rpcClient === undefined) { this.setSubmission({ @@ -644,6 +698,13 @@ function renderChatViewHtml( overflow: auto; } + .run-entry-row { + display: grid; + grid-template-columns: minmax(0, 1fr) 28px; + align-items: stretch; + gap: 4px; + } + .run-entry { display: grid; gap: 2px; @@ -659,6 +720,23 @@ function renderChatViewHtml( text-align: left; } + .run-delete { + width: 28px; + min-height: 48px; + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border: 1px solid transparent; + font: var(--vscode-font-size) var(--vscode-font-family); + } + + .run-delete:hover:enabled { + background: var(--vscode-button-secondaryHoverBackground); + } + + .run-delete:disabled { + opacity: 0.55; + } + .run-entry:hover:enabled, .run-entry.selected { background: var(--vscode-list-hoverBackground); @@ -1281,6 +1359,8 @@ function renderChatViewHtml( } function renderRunEntry(run, selectedRunId, disabled) { + const row = document.createElement("div"); + row.className = "run-entry-row"; const button = document.createElement("button"); const status = typeof run.status === "string" ? run.status : "running"; button.type = "button"; @@ -1305,8 +1385,22 @@ function renderChatViewHtml( meta.append(item); } + const deleteButton = document.createElement("button"); + deleteButton.type = "button"; + deleteButton.className = "run-delete"; + deleteButton.disabled = disabled; + deleteButton.title = "Delete run"; + deleteButton.setAttribute("aria-label", "Delete run"); + deleteButton.textContent = "x"; + deleteButton.addEventListener("click", () => { + if (typeof run.runId === "string" && run.runId.length > 0 && confirm("Delete this run?")) { + vscodeApi.postMessage({ type: "deleteRun", runId: run.runId }); + } + }); + button.append(title, meta); - return button; + row.append(button, deleteButton); + return row; } function runTitle(run) { diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index e1c5c6e..96d1755 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -106,6 +106,10 @@ export async function activate(context: vscode.ExtensionContext): Promise iconPath: new vscode.ThemeIcon("edit"), tooltip: "Rename alias", }, + deleteKeyButton: { + iconPath: new vscode.ThemeIcon("trash"), + tooltip: "Delete key", + }, }); const gitRepositoryProvider = createVscodeGitRepositoryProvider(); const markdownSink = createVscodeMarkdownSink(); diff --git a/vscode/extension/src/rpcServer.ts b/vscode/extension/src/rpcServer.ts index 1a66716..14e69da 100644 --- a/vscode/extension/src/rpcServer.ts +++ b/vscode/extension/src/rpcServer.ts @@ -6,6 +6,8 @@ import type { ApproveResult, CancelParams, CancelResult, + DeleteRunParams, + DeleteRunResult, FimPreviewParams, FimPreviewResult, ListRunsParams, @@ -28,6 +30,7 @@ export const RPC_EVENT_BATCH_METHOD = "agent.eventBatch"; export const RPC_SEND_TURN_METHOD = "agent.sendTurn"; export const RPC_RESUME_METHOD = "agent.resume"; export const RPC_LIST_RUNS_METHOD = "agent.listRuns"; +export const RPC_DELETE_RUN_METHOD = "agent.deleteRun"; export const RPC_APPROVE_METHOD = "agent.approve"; export const RPC_REJECT_METHOD = "agent.reject"; export const RPC_CANCEL_METHOD = "agent.cancel"; @@ -345,6 +348,10 @@ export class RpcServerManager implements DisposableLike { return this.sendRequest(RPC_LIST_RUNS_METHOD, params); } + deleteRun(params: DeleteRunParams): Promise { + return this.sendRequest(RPC_DELETE_RUN_METHOD, params); + } + approve(params: ApproveParams): Promise { return this.sendRequest(RPC_APPROVE_METHOD, params); } diff --git a/vscode/extension/src/runHistory.ts b/vscode/extension/src/runHistory.ts index 677e6a8..8ccd098 100644 --- a/vscode/extension/src/runHistory.ts +++ b/vscode/extension/src/runHistory.ts @@ -22,6 +22,11 @@ export interface ResumeRunMessage { readonly runId: string; } +export interface DeleteRunMessage { + readonly type: "deleteRun"; + readonly runId: string; +} + export function idleRunList(): RunListSnapshot { return { status: "idle", @@ -88,6 +93,20 @@ export function resumeRunIdFromMessage(message: unknown): string | undefined { return trimmed.length > 0 ? trimmed : undefined; } +export function deleteRunIdFromMessage(message: unknown): string | undefined { + if (!isRecord(message) || message["type"] !== "deleteRun") { + return undefined; + } + + const runId = message["runId"]; + if (typeof runId !== "string") { + return undefined; + } + + const trimmed = runId.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/vscode/extension/test/fixtures/rpcFixtureServer.mjs b/vscode/extension/test/fixtures/rpcFixtureServer.mjs index 5e2d992..04e22a1 100644 --- a/vscode/extension/test/fixtures/rpcFixtureServer.mjs +++ b/vscode/extension/test/fixtures/rpcFixtureServer.mjs @@ -84,6 +84,9 @@ function handleRequest(request) { case "agent.resume": handleResume(request); return; + case "agent.deleteRun": + handleDeleteRun(request); + return; default: respondError(request.id, -32601, `Unknown method: ${request.method}`); } @@ -92,14 +95,19 @@ function handleRequest(request) { function handleSendTurn(request) { const params = record(request.params); const message = typeof params?.message === "string" ? params.message : ""; - const runId = message.includes("cancel") ? "run-cancel-1" : "run-approval-1"; - const turnId = message.includes("cancel") ? "turn-cancel-1" : "turn-approval-1"; + const runId = typeof params?.runId === "string" && params.runId.length > 0 + ? params.runId + : message.includes("cancel") ? "run-cancel-1" : "run-approval-1"; + const existing = runs.get(runId); + const turnIndex = typeof existing?.eventCount === "number" ? existing.eventCount + 1 : 1; + const turnId = message.includes("cancel") ? "turn-cancel-1" : `turn-${turnIndex}`; const now = new Date().toISOString(); runs.set(runId, { + ...(existing ?? {}), runId, title: message || "Untitled fixture run", status: "running", - startedAt: now, + startedAt: existing?.startedAt ?? now, updatedAt: now, lastSeq: seq, eventCount: 0, @@ -148,6 +156,20 @@ function handleSendTurn(request) { }, 10); } +function handleDeleteRun(request) { + const runId = request.params?.runId; + if (!runs.has(runId)) { + respondError(request.id, -32003, `Run not found: ${runId}`); + return; + } + + runs.delete(runId); + respond(request.id, { + runId, + deleted: true, + }); +} + function handleApprove(request) { const approvalId = request.params?.approvalId; respond(request.id, { From 86e6dc4a9219d8f3fa7a6903491927f1b2bfaef5 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 39/84] Adds key deletion support to SecretStorage key manager --- .../extension/src/providerSecretCommands.ts | 65 +++++++++++++++++-- .../test/providerSecretCommands.test.ts | 64 ++++++++++++++++-- 2 files changed, 119 insertions(+), 10 deletions(-) diff --git a/vscode/extension/src/providerSecretCommands.ts b/vscode/extension/src/providerSecretCommands.ts index df4310e..35e8aa9 100644 --- a/vscode/extension/src/providerSecretCommands.ts +++ b/vscode/extension/src/providerSecretCommands.ts @@ -26,6 +26,7 @@ export const CONFIGURE_DEEPSEEK_API_KEY_COMMAND = "prole-coder.configureDeepSeek export const CLEAR_DEEPSEEK_API_KEY_COMMAND = "prole-coder.clearDeepSeekApiKey"; export const SHOW_PROVIDER_STATUS_COMMAND = "prole-coder.showProviderStatus"; export const SELECT_DEEPSEEK_MODEL_COMMAND = "prole-coder.selectDeepSeekModel"; +const DELETE_DEEPSEEK_API_KEY_CONFIRM_LABEL = "Delete"; export interface CommandRegistry { registerCommand(command: string, callback: () => unknown): DisposableLike; @@ -60,7 +61,7 @@ export interface SecretWindowMessenger { ): T | undefined | PromiseLike; createQuickPick(): SecretQuickPickController; showInformationMessage(message: string): unknown; - showWarningMessage(message: string): unknown; + showWarningMessage(message: string, ...items: readonly string[]): unknown; } export interface SecretQuickInputButton { @@ -118,6 +119,7 @@ export interface RegisterProviderSecretCommandOptions { readonly providerConfiguration?: ProviderConfigurationLike | undefined; readonly configurationTarget?: unknown; readonly renameAliasButton?: SecretQuickInputButton | undefined; + readonly deleteKeyButton?: SecretQuickInputButton | undefined; } export interface ProviderSecretRuntimeState { @@ -246,10 +248,13 @@ async function showDeepSeekApiKeyManager( let store = await readDeepSeekApiKeyStore(options.secrets); const quickPick = options.window.createQuickPick(); quickPick.title = "Select DeepSeek API Key"; - quickPick.placeholder = "Choose a key, add a new one, or use the edit button to rename an alias."; + quickPick.placeholder = "Choose a key, add one, or use item buttons to rename or delete."; quickPick.ignoreFocusOut = true; quickPick.matchOnDescription = true; - quickPick.items = deepSeekApiKeyPickItems(store, options.renameAliasButton); + quickPick.items = deepSeekApiKeyPickItems(store, { + deleteKeyButton: options.deleteKeyButton, + renameAliasButton: options.renameAliasButton, + }); return new Promise((resolve) => { let finished = false; @@ -306,7 +311,11 @@ async function showDeepSeekApiKeyManager( actionInProgress = true; quickPick.hide(); void (async () => { - finish(await renameDeepSeekApiKeyAlias(options, state, store, event.item.keyId)); + finish( + event.button === options.deleteKeyButton + ? await deleteDeepSeekApiKey(options, state, store, event.item.keyId) + : await renameDeepSeekApiKeyAlias(options, state, store, event.item.keyId), + ); })(); }), ); @@ -395,6 +404,45 @@ async function renameDeepSeekApiKeyAlias( return status; } +async function deleteDeepSeekApiKey( + options: RegisterProviderSecretCommandOptions, + state: ProviderSecretRuntimeState, + store: DeepSeekApiKeyStore, + keyId: string, +): Promise { + const entry = store.entries.find((candidate) => candidate.id === keyId); + if (entry === undefined) { + return undefined; + } + + const confirmation = await options.window.showWarningMessage( + `Delete DeepSeek API key "${entry.alias}"?`, + DELETE_DEEPSEEK_API_KEY_CONFIRM_LABEL, + ); + if (confirmation !== DELETE_DEEPSEEK_API_KEY_CONFIRM_LABEL) { + return undefined; + } + + const activeKeyId = selectedDeepSeekApiKeyEntry(store)?.id; + const entries = store.entries.filter((candidate) => candidate.id !== keyId); + const nextSelectedKeyId = + activeKeyId === keyId + ? entries[0]?.id + : entries.some((candidate) => candidate.id === store.selectedKeyId) + ? store.selectedKeyId + : entries[0]?.id; + await saveDeepSeekApiKeyStore(options.secrets, { + selectedKeyId: nextSelectedKeyId, + entries, + }); + const status = await refreshProviderSecretState(options, state); + if (activeKeyId === keyId) { + await restartRpcAfterSecretChange(options, "DeepSeek API key deleted."); + } + options.window.showInformationMessage(`DeepSeek API key deleted: ${entry.alias}`); + return status; +} + async function clearSelectedDeepSeekApiKey( options: Pick, ): Promise { @@ -461,14 +509,19 @@ interface DeepSeekApiKeyPickItem extends SecretQuickPickItem { function deepSeekApiKeyPickItems( store: DeepSeekApiKeyStore, - renameAliasButton: SecretQuickInputButton | undefined, + buttons: { + readonly deleteKeyButton?: SecretQuickInputButton | undefined; + readonly renameAliasButton?: SecretQuickInputButton | undefined; + }, ): readonly DeepSeekApiKeyPickItem[] { const selected = selectedDeepSeekApiKeyEntry(store); const keyItems = store.entries.map((entry) => ({ label: entry.alias, description: `${maskDeepSeekApiKey(entry.apiKey)}${entry.id === selected?.id ? " (current)" : ""}`, detail: "Stored in VS Code SecretStorage", - buttons: renameAliasButton === undefined ? [] : [renameAliasButton], + buttons: [buttons.renameAliasButton, buttons.deleteKeyButton].filter( + (button): button is SecretQuickInputButton => button !== undefined, + ), kind: "key" as const, keyId: entry.id, })); diff --git a/vscode/extension/test/providerSecretCommands.test.ts b/vscode/extension/test/providerSecretCommands.test.ts index 1c1bbb2..b050397 100644 --- a/vscode/extension/test/providerSecretCommands.test.ts +++ b/vscode/extension/test/providerSecretCommands.test.ts @@ -140,6 +140,55 @@ test("configure DeepSeek API key renames an existing key alias without restartin assert.deepEqual(window.infos.at(-1), "DeepSeek API key alias updated: Team"); }); +test("configure DeepSeek API key deletes a non-selected key without restarting RPC", async () => { + const commands = new FakeCommands(); + const window = new FakeSecretWindow([], [], [{ label: "Personal", button: 1 }], ["Delete"]); + const secrets = new FakeSecrets({ + [DEEPSEEK_API_KEY_STORE_SECRET_ID]: serializeDeepSeekApiKeyStore({ + selectedKeyId: "work", + entries: [ + { + id: "work", + alias: "Work", + apiKey: "work-secret-value", + }, + { + id: "personal", + alias: "Personal", + apiKey: "personal-secret-value", + }, + ], + }), + }); + const rpc = new FakeRpcServer("ready"); + + registerProviderSecretCommands({ + commands, + window, + secrets, + rpcServer: rpc, + isRpcIdle: () => true, + renameAliasButton: FakeSecretWindow.renameButton, + deleteKeyButton: FakeSecretWindow.deleteButton, + }); + + await commands.run(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); + + const keyStore = parseDeepSeekApiKeyStore(await secrets.get(DEEPSEEK_API_KEY_STORE_SECRET_ID)); + assert.equal(keyStore.selectedKeyId, "work"); + assert.deepEqual( + keyStore.entries.map((entry) => entry.id), + ["work"], + ); + assert.deepEqual(rpc.processEnv, { + [DEEPSEEK_API_KEY_ENV]: "work-secret-value", + }); + assert.equal(rpc.stopCount, 0); + assert.equal(rpc.startCount, 0); + assert.deepEqual(window.warnings.at(-1), 'Delete DeepSeek API key "Personal"?'); + assert.deepEqual(window.infos.at(-1), "DeepSeek API key deleted: Personal"); +}); + test("clear DeepSeek API key falls back to process env without restarting active RPC", async () => { const commands = new FakeCommands(); const window = new FakeSecretWindow([]); @@ -259,6 +308,7 @@ class FakeCommands { class FakeSecretWindow { static readonly renameButton = { tooltip: "Rename alias" }; + static readonly deleteButton = { tooltip: "Delete key" }; readonly infos: string[] = []; readonly warnings: string[] = []; @@ -267,6 +317,7 @@ class FakeSecretWindow { private readonly inputValues: string[], private readonly quickPickLabels: string[] = [], private readonly quickPickInteractions: FakeQuickPickInteraction[] = [], + private readonly warningSelections: string[] = [], ) {} showInputBox(): string | undefined { @@ -289,14 +340,18 @@ class FakeSecretWindow { this.infos.push(message); } - showWarningMessage(message: string): void { + showWarningMessage(message: string, ...items: readonly string[]): string | undefined { this.warnings.push(message); + if (items.length === 0) { + return undefined; + } + return this.warningSelections.shift(); } } interface FakeQuickPickInteraction { readonly label: string; - readonly button?: boolean; + readonly button?: boolean | number; } class FakeQuickPick { @@ -338,8 +393,9 @@ class FakeQuickPick candidate.label === interaction.label); assert.ok(item, `${interaction.label} should exist in quick pick`); - if (interaction.button === true) { - const button = item.buttons?.[0]; + if (interaction.button !== undefined && interaction.button !== false) { + const buttonIndex = interaction.button === true ? 0 : interaction.button; + const button = item.buttons?.[buttonIndex]; assert.ok(button, `${interaction.label} should have a button`); for (const callback of this.buttonCallbacks) { callback({ button, item }); From 441cbd071b18d2039686e5d4adec5e68cff6a93c Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:50 +0800 Subject: [PATCH 40/84] Adds defensive parsing for malformed API key store entries --- vscode/extension/test/providerSecrets.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/vscode/extension/test/providerSecrets.test.ts b/vscode/extension/test/providerSecrets.test.ts index b76992c..6a61dcc 100644 --- a/vscode/extension/test/providerSecrets.test.ts +++ b/vscode/extension/test/providerSecrets.test.ts @@ -137,6 +137,48 @@ test("serializeDeepSeekApiKeyStore validates entries before writing", () => { }); }); +test("parseDeepSeekApiKeyStore skips malformed entries defensively", () => { + const store = parseDeepSeekApiKeyStore( + JSON.stringify({ + version: 1, + selectedKeyId: " duplicate ", + keys: [ + "not an entry", + { + id: " duplicate ", + alias: " First Key ", + apiKey: " first-secret-value ", + }, + { + id: "duplicate", + alias: "Duplicate", + apiKey: "second-secret-value", + }, + { + id: "empty-key", + alias: "Empty", + apiKey: " ", + }, + { + id: "missing-key", + alias: "Missing", + }, + ], + }), + ); + + assert.deepEqual(store, { + selectedKeyId: "duplicate", + entries: [ + { + id: "duplicate", + alias: "First Key", + apiKey: "first-secret-value", + }, + ], + }); +}); + test("resolveDeepSeekApiKey falls back to process env and reports missing", () => { const envResolution = resolveDeepSeekApiKey({ processEnv: { From b0dbcdd050e581f7c5e6b560c10c2d36bb607c18 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:51 +0800 Subject: [PATCH 41/84] Adds provider configuration UX test for missing API key detection --- .../test/providerConfigurationUx.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 vscode/extension/test/providerConfigurationUx.test.ts diff --git a/vscode/extension/test/providerConfigurationUx.test.ts b/vscode/extension/test/providerConfigurationUx.test.ts new file mode 100644 index 0000000..a9a7f68 --- /dev/null +++ b/vscode/extension/test/providerConfigurationUx.test.ts @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { isDeepSeekApiKeyRequiredMessage } from "../src/providerConfigurationUx.js"; + +test("isDeepSeekApiKeyRequiredMessage detects missing DeepSeek API key errors", () => { + assert.equal( + isDeepSeekApiKeyRequiredMessage( + "DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required", + ), + true, + ); + assert.equal(isDeepSeekApiKeyRequiredMessage("OpenAI API key is required"), false); + assert.equal(isDeepSeekApiKeyRequiredMessage("DeepSeek request failed with status 401"), false); +}); From 937a4f8c7b108a9960dec4e0de2136c2d08b0931 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 05:58:51 +0800 Subject: [PATCH 42/84] Adds typed deleteRun test coverage to RPC server and run history tests --- vscode/extension/test/rpcServer.test.ts | 22 +++++++++++++++++++++- vscode/extension/test/runHistory.test.ts | 7 ++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/vscode/extension/test/rpcServer.test.ts b/vscode/extension/test/rpcServer.test.ts index a071839..fc9e2dd 100644 --- a/vscode/extension/test/rpcServer.test.ts +++ b/vscode/extension/test/rpcServer.test.ts @@ -6,6 +6,7 @@ import { DEFAULT_RPC_COMMAND, RPC_APPROVE_METHOD, RPC_CANCEL_METHOD, + RPC_DELETE_RUN_METHOD, RPC_EVENT_BATCH_METHOD, RPC_INITIALIZE_METHOD, RPC_LIST_RUNS_METHOD, @@ -318,7 +319,7 @@ test("RPC server manager sends typed agent.sendTurn requests and resolves matchi }); }); -test("RPC server manager sends typed run list and resume requests", async () => { +test("RPC server manager sends typed run list, resume, and delete requests", async () => { const factory = new FakeProcessFactory(); const manager = rpcManagerWithFactory(factory); const readyPromise = manager.start(); @@ -388,6 +389,25 @@ test("RPC server manager sends typed run list and resume requests", async () => nextSeq: 9, replayStarted: true, }); + + const deletePromise = manager.deleteRun({ runId: "run_1" }); + await flushMicrotasks(); + const deleteRequest = child.requestAt(3); + assert.equal(deleteRequest.method, RPC_DELETE_RUN_METHOD); + assert.deepEqual(deleteRequest.params, { runId: "run_1" }); + child.stdout.pushJson({ + jsonrpc: "2.0", + id: deleteRequest.id, + result: { + runId: "run_1", + deleted: true, + }, + }); + + assert.deepEqual(await deletePromise, { + runId: "run_1", + deleted: true, + }); }); test("RPC server manager sends typed approval requests", async () => { diff --git a/vscode/extension/test/runHistory.test.ts b/vscode/extension/test/runHistory.test.ts index 5387127..bff8572 100644 --- a/vscode/extension/test/runHistory.test.ts +++ b/vscode/extension/test/runHistory.test.ts @@ -6,6 +6,7 @@ import { failedRunList, idleRunList, isRefreshRunsMessage, + deleteRunIdFromMessage, loadingRunList, readyRunList, resumeRunIdFromMessage, @@ -46,7 +47,7 @@ test("run history leaves result limiting to the RPC server", () => { assert.equal(readyRunList({ runs }).runs.length, RUN_LIST_LIMIT + 1); }); -test("run history parses refresh and resume webview messages defensively", () => { +test("run history parses refresh, resume, and delete webview messages defensively", () => { assert.equal(RUN_LIST_LIMIT, 20); assert.equal(isRefreshRunsMessage({ type: "refreshRuns" }), true); assert.equal(isRefreshRunsMessage({ type: "refreshRuns", runId: "run_1" }), true); @@ -54,7 +55,11 @@ test("run history parses refresh and resume webview messages defensively", () => assert.equal(resumeRunIdFromMessage({ type: "resumeRun", runId: " run_1 " }), "run_1"); assert.equal(resumeRunIdFromMessage({ type: "resumeRun", runId: " " }), undefined); assert.equal(resumeRunIdFromMessage({ type: "resumeRun", runId: 1 }), undefined); + assert.equal(deleteRunIdFromMessage({ type: "deleteRun", runId: " run_1 " }), "run_1"); + assert.equal(deleteRunIdFromMessage({ type: "deleteRun", runId: " " }), undefined); + assert.equal(deleteRunIdFromMessage({ type: "deleteRun", runId: 1 }), undefined); assert.equal(resumeRunIdFromMessage(idleRunList()), undefined); + assert.equal(deleteRunIdFromMessage(idleRunList()), undefined); }); function runSummary(runId: string) { From 889e280d8fef5261d9f9d00f95b4e63535a329f3 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:40 +0800 Subject: [PATCH 43/84] Marks P5-13 as complete and adds P5-14 placeholder across documentation --- README.md | 7 ++++--- docs/phase-tasks.md | 7 ++++--- docs/roadmap.md | 9 +++++---- docs/testing.md | 8 +++++--- docs/vscode-extension.md | 5 +++-- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 35f824d..7329fee 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和过程事件默认折叠;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置和 Git 工作流均已完成。完成 P5-13 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -706,7 +706,8 @@ extension.ts - [x] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版只提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 - [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 - [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX:`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar resume 后继续同一会话发送多轮 turn;Run List 支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder` 便于 debug。 -- [ ] P5-13:结构化 provider 配置错误码与恢复动作:将缺 API key 等配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 +- [x] P5-13:结构化 provider 配置错误码与恢复动作:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回结构化 `data.recoverableAction`,Sidebar/原生 Chat 依据恢复动作展示或触发 Configure API Key 入口,不再依赖后端英文错误消息;run failed payload 也可携带同一恢复动作。 +- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 验收标准: @@ -714,7 +715,7 @@ extension.ts - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 - 主审批弹窗保持简单;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-13 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 +- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 9eb0cf5..d3c95fd 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-12 已完成,P5-13 结构化 provider 配置错误码仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成,P5-14 持续 UX 测试与体验改进占位仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -109,7 +109,7 @@ | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-12 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流,以及 Sidebar 连续会话 / Run 删除 / 折叠事件 UX;P5-13 结构化 provider 配置错误码与恢复动作未完成。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -125,7 +125,8 @@ | [x] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | -| [ ] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码;VS Code Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口,避免依赖后端英文错误消息。 | +| [x] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | +| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已登记 extension-host E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、原生确认后的 run 删除与 Run List 刷新。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 0c448e3..3da6418 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-12 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-12 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status(含 add/select/rename/delete)、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description、Sidebar 连续会话、Run 删除、过程事件默认折叠以及测试/文档验收收敛;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。 +- Phase 5 的 P5-1 到 P5-13 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status(含 add/select/rename/delete)、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description、Sidebar 连续会话、Run 删除、过程事件默认折叠、结构化 provider 配置错误码与恢复动作以及测试/文档验收收敛;P5-14 持续 UX 测试与体验改进占位仍未完成。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 @@ -120,7 +120,8 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 审批持久化存储已支持 session/workspace,继续禁止 network/destructive 风险持久化。 - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 -- P5-13:provider 配置失败应从前端字符串匹配升级为 RPC 结构化错误码,例如缺少 API key 时返回可恢复的错误码,供 VS Code/TUI 统一展示配置动作。 +- P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 +- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 @@ -134,7 +135,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-13 未完成,README 不能把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-14 未完成,README 不能把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/testing.md b/docs/testing.md index 5fa5a30..d3bc733 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -79,17 +79,19 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 -Phase 5 P5-6 到 P5-12 的 API key/model、Git 工作流和 Sidebar 连续会话 UX 已补齐以下确定性覆盖: +Phase 5 P5-6 到 P5-13 的 API key/model、Git 工作流、Sidebar 连续会话和结构化 provider 配置错误恢复 UX 已补齐以下确定性覆盖: - `providerSecrets.test.ts` 覆盖 SecretStorage 多 key store 解析、malformed entry/重复 id/空 key 边界、写入前显式校验、active key 选择、masked key 展示、所有 stored key redaction、process env fallback、missing status、child env 覆盖值、model 配置优先级、`DEEPSEEK_MODEL` 注入和 redaction source。 -- `providerConfigurationUx.test.ts` 覆盖缺少 `DEEPSEEK_API_KEY` 的临时字符串匹配入口,确认不会误判其他 provider 或普通 DeepSeek 请求错误;该逻辑后续由 P5-13 结构化错误码替换。 +- `providerConfigurationUx.test.ts` 覆盖 RPC error data、run failed payload 和 Chat Participant metadata 中的结构化 `recoverableAction` 解析,确认缺少 API key 的恢复入口不依赖后端英文错误消息。 - `notifier.test.ts` 覆盖 Output Channel/toast message 统一脱敏 SecretStorage/env key。 - `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、trash 按钮删除非 active key、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 - `rpcServer.test.ts` 覆盖 RPC child env 注入、key 轮换后重启使用新 env,以及 typed `agent.deleteRun` request;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 -- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除。 +- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 +P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、通过原生确认完成 run 删除以及 Run List 刷新。 + ## 新增测试的协作要求 - PR 或提交说明中标明测试类型:unit、integration、regression、live、demo 或 stress。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 80b3f27..f2321f3 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除和折叠事件 UX 均已实现;P5-13 结构化 provider 配置错误码与恢复动作仍未完成。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 持续 UX 测试与体验改进占位仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -149,7 +149,8 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 10. P5-10:Generate PR Description,已完成:根据 upstream/main/master/用户选择的 base、diff/stat 和 commit summary 生成 PR title/body markdown,用带标题的 untitled markdown 预览承载结果,不自动创建 PR。 11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX,已完成:Sidebar resume 后继续同一 run 发送多轮 turn,Run List 可通过 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder`。 -13. P5-13:结构化 provider 配置错误码与恢复动作,未完成:将缺少 API key 等 provider 配置失败从前端字符串匹配升级为 RPC 结构化错误码,Sidebar/原生 Chat 和后续 TUI 依据错误码展示配置入口。 +13. P5-13:结构化 provider 配置错误码与恢复动作,已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`,Sidebar/原生 Chat Participant 依据 recoverable action 展示或自动触发 Configure API Key,不再解析后端英文错误消息;run failed payload 也支持同一恢复动作。 +14. P5-14:持续 UX 测试与体验改进占位,未完成:根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From b83f449bd3c01f2540f9614f80da7cef86a509fc Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:40 +0800 Subject: [PATCH 44/84] Adds structured provider configuration error types to protocol package --- packages/protocol/src/index.ts | 16 ++++++++++++++++ packages/protocol/test/index.test.ts | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 7770bf7..ab3d6e7 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -40,6 +40,22 @@ export const rpcErrorCodes = { internalInvariant: -32060, } as const; +export const rpcRecoverableActionKinds = ["configureDeepSeekApiKey"] as const; +export type RpcRecoverableActionKind = (typeof rpcRecoverableActionKinds)[number]; + +export interface RpcRecoverableAction { + readonly kind: RpcRecoverableActionKind; + readonly label: string; +} + +export type ProviderConfigurationErrorCode = "missingApiKey"; + +export interface ProviderConfigurationErrorData { + readonly provider: "deepseek"; + readonly configurationError: ProviderConfigurationErrorCode; + readonly recoverableAction: RpcRecoverableAction; +} + export const protocolErrorDefinitions = [ { code: jsonRpcErrorCodes.parseError, name: "Parse error" }, { code: jsonRpcErrorCodes.invalidRequest, name: "Invalid Request" }, diff --git a/packages/protocol/test/index.test.ts b/packages/protocol/test/index.test.ts index fd41d00..ca62755 100644 --- a/packages/protocol/test/index.test.ts +++ b/packages/protocol/test/index.test.ts @@ -26,12 +26,14 @@ import { type FimPreviewResult, type ListRunsParams, type ListRunsResult, + type ProviderConfigurationErrorData, type ProviderCapabilities, type ProviderCompletedPayload, type ProviderRequestedPayload, type RunCompletedPayload, type RunLogPayloadMetadata, type RunSummary, + type RpcRecoverableAction, type RejectParams, type RejectResult, type ServerCapabilities, @@ -50,6 +52,7 @@ import { riskLevels, toolDefinitions, toolNames, + rpcRecoverableActionKinds, } from "../src/index.js"; interface ToolRegistryFixture { @@ -195,6 +198,21 @@ test("protocol error code registry matches protocol document", () => { } }); +test("provider configuration errors expose recoverable actions", () => { + const action = { + kind: "configureDeepSeekApiKey", + label: "Configure API Key", + } satisfies RpcRecoverableAction; + const data = { + provider: "deepseek", + configurationError: "missingApiKey", + recoverableAction: action, + } satisfies ProviderConfigurationErrorData; + + assert.deepEqual(rpcRecoverableActionKinds, ["configureDeepSeekApiKey"]); + assert.equal(data.recoverableAction.kind, "configureDeepSeekApiKey"); +}); + test("approval request and decision params use stable protocol fields", () => { const request = { approvalId: "approval_1", From 296d6e6a87b180d70be87138184065d0c684175c Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:41 +0800 Subject: [PATCH 45/84] Documents structured provider configuration error protocol in JSON-RPC spec --- docs/json-rpc-protocol.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/json-rpc-protocol.md b/docs/json-rpc-protocol.md index dd483fd..638b904 100644 --- a/docs/json-rpc-protocol.md +++ b/docs/json-rpc-protocol.md @@ -929,6 +929,34 @@ JSON-RPC 标准错误保留标准语义。项目特定错误使用 `-32000` 到 } ``` +Provider 配置错误可以在 `error.data` 中携带恢复动作。VS Code、TUI 等前端必须优先依据结构化字段决定 UX,不解析 `message` 文案: + +```ts +interface RpcRecoverableAction { + kind: "configureDeepSeekApiKey"; + label: string; +} + +interface ProviderConfigurationErrorData { + provider: "deepseek"; + configurationError: "missingApiKey"; + recoverableAction: RpcRecoverableAction; +} +``` + +例如 DeepSeek API key 缺失时,`agent.sendTurn` / `agent.previewFim` 会返回 `E_PROVIDER_ERROR`,并附带: + +```json +{ + "provider": "deepseek", + "configurationError": "missingApiKey", + "recoverableAction": { + "kind": "configureDeepSeekApiKey", + "label": "Configure API Key" + } +} +``` + ## 端到端示例 Client 发送 turn: From 27039e44b76df5c5de2cb62a412bb0c68e569adf Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:41 +0800 Subject: [PATCH 46/84] Implements structured provider configuration error handling in CLI --- crates/cli/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 008b0a9..3726e2b 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -15,8 +15,8 @@ use prole_coder_agent_core::{ context::ContextBuildError, provider::deepseek_api::{ ChatCompletionStream, ChatFunctionDefinition, ChatTool, ChatToolCall, - ChatToolCallAccumulator, DeepSeekApiAdapter, DeepSeekApiConfig, DeepSeekModelId, - FimCompletionRequest, FinishReason, StreamEvent, ThinkingConfig, Usage, + ChatToolCallAccumulator, DeepSeekApiAdapter, DeepSeekApiConfig, DeepSeekApiError, + DeepSeekModelId, FimCompletionRequest, FinishReason, StreamEvent, ThinkingConfig, Usage, }, reasoning::ReasoningContentMode, run_log::{RunLog, RunLogError, RunLogStore}, @@ -49,6 +49,10 @@ use thiserror::Error; const DEFAULT_MAX_OUTPUT_TOKENS: u32 = 1_024; const DEFAULT_VERIFY_TIMEOUT_MS: u64 = 120_000; const CLI_RUN_JSON_RPC_ID: &str = "cli.run"; +const DEEPSEEK_PROVIDER_NAME: &str = "deepseek"; +const MISSING_API_KEY_CONFIGURATION_ERROR: &str = "missingApiKey"; +const CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND: &str = "configureDeepSeekApiKey"; +const CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL: &str = "Configure API Key"; pub fn run_cli(args: I, stdout: &mut W, stderr: &mut E) -> Result<(), CliError> where @@ -986,7 +990,7 @@ impl RpcTurnProviderFactory for CliRpcProviderFactory { self.max_output_tokens, self.thinking, ) - .map_err(|error| AgentRpcHandlerError::new(RPC_INTERNAL_INVARIANT, error.to_string())) + .map_err(cli_provider_rpc_error) } fn preview_fim( @@ -1015,11 +1019,33 @@ fn fixture_fim_preview_text(params: &FimPreviewParams) -> String { } } +fn cli_provider_rpc_error(error: CliError) -> AgentRpcHandlerError { + match error { + CliError::DeepSeek(error) => deepseek_configuration_rpc_error(error), + other => AgentRpcHandlerError::new(RPC_INTERNAL_INVARIANT, other.to_string()), + } +} + +fn deepseek_configuration_rpc_error(error: DeepSeekApiError) -> AgentRpcHandlerError { + let message = format!("DeepSeek provider configuration failed: {error}"); + let rpc_error = AgentRpcHandlerError::new(RPC_PROVIDER_ERROR, message); + match error { + DeepSeekApiError::MissingApiKey => rpc_error.with_data(json!({ + "provider": DEEPSEEK_PROVIDER_NAME, + "configurationError": MISSING_API_KEY_CONFIGURATION_ERROR, + "recoverableAction": { + "kind": CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND, + "label": CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, + })), + _ => rpc_error, + } +} + fn deepseek_fim_preview( params: &FimPreviewParams, ) -> Result { - let config = DeepSeekApiConfig::from_env_for_fim() - .map_err(|error| AgentRpcHandlerError::new(RPC_PROVIDER_ERROR, error.to_string()))?; + let config = DeepSeekApiConfig::from_env_for_fim().map_err(deepseek_configuration_rpc_error)?; let adapter = DeepSeekApiAdapter::new(config) .map_err(|error| AgentRpcHandlerError::new(RPC_PROVIDER_ERROR, error.to_string()))?; let model = params @@ -1539,15 +1565,15 @@ mod tests { provider::deepseek_api::{ ChatCompletionChunk, ChatCompletionChunkChoice, ChatCompletionDelta, ChatFunctionCallDelta, ChatToolCallDelta, ChatToolType, CompletionTokensDetails, - FinishReason, StreamEvent, Usage, + DeepSeekApiError, FinishReason, StreamEvent, Usage, }, run_log::{REDACTED_VALUE, RunLogStore}, test_helpers::TestWorkspace, turn_loop::TurnProviderEvent, }; use prole_coder_agent_rpc::{ - FimPreviewParams, PROTOCOL_VERSION, RPC_APPROVAL_DENIED, RPC_TOOL_EXECUTION_FAILED, - RpcTurnProviderFactory, + FimPreviewParams, PROTOCOL_VERSION, RPC_APPROVAL_DENIED, RPC_PROVIDER_ERROR, + RPC_TOOL_EXECUTION_FAILED, RpcTurnProviderFactory, }; use serde_json::{Value, json}; use std::{ @@ -1559,7 +1585,8 @@ mod tests { use super::{ CliCommand, CliRpcProviderFactory, FixtureKind, ProviderKind, RunCommand, ThinkingKind, - deepseek_chat_stream_to_turn_provider_stream, run_cli, run_cli_with_input, + deepseek_chat_stream_to_turn_provider_stream, deepseek_configuration_rpc_error, run_cli, + run_cli_with_input, }; #[test] @@ -2066,6 +2093,21 @@ mod tests { assert!(result.text.contains("prole fixture")); } + #[test] + fn deepseek_missing_api_key_rpc_error_exposes_recoverable_action() { + let error = deepseek_configuration_rpc_error(DeepSeekApiError::MissingApiKey); + + assert_eq!(error.code, RPC_PROVIDER_ERROR); + assert!(error.message.contains("DEEPSEEK_API_KEY is required")); + let data = error + .data + .expect("missing key should include structured data"); + assert_eq!(data["provider"], "deepseek"); + assert_eq!(data["configurationError"], "missingApiKey"); + assert_eq!(data["recoverableAction"]["kind"], "configureDeepSeekApiKey"); + assert_eq!(data["recoverableAction"]["label"], "Configure API Key"); + } + type InteractiveCliRpc = ( mpsc::Sender, SharedOutput, From c596808117b828d1776c7c01a7a310439fd0ee7e Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:41 +0800 Subject: [PATCH 47/84] Refactors provider configuration UX to use structured error data instead of string matching --- .../extension/src/providerConfigurationUx.ts | 95 ++++++++++++++++++- 1 file changed, 92 insertions(+), 3 deletions(-) diff --git a/vscode/extension/src/providerConfigurationUx.ts b/vscode/extension/src/providerConfigurationUx.ts index 42a3ff5..340b3c0 100644 --- a/vscode/extension/src/providerConfigurationUx.ts +++ b/vscode/extension/src/providerConfigurationUx.ts @@ -1,6 +1,95 @@ +import type { RpcRecoverableAction } from "@prole-coder/protocol" with { + "resolution-mode": "import", +}; + +export const CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND = "configureDeepSeekApiKey"; export const CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL = "Configure API Key"; -export function isDeepSeekApiKeyRequiredMessage(message: string): boolean { - const normalized = message.toLowerCase(); - return normalized.includes("deepseek") && normalized.includes("deepseek_api_key") && normalized.includes("required"); +export interface ConfigureDeepSeekApiKeyAction { + readonly type: typeof CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND; + readonly label: string; +} + +export type ProviderConfigurationAction = ConfigureDeepSeekApiKeyAction; + +export function providerConfigurationActionFromError( + error: unknown, +): ProviderConfigurationAction | undefined { + const data = isRecord(error) ? error["data"] : undefined; + return providerConfigurationActionFromData(data); +} + +export function providerConfigurationActionFromPayload( + payload: unknown, +): ProviderConfigurationAction | undefined { + return providerConfigurationActionFromData(payload); +} + +export function providerConfigurationActionFromMetadata( + metadata: unknown, +): ProviderConfigurationAction | undefined { + const action = isRecord(metadata) ? metadata["providerConfigurationAction"] : undefined; + return actionFromProviderConfigurationAction(action) ?? + providerConfigurationActionFromData({ recoverableAction: action }); +} + +export function providerConfigurationActionFromData( + data: unknown, +): ProviderConfigurationAction | undefined { + const record = isRecord(data) ? data : undefined; + return actionFromRecoverableAction(record?.["recoverableAction"]); +} + +export function isConfigureDeepSeekApiKeyAction( + action: ProviderConfigurationAction | undefined, +): action is ConfigureDeepSeekApiKeyAction { + return action?.type === CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND; +} + +function actionFromRecoverableAction( + action: unknown, +): ProviderConfigurationAction | undefined { + if (!isRecoverableAction(action)) { + return undefined; + } + + if (action.kind === CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND) { + return { + type: CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND, + label: action.label.length > 0 ? action.label : CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }; + } + + return undefined; +} + +function actionFromProviderConfigurationAction( + action: unknown, +): ProviderConfigurationAction | undefined { + if ( + isRecord(action) && + action["type"] === CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND && + typeof action["label"] === "string" + ) { + return { + type: CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND, + label: action["label"].length > 0 + ? action["label"] + : CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }; + } + + return undefined; +} + +function isRecoverableAction(value: unknown): value is RpcRecoverableAction { + return ( + isRecord(value) && + value["kind"] === CONFIGURE_DEEPSEEK_API_KEY_ACTION_KIND && + typeof value["label"] === "string" + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; } From b3df5ba649574e933cc4d4c7f850e6645aaa89be Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:41 +0800 Subject: [PATCH 48/84] Integrates structured provider configuration recovery into VS Code chat participants --- vscode/extension/src/chatParticipant.ts | 8 +++-- vscode/extension/src/chatParticipantCore.ts | 18 +++++++++-- vscode/extension/src/chatView.ts | 35 +++++++++++++++------ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/vscode/extension/src/chatParticipant.ts b/vscode/extension/src/chatParticipant.ts index 2db4d7b..33ff7cc 100644 --- a/vscode/extension/src/chatParticipant.ts +++ b/vscode/extension/src/chatParticipant.ts @@ -16,7 +16,10 @@ import { } from "./chatParticipantCore"; import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; -import { isDeepSeekApiKeyRequiredMessage } from "./providerConfigurationUx"; +import { + isConfigureDeepSeekApiKeyAction, + providerConfigurationActionFromMetadata, +} from "./providerConfigurationUx"; import { CONFIGURE_DEEPSEEK_API_KEY_COMMAND } from "./providerSecretCommands"; import type { MessageRedactor } from "./redaction"; @@ -47,7 +50,8 @@ export function registerProleChatParticipant( ...(redactor === undefined ? {} : { redactor }), token, }); - if (isDeepSeekApiKeyRequiredMessage(result.errorDetails?.message ?? "")) { + const action = providerConfigurationActionFromMetadata(result.metadata); + if (isConfigureDeepSeekApiKeyAction(action)) { await vscode.commands.executeCommand(CONFIGURE_DEEPSEEK_API_KEY_COMMAND); } return result; diff --git a/vscode/extension/src/chatParticipantCore.ts b/vscode/extension/src/chatParticipantCore.ts index 0b0b369..d315a7c 100644 --- a/vscode/extension/src/chatParticipantCore.ts +++ b/vscode/extension/src/chatParticipantCore.ts @@ -14,6 +14,11 @@ import { } from "./automaticContext"; import { DEFAULT_CHAT_MODE, sendTurnParams } from "./chatInput"; import type { ProleLogger } from "./logging"; +import { + providerConfigurationActionFromError, + providerConfigurationActionFromPayload, + type ProviderConfigurationAction, +} from "./providerConfigurationUx"; import type { MessageRedactor } from "./redaction"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; @@ -159,8 +164,9 @@ export async function runChatParticipantTurn( `ProleCoder turn failed: ${errorMessage(error)}`, options.redactor, ); + const action = providerConfigurationActionFromError(error); options.logger?.error(message); - return errorResult(message, runId); + return errorResult(message, runId, action); } finally { eventSubscription.dispose(); cancellationSubscription?.dispose(); @@ -220,8 +226,9 @@ function handleParticipantEvent( return; case "run.failed": { const message = redactMessage(terminalMessage(payload, "Run failed."), redactor); + const action = providerConfigurationActionFromPayload(payload); response.markdown(`\n\n${message}`); - finish(errorResult(message, event.runId)); + finish(errorResult(message, event.runId, action)); return; } case "run.canceled": { @@ -271,12 +278,17 @@ function terminalMessage(payload: Record | undefined, fallback: ); } -function errorResult(message: string, runId?: string): ChatParticipantResult { +function errorResult( + message: string, + runId?: string, + action?: ProviderConfigurationAction, +): ChatParticipantResult { return { errorDetails: { message }, metadata: { status: "failed", ...(runId === undefined ? {} : { runId }), + ...(action === undefined ? {} : { providerConfigurationAction: action }), }, }; } diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index d045589..cc95a3c 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -30,8 +30,10 @@ import { import { diagnosticAttachmentsFromProblems } from "./diagnostics"; import type { ProleLogger } from "./logging"; import { - CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, - isDeepSeekApiKeyRequiredMessage, + isConfigureDeepSeekApiKeyAction, + providerConfigurationActionFromError, + providerConfigurationActionFromPayload, + type ProviderConfigurationAction, } from "./providerConfigurationUx"; import { CONFIGURE_DEEPSEEK_API_KEY_COMMAND, @@ -41,6 +43,7 @@ import type { MessageRedactor } from "./redaction"; import type { AgentEventEnvelope, DisposableLike } from "./rpcServer"; import { RUN_LIST_LIMIT, + deletedRunList, failedRunList, idleRunList, isRefreshRunsMessage, @@ -103,7 +106,7 @@ type ChatSubmissionStatus = "idle" | "sending" | "running" | "completed" | "fail type TerminalSubmissionStatus = Extract; export interface ChatSubmissionAction { - readonly type: "configureDeepSeekApiKey"; + readonly type: ProviderConfigurationAction["type"]; readonly label: string; } @@ -122,6 +125,7 @@ interface TerminalRunState { readonly status: TerminalSubmissionStatus; readonly message: string; readonly error?: string; + readonly action?: ChatSubmissionAction; } export interface ChatViewTestState { @@ -327,16 +331,18 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } catch (error) { const messageText = `Failed to send turn: ${errorMessage(error)}`; const redacted = this.redact(messageText); - const missingApiKey = isDeepSeekApiKeyRequiredMessage(messageText); + const action = chatSubmissionActionFromProviderAction( + providerConfigurationActionFromError(error), + ); this.logger?.error(redacted); this.setSubmission({ ...idleSubmission(), status: "failed", message: redacted, error: redacted, - ...(missingApiKey ? { action: configureDeepSeekApiKeyAction() } : {}), + ...(action === undefined ? {} : { action }), }); - if (missingApiKey) { + if (isConfigureDeepSeekApiKeyAction(action)) { await this.configureDeepSeekApiKey(); } } @@ -1765,10 +1771,16 @@ function idleSubmission(): ChatSubmissionSnapshot { }; } -function configureDeepSeekApiKeyAction(): ChatSubmissionAction { +function chatSubmissionActionFromProviderAction( + action: ProviderConfigurationAction | undefined, +): ChatSubmissionAction | undefined { + if (action === undefined) { + return undefined; + } + return { - type: "configureDeepSeekApiKey", - label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + type: action.type, + label: action.label, }; } @@ -1782,6 +1794,7 @@ function terminalSubmission( status: terminal.status, message: terminal.message, ...(terminal.error === undefined ? {} : { error: terminal.error }), + ...(terminal.action === undefined ? {} : { action: terminal.action }), runId, ...(turnId === undefined ? {} : { turnId }), }; @@ -1797,10 +1810,14 @@ function terminalRunState(event: AgentEventEnvelope): TerminalRunState | undefin if (event.type === "run.failed") { const message = terminalMessage(event, "Run failed."); + const action = chatSubmissionActionFromProviderAction( + providerConfigurationActionFromPayload(event.payload), + ); return { status: "failed", message, error: message, + ...(action === undefined ? {} : { action }), }; } From b3b7b99ffa410a2d7e1295e17978067df57a4b27 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:41 +0800 Subject: [PATCH 49/84] Adds native confirmation dialog for run deletion in Sidebar Chat --- vscode/extension/src/chatView.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index cc95a3c..d3d816b 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -461,10 +461,18 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } + const confirmation = await vscode.window.showWarningMessage( + `Delete run ${runId}?`, + { modal: true }, + "Delete", + ); + if (confirmation !== "Delete") { + return; + } + this.setRunList(loadingRunList(this.runList, "Deleting run...")); try { const result = await this.rpcClient.deleteRun({ runId }); - const runs = this.runList.runs.filter((run) => run.runId !== result.runId); if (this.activeConversationRunId === result.runId) { this.activeConversationRunId = undefined; this.timeline.clear(); @@ -473,7 +481,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.setContextViz(emptyContextViz()); this.postSnapshot(); } - this.setRunList(readyRunList({ runs }, undefined, `Deleted ${result.runId}.`)); + this.setRunList(deletedRunList(this.runList, result.runId, `Deleted ${result.runId}.`)); void this.refreshRuns("Refreshing runs..."); } catch (error) { const messageText = `Failed to delete run: ${errorMessage(error)}`; @@ -1398,8 +1406,9 @@ function renderChatViewHtml( deleteButton.title = "Delete run"; deleteButton.setAttribute("aria-label", "Delete run"); deleteButton.textContent = "x"; - deleteButton.addEventListener("click", () => { - if (typeof run.runId === "string" && run.runId.length > 0 && confirm("Delete this run?")) { + deleteButton.addEventListener("click", (event) => { + event.stopPropagation(); + if (typeof run.runId === "string" && run.runId.length > 0) { vscodeApi.postMessage({ type: "deleteRun", runId: run.runId }); } }); From 40ab7f54cb12037b2fa7c52b0e2db451beb4f611 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:42 +0800 Subject: [PATCH 50/84] Adds deletedRunList helper for proper run list state management after deletion --- vscode/extension/src/runHistory.ts | 10 ++++++++++ vscode/extension/test/runHistory.test.ts | 20 ++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/vscode/extension/src/runHistory.ts b/vscode/extension/src/runHistory.ts index 8ccd098..6200b41 100644 --- a/vscode/extension/src/runHistory.ts +++ b/vscode/extension/src/runHistory.ts @@ -63,6 +63,16 @@ export function readyRunList( }; } +export function deletedRunList( + previous: RunListSnapshot, + deletedRunId: string, + message?: string, +): RunListSnapshot { + const runs = previous.runs.filter((run) => run.runId !== deletedRunId); + const selectedRunId = previous.selectedRunId === deletedRunId ? undefined : previous.selectedRunId; + return readyRunList({ runs }, selectedRunId, message); +} + export function failedRunList( message: string, previous: RunListSnapshot = idleRunList(), diff --git a/vscode/extension/test/runHistory.test.ts b/vscode/extension/test/runHistory.test.ts index bff8572..9f41179 100644 --- a/vscode/extension/test/runHistory.test.ts +++ b/vscode/extension/test/runHistory.test.ts @@ -3,6 +3,7 @@ import test from "node:test"; import { RUN_LIST_LIMIT, + deletedRunList, failedRunList, idleRunList, isRefreshRunsMessage, @@ -41,6 +42,25 @@ test("run history ready snapshots only keep a selected run that still exists", ( }); }); +test("run history delete snapshots remove runs and keep valid selections", () => { + const ready = readyRunList( + { runs: [runSummary("run_1"), runSummary("run_2"), runSummary("run_3")] }, + "run_2", + ); + + assert.deepEqual(deletedRunList(ready, "run_1", "Deleted run_1."), { + status: "ready", + runs: [runSummary("run_2"), runSummary("run_3")], + selectedRunId: "run_2", + message: "Deleted run_1.", + }); + assert.deepEqual(deletedRunList(ready, "run_2", "Deleted run_2."), { + status: "ready", + runs: [runSummary("run_1"), runSummary("run_3")], + message: "Deleted run_2.", + }); +}); + test("run history leaves result limiting to the RPC server", () => { const runs = Array.from({ length: RUN_LIST_LIMIT + 1 }, (_, index) => runSummary(`run_${index}`)); From 712d2889d979356a7338f8965276d34474532388 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:42 +0800 Subject: [PATCH 51/84] Adds tests for structured provider configuration recovery in chat participant core --- .../test/chatParticipantCore.test.ts | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/vscode/extension/test/chatParticipantCore.test.ts b/vscode/extension/test/chatParticipantCore.test.ts index 24550b6..1b06b43 100644 --- a/vscode/extension/test/chatParticipantCore.test.ts +++ b/vscode/extension/test/chatParticipantCore.test.ts @@ -113,9 +113,39 @@ test("runChatParticipantTurn handles terminal events buffered before sendTurn re }); }); +test("runChatParticipantTurn preserves run failed recovery actions", async () => { + const rpc = new FailedRecoveryChatParticipantRpcClient(); + const response = new FakeChatResponseStream(); + const result = await withTimeout( + runChatParticipantTurn({ + rpcClient: rpc, + request: { + prompt: "hello", + }, + response, + }), + ); + + assert.equal(response.markdownParts.join(""), "\n\nMissing DeepSeek API key."); + assert.deepEqual(result.metadata?.["providerConfigurationAction"], { + type: "configureDeepSeekApiKey", + label: "Configure API Key", + }); +}); + test("runChatParticipantTurn logs RPC failures before returning chat errors", async () => { const rpc = new RejectingChatParticipantRpcClient( - new Error("DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required"), + new StructuredRpcRequestError( + "DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required", + { + provider: "deepseek", + configurationError: "missingApiKey", + recoverableAction: { + kind: "configureDeepSeekApiKey", + label: "Configure API Key", + }, + }, + ), ); const response = new FakeChatResponseStream(); const logger = new FakeLogger(); @@ -129,6 +159,10 @@ test("runChatParticipantTurn logs RPC failures before returning chat errors", as }); assert.ok(result.errorDetails?.message.includes("DEEPSEEK_API_KEY is required")); + assert.deepEqual(result.metadata?.["providerConfigurationAction"], { + type: "configureDeepSeekApiKey", + label: "Configure API Key", + }); assert.deepEqual(logger.errors, [result.errorDetails?.message]); }); @@ -199,6 +233,27 @@ class EarlyTerminalChatParticipantRpcClient extends FakeChatParticipantRpcClient } } +class FailedRecoveryChatParticipantRpcClient extends FakeChatParticipantRpcClient { + override async sendTurn(params: SendTurnParams): Promise { + this.sendTurns.push(params); + queueMicrotask(() => { + this.emit(agentEvent(1, "run.failed", { + code: "E_PROVIDER_ERROR", + message: "Missing DeepSeek API key.", + recoverableAction: { + kind: "configureDeepSeekApiKey", + label: "Configure API Key", + }, + })); + }); + return { + runId: "run_chat_1", + turnId: "turn_chat_1", + accepted: true as const, + }; + } +} + class RejectingChatParticipantRpcClient extends FakeChatParticipantRpcClient { constructor(private readonly error: Error) { super(); @@ -210,6 +265,13 @@ class RejectingChatParticipantRpcClient extends FakeChatParticipantRpcClient { } } +class StructuredRpcRequestError extends Error { + constructor(message: string, readonly data: unknown) { + super(message); + this.name = "RpcRequestError"; + } +} + class FakeChatResponseStream implements ChatParticipantResponseStream { readonly markdownParts: string[] = []; readonly progressParts: string[] = []; From 6665ca13975c9fad12b1bf7286d1da2dfff22f01 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 07:40:42 +0800 Subject: [PATCH 52/84] Replaces string-matching tests with structured data tests for provider configuration UX --- .../test/providerConfigurationUx.test.ts | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/vscode/extension/test/providerConfigurationUx.test.ts b/vscode/extension/test/providerConfigurationUx.test.ts index a9a7f68..347709b 100644 --- a/vscode/extension/test/providerConfigurationUx.test.ts +++ b/vscode/extension/test/providerConfigurationUx.test.ts @@ -1,15 +1,64 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { isDeepSeekApiKeyRequiredMessage } from "../src/providerConfigurationUx.js"; +import { + CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + providerConfigurationActionFromError, + providerConfigurationActionFromMetadata, + providerConfigurationActionFromPayload, +} from "../src/providerConfigurationUx.js"; -test("isDeepSeekApiKeyRequiredMessage detects missing DeepSeek API key errors", () => { +test("providerConfigurationActionFromError reads structured RPC recovery data", () => { + const error = { + data: { + provider: "deepseek", + configurationError: "missingApiKey", + recoverableAction: { + kind: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, + }, + }; + + assert.deepEqual(providerConfigurationActionFromError(error), { + type: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }); assert.equal( - isDeepSeekApiKeyRequiredMessage( - "DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required", + providerConfigurationActionFromError( + new Error("DeepSeek provider configuration failed: DEEPSEEK_API_KEY is required"), ), - true, + undefined, + ); +}); + +test("providerConfigurationActionFromPayload reads run failed recovery data", () => { + assert.deepEqual( + providerConfigurationActionFromPayload({ + code: "E_PROVIDER_ERROR", + recoverableAction: { + kind: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, + }), + { + type: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, + ); +}); + +test("providerConfigurationActionFromMetadata reads chat participant actions", () => { + assert.deepEqual( + providerConfigurationActionFromMetadata({ + providerConfigurationAction: { + type: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, + }), + { + type: "configureDeepSeekApiKey", + label: CONFIGURE_DEEPSEEK_API_KEY_ACTION_LABEL, + }, ); - assert.equal(isDeepSeekApiKeyRequiredMessage("OpenAI API key is required"), false); - assert.equal(isDeepSeekApiKeyRequiredMessage("DeepSeek request failed with status 401"), false); }); From 9faea274170758b1aed28c4a1686a39c59dcd3b4 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 20:41:37 +0800 Subject: [PATCH 53/84] Adds Sidebar inline approval cards and dedicated conversation view --- vscode/extension/src/chatApprovals.ts | 126 ++++ vscode/extension/src/chatView.ts | 655 +++++++++++++++++- vscode/extension/src/extension.ts | 7 +- vscode/extension/src/webviewSerialization.ts | 4 + vscode/extension/test/chatApprovals.test.ts | 177 +++++ vscode/extension/test/electron/index.ts | 4 + .../test/webviewSerialization.test.ts | 17 + 7 files changed, 962 insertions(+), 28 deletions(-) create mode 100644 vscode/extension/src/chatApprovals.ts create mode 100644 vscode/extension/src/webviewSerialization.ts create mode 100644 vscode/extension/test/chatApprovals.test.ts create mode 100644 vscode/extension/test/webviewSerialization.test.ts diff --git a/vscode/extension/src/chatApprovals.ts b/vscode/extension/src/chatApprovals.ts new file mode 100644 index 0000000..78d4e4c --- /dev/null +++ b/vscode/extension/src/chatApprovals.ts @@ -0,0 +1,126 @@ +import { + APPROVAL_REJECTED_REASON, + type ApprovalPromptDecision, + type ApprovalPromptHunk, + type ApprovalPromptRequest, +} from "./commands"; + +export interface ChatApprovalSnapshot { + readonly approvalId: string; + readonly toolCallId: string; + readonly toolName: string; + readonly risk: string; + readonly title: string; + readonly detail: string; + readonly persistable: boolean; + readonly command?: string; + readonly cwd?: string; + readonly outputSummary?: string; + readonly paths?: readonly string[]; + readonly riskReasons?: readonly string[]; + readonly hunks?: readonly ChatApprovalHunkSnapshot[]; +} + +export interface ChatApprovalHunkSnapshot extends ApprovalPromptHunk {} + +export function chatApprovalSnapshotFromRequest( + request: ApprovalPromptRequest, +): ChatApprovalSnapshot { + return { + approvalId: request.approvalId, + toolCallId: request.toolCallId, + toolName: request.toolName, + risk: request.risk, + title: request.title, + detail: request.detail, + persistable: request.persistable, + ...(request.command === undefined ? {} : { command: request.command }), + ...(request.cwd === undefined ? {} : { cwd: request.cwd }), + ...(request.outputSummary === undefined ? {} : { outputSummary: request.outputSummary }), + ...(request.paths === undefined ? {} : { paths: request.paths }), + ...(request.riskReasons === undefined ? {} : { riskReasons: request.riskReasons }), + ...(request.hunks === undefined + ? {} + : { + hunks: request.hunks.map((hunk) => ({ ...hunk })), + }), + }; +} + +export function approvalDecisionFromWebviewMessage( + message: unknown, + request: ApprovalPromptRequest, +): ApprovalPromptDecision | undefined { + if (!isRecord(message) || message["type"] !== "approvalDecision") { + return undefined; + } + + if (message["approvalId"] !== request.approvalId) { + return undefined; + } + + if (message["decision"] === "reject") { + return { + kind: "reject", + approvalId: request.approvalId, + reason: APPROVAL_REJECTED_REASON, + }; + } + + if (message["decision"] !== "approve") { + return undefined; + } + + const approvedHunks = approvedHunkIdsFromMessage(message["approvedHunks"], request); + if (approvedHunks !== undefined && approvedHunks.length === 0) { + return { + kind: "reject", + approvalId: request.approvalId, + reason: "no patch hunks selected in VS Code", + }; + } + + const allHunkCount = request.hunks?.length ?? 0; + const partialHunks = + approvedHunks !== undefined && approvedHunks.length > 0 && approvedHunks.length < allHunkCount + ? { + hunks: { + approved: approvedHunks, + }, + } + : {}; + + return { + kind: "approve", + approvalId: request.approvalId, + persist: "never", + ...partialHunks, + }; +} + +function approvedHunkIdsFromMessage( + value: unknown, + request: ApprovalPromptRequest, +): readonly string[] | undefined { + if (!Array.isArray(value) || request.hunks === undefined || request.hunks.length === 0) { + return undefined; + } + + const knownHunks = new Set(request.hunks.map((hunk) => hunk.id)); + const seen = new Set(); + const selected: string[] = []; + for (const entry of value) { + if (typeof entry !== "string" || !knownHunks.has(entry) || seen.has(entry)) { + continue; + } + + seen.add(entry); + selected.push(entry); + } + + return selected; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index d3d816b..ff201f8 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -20,8 +20,14 @@ import { automaticContextAttachmentFromTimeline, mergeTurnAttachments, } from "./automaticContext"; +import { + approvalDecisionFromWebviewMessage, + chatApprovalSnapshotFromRequest, + type ChatApprovalSnapshot, +} from "./chatApprovals"; import { CHAT_RUN_MODES, DEFAULT_CHAT_MODE, parseChatTurnSubmission, sendTurnParams } from "./chatInput"; import { ChatEventTimeline, type ChatTimelineSnapshot } from "./chatEvents"; +import type { ApprovalPromptDecision, ApprovalPromptRequest } from "./commands"; import { contextVizFromEvent, emptyContextViz, @@ -53,6 +59,7 @@ import { resumeRunIdFromMessage, type RunListSnapshot, } from "./runHistory"; +import { safeScriptJson } from "./webviewSerialization"; export const CHAT_VIEW_ID = "prole-coder.chat"; @@ -96,11 +103,17 @@ interface ContextWebviewMessage { readonly context: ContextVizSnapshot; } +interface ApprovalWebviewMessage { + readonly type: "approval"; + readonly approval?: ChatApprovalSnapshot; +} + type ExtensionToWebviewMessage = | SnapshotWebviewMessage | SubmissionWebviewMessage | RunsWebviewMessage - | ContextWebviewMessage; + | ContextWebviewMessage + | ApprovalWebviewMessage; type ChatSubmissionStatus = "idle" | "sending" | "running" | "completed" | "failed" | "canceled"; type TerminalSubmissionStatus = Extract; @@ -133,6 +146,12 @@ export interface ChatViewTestState { readonly submission: ChatSubmissionSnapshot; readonly runs: RunListSnapshot; readonly context: ContextVizSnapshot; + readonly approval?: ChatApprovalSnapshot; +} + +interface PendingApprovalState { + readonly request: ApprovalPromptRequest; + resolve(decision: ApprovalPromptDecision): void; } export class ProleChatViewProvider implements vscode.WebviewViewProvider, DisposableLike { @@ -143,6 +162,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private runList: RunListSnapshot = idleRunList(); private contextViz: ContextVizSnapshot = emptyContextViz(); private activeConversationRunId: string | undefined; + private pendingApproval: PendingApprovalState | undefined; private rpcSubscription: DisposableLike | undefined; private viewMessageSubscription: DisposableLike | undefined; private view: vscode.WebviewView | undefined; @@ -163,6 +183,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.contextViz = contextViz; } const terminal = this.updateSubmissionForEvent(event); + if (terminal) { + this.rejectPendingApproval("run ended before approval was resolved"); + } this.postSnapshot(); if (contextViz !== undefined) { this.postContext(); @@ -190,11 +213,13 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.submission, this.runList, this.contextViz, + this.pendingApprovalSnapshot(), ); this.postSnapshot(); this.postSubmission(); this.postRuns(); this.postContext(); + this.postApproval(); void this.refreshRuns(); } @@ -207,11 +232,13 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } testState(): ChatViewTestState { + const approval = this.pendingApprovalSnapshot(); return { timeline: this.timeline.snapshot(), submission: this.submission, runs: this.runList, context: this.contextViz, + ...(approval === undefined ? {} : { approval }), }; } @@ -220,18 +247,46 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } dispose(): void { + this.rejectPendingApproval("approval view disposed"); this.rpcSubscription?.dispose(); this.viewMessageSubscription?.dispose(); this.rpcSubscription = undefined; this.viewMessageSubscription = undefined; } + requestApproval(request: ApprovalPromptRequest): Promise { + this.rejectPendingApproval("superseded by a newer approval request"); + void this.openChatView(); + + return new Promise((resolve) => { + this.pendingApproval = { + request, + resolve, + }; + this.postApproval(); + }); + } + private async handleWebviewMessage(message: unknown): Promise { if (isRefreshRunsMessage(message)) { await this.refreshRuns(); return; } + if (isShowRunsMessage(message)) { + this.showRuns(); + return; + } + + const approvalDecision = + this.pendingApproval === undefined + ? undefined + : approvalDecisionFromWebviewMessage(message, this.pendingApproval.request); + if (approvalDecision !== undefined) { + this.resolvePendingApproval(approvalDecision); + return; + } + const resumeRunId = resumeRunIdFromMessage(message); if (resumeRunId !== undefined) { await this.resumeRun(resumeRunId); @@ -380,6 +435,15 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos void this.view?.webview.postMessage(message); } + private postApproval(): void { + const approval = this.pendingApprovalSnapshot(); + const message: ExtensionToWebviewMessage = { + type: "approval", + ...(approval === undefined ? {} : { approval }), + }; + void this.view?.webview.postMessage(message); + } + private setSubmission(submission: ChatSubmissionSnapshot): void { this.submission = submission; this.postSubmission(); @@ -395,6 +459,12 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.postContext(); } + private pendingApprovalSnapshot(): ChatApprovalSnapshot | undefined { + return this.pendingApproval === undefined + ? undefined + : chatApprovalSnapshotFromRequest(this.pendingApproval.request); + } + private async refreshRuns(message = "Loading runs..."): Promise { if (this.rpcClient === undefined) { this.setRunList( @@ -461,15 +531,6 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } - const confirmation = await vscode.window.showWarningMessage( - `Delete run ${runId}?`, - { modal: true }, - "Delete", - ); - if (confirmation !== "Delete") { - return; - } - this.setRunList(loadingRunList(this.runList, "Deleting run...")); try { const result = await this.rpcClient.deleteRun({ runId }); @@ -491,6 +552,23 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } } + private showRuns(): void { + if (this.submission.busy) { + return; + } + + this.activeConversationRunId = undefined; + this.timeline.clear(); + this.setSubmission(idleSubmission()); + this.setContextViz(emptyContextViz()); + this.postSnapshot(); + this.setRunList({ + status: this.runList.status, + runs: this.runList.runs, + ...(this.runList.message === undefined ? {} : { message: this.runList.message }), + }); + } + private async cancelTurn(runId: string): Promise { if (this.rpcClient === undefined) { this.setSubmission({ @@ -580,6 +658,35 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } } + private resolvePendingApproval(decision: ApprovalPromptDecision): void { + if ( + this.pendingApproval === undefined || + this.pendingApproval.request.approvalId !== decision.approvalId + ) { + return; + } + + const pending = this.pendingApproval; + this.pendingApproval = undefined; + this.postApproval(); + pending.resolve(decision); + } + + private rejectPendingApproval(reason: string): void { + if (this.pendingApproval === undefined) { + return; + } + + const pending = this.pendingApproval; + this.pendingApproval = undefined; + this.postApproval(); + pending.resolve({ + kind: "reject", + approvalId: pending.request.approvalId, + reason, + }); + } + private redact(message: string): string { return this.redactor?.redact(message) ?? message; } @@ -599,12 +706,14 @@ function renderChatViewHtml( submission: ChatSubmissionSnapshot, runList: RunListSnapshot, contextViz: ContextVizSnapshot, + approval: ChatApprovalSnapshot | undefined, ): string { const nonce = nonceValue(); const initialSnapshot = safeScriptJson(snapshot); const initialSubmission = safeScriptJson(submission); const initialRuns = safeScriptJson(runList); const initialContext = safeScriptJson(contextViz); + const initialApproval = safeScriptJson(approval); return ` @@ -630,6 +739,53 @@ function renderChatViewHtml( flex-direction: column; } + .shell.conversation-active .status, + .shell.conversation-active .runs, + .shell.conversation-active .context-viz { + display: none; + } + + .conversation-bar { + display: none; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border); + background: var(--vscode-sideBarSectionHeader-background); + } + + .shell.conversation-active .conversation-bar { + display: flex; + } + + .conversation-back { + width: 26px; + height: 26px; + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + font: var(--vscode-font-size) var(--vscode-font-family); + } + + .conversation-back:hover:enabled { + background: var(--vscode-button-secondaryHoverBackground); + } + + .conversation-back:disabled { + opacity: 0.55; + cursor: default; + } + + .conversation-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; + } + .status { display: grid; gap: 2px; @@ -751,6 +907,50 @@ function renderChatViewHtml( opacity: 0.55; } + .run-delete-confirm { + grid-column: 1 / -1; + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 6px; + padding: 6px 7px; + color: var(--vscode-descriptionForeground); + background: var(--vscode-input-background); + border: 1px solid var(--vscode-editorWidget-border); + font-size: 12px; + } + + .run-delete-confirm span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .run-delete-confirm button { + min-width: 52px; + height: 24px; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + font: var(--vscode-font-size) var(--vscode-font-family); + } + + .run-delete-confirm .confirm-delete { + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + } + + .run-delete-confirm .confirm-cancel { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + } + + .run-delete-confirm .confirm-delete:hover, + .run-delete-confirm .confirm-cancel:hover { + filter: brightness(1.08); + } + .run-entry:hover:enabled, .run-entry.selected { background: var(--vscode-list-hoverBackground); @@ -1037,6 +1237,117 @@ function renderChatViewHtml( overflow-wrap: anywhere; } + details.item:not([open]) > .body { + display: none; + } + + .approval-host { + display: none; + padding: 8px 10px 0; + border-top: 1px solid var(--vscode-sideBarSectionHeader-border); + background: var(--vscode-sideBar-background); + } + + .approval-host.active { + display: grid; + } + + .approval-card { + display: grid; + gap: 8px; + padding: 8px; + background: var(--vscode-editorWidget-background); + border: 1px solid var(--vscode-editorWidget-border); + border-left: 3px solid var(--vscode-editorWarning-foreground); + } + + .approval-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + .approval-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: 600; + } + + .approval-risk { + flex: 0 0 auto; + color: var(--vscode-editorWarning-foreground); + font-size: 11px; + } + + .approval-detail, + .approval-meta, + .approval-output { + color: var(--vscode-descriptionForeground); + font-size: 12px; + line-height: 1.4; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + + .approval-hunks { + display: grid; + gap: 4px; + max-height: 150px; + overflow: auto; + } + + .approval-hunk { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: start; + gap: 6px; + color: var(--vscode-foreground); + font-size: 12px; + line-height: 1.35; + } + + .approval-hunk span { + min-width: 0; + overflow-wrap: anywhere; + } + + .approval-actions { + display: flex; + justify-content: flex-end; + gap: 6px; + } + + .approval-action { + min-width: 64px; + height: 26px; + border: 1px solid transparent; + border-radius: 3px; + cursor: pointer; + font: var(--vscode-font-size) var(--vscode-font-family); + } + + .approval-action.approve { + color: var(--vscode-button-foreground); + background: var(--vscode-button-background); + } + + .approval-action.reject { + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + } + + .approval-action:hover:enabled { + filter: brightness(1.08); + } + + .approval-action:disabled { + opacity: 0.55; + cursor: default; + } + .composer { display: grid; gap: 8px; @@ -1064,9 +1375,13 @@ function renderChatViewHtml( .send:focus, .cancel:focus, .provider-action:focus, + .conversation-back:focus, .refresh-runs:focus, .run-entry:focus, .run-delete:focus, + .run-delete-confirm button:focus, + .approval-action:focus, + .approval-hunk input:focus, .context-tab:focus { outline: 1px solid var(--vscode-focusBorder); outline-offset: 1px; @@ -1189,11 +1504,15 @@ function renderChatViewHtml(
+
+ +
Conversation
+
ProleCoder
No run events yet.
-
+
Runs
@@ -1209,6 +1528,7 @@ function renderChatViewHtml(
+
@@ -1226,9 +1546,13 @@ function renderChatViewHtml( const initialSubmission = ${initialSubmission}; const initialRuns = ${initialRuns}; const initialContext = ${initialContext}; + const initialApproval = ${initialApproval}; const runModes = ${safeScriptJson(CHAT_RUN_MODES)}; const defaultMode = ${safeScriptJson(DEFAULT_CHAT_MODE)}; const vscodeApi = acquireVsCodeApi(); + const shellRoot = document.querySelector(".shell"); + const showRunsButton = document.getElementById("show-runs"); + const conversationTitleRoot = document.getElementById("conversation-title"); const eventsRoot = document.getElementById("events"); const runListRoot = document.getElementById("run-list"); const runMessageRoot = document.getElementById("run-message"); @@ -1245,7 +1569,14 @@ function renderChatViewHtml( const sendButton = document.getElementById("send"); const cancelButton = document.getElementById("cancel"); const submissionRoot = document.getElementById("submission"); + const approvalRoot = document.getElementById("approval"); + let currentSnapshot = initialSnapshot; + let currentSubmission = initialSubmission; + let currentRuns = initialRuns; let currentContext = initialContext; + let currentApproval = initialApproval; + let pendingRunDeleteId = ""; + const resolvedApprovalIds = new Set(); let contextSourceTab = "included"; for (const mode of runModes) { @@ -1270,6 +1601,13 @@ function renderChatViewHtml( if (message && message.type === "context") { renderContext(message.context); } + if (message && message.type === "approval") { + renderApproval(message.approval); + } + }); + + showRunsButton.addEventListener("click", () => { + vscodeApi.postMessage({ type: "showRuns" }); }); refreshRunsButton.addEventListener("click", () => { @@ -1307,6 +1645,12 @@ function renderChatViewHtml( setComposerBusy(true, false); submissionRoot.className = "submission sending"; submissionRoot.textContent = "Sending turn..."; + currentSubmission = { + busy: true, + status: "sending", + message: "Sending turn...", + }; + syncConversationChrome(); vscodeApi.postMessage({ type: "submitTurn", message, @@ -1315,7 +1659,7 @@ function renderChatViewHtml( }); promptInput.addEventListener("keydown", (event) => { - if ((event.ctrlKey || event.metaKey) && event.key === "Enter") { + if (event.key === "Enter" && event.shiftKey !== true && event.isComposing !== true) { event.preventDefault(); composer.requestSubmit(); } @@ -1325,14 +1669,17 @@ function renderChatViewHtml( renderSubmission(initialSubmission); renderRuns(initialRuns); renderContext(initialContext); + renderApproval(initialApproval); function render(snapshot) { - const items = Array.isArray(snapshot.items) ? snapshot.items : []; - statusTitle.textContent = snapshot.latestStatus || "ProleCoder"; - statusSubtitle.textContent = snapshot.latestRunId - ? snapshot.latestRunId + " - " + snapshot.eventCount + " events" + currentSnapshot = snapshot && typeof snapshot === "object" ? snapshot : initialSnapshot; + const items = Array.isArray(currentSnapshot.items) ? currentSnapshot.items : []; + statusTitle.textContent = currentSnapshot.latestStatus || "ProleCoder"; + statusSubtitle.textContent = currentSnapshot.latestRunId + ? currentSnapshot.latestRunId + " - " + currentSnapshot.eventCount + " events" : "No run events yet."; eventsRoot.replaceChildren(); + syncConversationChrome(); if (items.length === 0) { const empty = document.createElement("div"); @@ -1349,6 +1696,7 @@ function renderChatViewHtml( function renderRuns(snapshot) { const state = snapshot && typeof snapshot === "object" ? snapshot : initialRuns; + currentRuns = state; const runs = Array.isArray(state.runs) ? state.runs : []; const status = typeof state.status === "string" ? state.status : "idle"; const loading = status === "loading"; @@ -1358,6 +1706,10 @@ function renderChatViewHtml( runMessageRoot.textContent = runMessage; runMessageRoot.title = runMessage; runListRoot.replaceChildren(); + if (pendingRunDeleteId.length > 0 && !runs.some((run) => run.runId === pendingRunDeleteId)) { + pendingRunDeleteId = ""; + } + syncConversationChrome(); if (runs.length === 0) { const empty = document.createElement("div"); @@ -1377,13 +1729,15 @@ function renderChatViewHtml( row.className = "run-entry-row"; const button = document.createElement("button"); const status = typeof run.status === "string" ? run.status : "running"; + const runId = typeof run.runId === "string" ? run.runId : ""; + const confirmingDelete = runId.length > 0 && pendingRunDeleteId === runId; button.type = "button"; button.className = "run-entry " + status + (run.runId === selectedRunId ? " selected" : ""); - button.disabled = disabled; - button.title = typeof run.runId === "string" ? run.runId : ""; + button.disabled = disabled || confirmingDelete; + button.title = runId; button.addEventListener("click", () => { - if (typeof run.runId === "string" && run.runId.length > 0) { - vscodeApi.postMessage({ type: "resumeRun", runId: run.runId }); + if (runId.length > 0) { + vscodeApi.postMessage({ type: "resumeRun", runId }); } }); @@ -1405,19 +1759,69 @@ function renderChatViewHtml( deleteButton.disabled = disabled; deleteButton.title = "Delete run"; deleteButton.setAttribute("aria-label", "Delete run"); - deleteButton.textContent = "x"; + deleteButton.textContent = confirmingDelete ? "-" : "x"; deleteButton.addEventListener("click", (event) => { event.stopPropagation(); - if (typeof run.runId === "string" && run.runId.length > 0) { - vscodeApi.postMessage({ type: "deleteRun", runId: run.runId }); + if (runId.length === 0) { + return; } + pendingRunDeleteId = confirmingDelete ? "" : runId; + renderRuns(currentRuns); }); button.append(title, meta); row.append(button, deleteButton); + if (confirmingDelete) { + row.append(renderRunDeleteConfirm(runId)); + } return row; } + function renderRunDeleteConfirm(runId) { + const confirmRoot = document.createElement("div"); + confirmRoot.className = "run-delete-confirm"; + const label = document.createElement("span"); + label.textContent = "Delete this run?"; + + const confirmButton = document.createElement("button"); + confirmButton.type = "button"; + confirmButton.className = "confirm-delete"; + confirmButton.textContent = "Delete"; + confirmButton.addEventListener("click", () => { + pendingRunDeleteId = ""; + optimisticallyDeleteRun(runId); + vscodeApi.postMessage({ type: "deleteRun", runId }); + }); + + const cancelButton = document.createElement("button"); + cancelButton.type = "button"; + cancelButton.className = "confirm-cancel"; + cancelButton.textContent = "Cancel"; + cancelButton.addEventListener("click", () => { + pendingRunDeleteId = ""; + renderRuns(currentRuns); + }); + + confirmRoot.append(label, confirmButton, cancelButton); + return confirmRoot; + } + + function optimisticallyDeleteRun(runId) { + const runs = Array.isArray(currentRuns.runs) + ? currentRuns.runs.filter((run) => run.runId !== runId) + : []; + const selectedRunId = + currentRuns && typeof currentRuns.selectedRunId === "string" && currentRuns.selectedRunId !== runId + ? currentRuns.selectedRunId + : undefined; + renderRuns({ + status: "loading", + runs, + ...(selectedRunId === undefined ? {} : { selectedRunId }), + message: "Deleting run...", + }); + } + function runTitle(run) { if (typeof run.title === "string" && run.title.length > 0) { return run.title; @@ -1661,8 +2065,180 @@ function renderChatViewHtml( return value.length > 18 ? value.slice(0, 18) + "..." : value; } + function renderApproval(approval) { + const nextApproval = approval && typeof approval === "object" ? approval : undefined; + currentApproval = + nextApproval !== undefined && resolvedApprovalIds.has(nextApproval.approvalId) + ? undefined + : nextApproval; + approvalRoot.replaceChildren(); + if (currentApproval === undefined) { + approvalRoot.className = "approval-host"; + return; + } + + approvalRoot.className = "approval-host active"; + approvalRoot.append(renderApprovalCard(currentApproval)); + } + + function renderApprovalCard(approval) { + const card = document.createElement("div"); + card.className = "approval-card"; + + const heading = document.createElement("div"); + heading.className = "approval-heading"; + const title = document.createElement("div"); + title.className = "approval-title"; + title.textContent = typeof approval.title === "string" ? approval.title : "Approval required"; + const risk = document.createElement("div"); + risk.className = "approval-risk"; + risk.textContent = typeof approval.risk === "string" ? approval.risk : "risk"; + heading.append(title, risk); + card.append(heading); + + const detail = document.createElement("div"); + detail.className = "approval-detail"; + detail.textContent = approvalDetailText(approval); + card.append(detail); + + const meta = approvalMetaText(approval); + if (meta.length > 0) { + const metaRoot = document.createElement("div"); + metaRoot.className = "approval-meta"; + metaRoot.textContent = meta; + card.append(metaRoot); + } + + if (typeof approval.outputSummary === "string" && approval.outputSummary.length > 0) { + const output = document.createElement("div"); + output.className = "approval-output"; + output.textContent = "Output: " + approval.outputSummary; + card.append(output); + } + + const hunkInputs = []; + const hunks = Array.isArray(approval.hunks) ? approval.hunks : []; + if (hunks.length > 0) { + const hunkRoot = document.createElement("div"); + hunkRoot.className = "approval-hunks"; + for (const hunk of hunks) { + const label = document.createElement("label"); + label.className = "approval-hunk"; + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = true; + input.dataset.hunkId = hunk.id; + const text = document.createElement("span"); + text.textContent = hunkLabel(hunk); + label.append(input, text); + hunkRoot.append(label); + hunkInputs.push(input); + } + card.append(hunkRoot); + } + + const actions = document.createElement("div"); + actions.className = "approval-actions"; + const rejectButton = document.createElement("button"); + rejectButton.type = "button"; + rejectButton.className = "approval-action reject"; + rejectButton.textContent = "Reject"; + rejectButton.addEventListener("click", () => { + markApprovalResolved(approval.approvalId); + vscodeApi.postMessage({ + type: "approvalDecision", + approvalId: approval.approvalId, + decision: "reject", + }); + renderApproval(undefined); + }); + + const approveButton = document.createElement("button"); + approveButton.type = "button"; + approveButton.className = "approval-action approve"; + approveButton.textContent = "Approve"; + approveButton.addEventListener("click", () => { + markApprovalResolved(approval.approvalId); + const message = { + type: "approvalDecision", + approvalId: approval.approvalId, + decision: "approve", + }; + if (hunkInputs.length > 0) { + message.approvedHunks = selectedHunkIds(hunkInputs); + } + vscodeApi.postMessage(message); + renderApproval(undefined); + }); + + const updateApproveState = () => { + approveButton.disabled = hunkInputs.length > 0 && selectedHunkIds(hunkInputs).length === 0; + }; + for (const input of hunkInputs) { + input.addEventListener("change", updateApproveState); + } + updateApproveState(); + + actions.append(rejectButton, approveButton); + card.append(actions); + return card; + } + + function markApprovalResolved(approvalId) { + if (typeof approvalId === "string" && approvalId.length > 0) { + resolvedApprovalIds.add(approvalId); + } + } + + function approvalDetailText(approval) { + const lines = []; + if (typeof approval.detail === "string" && approval.detail.length > 0) { + lines.push(approval.detail); + } + if (typeof approval.toolName === "string" && approval.toolName.length > 0) { + lines.push("Tool: " + approval.toolName); + } + return lines.join("\\n"); + } + + function approvalMetaText(approval) { + const lines = []; + if (typeof approval.command === "string" && approval.command.length > 0) { + lines.push("Command: " + approval.command); + } + if (typeof approval.cwd === "string" && approval.cwd.length > 0) { + lines.push("Cwd: " + approval.cwd); + } + if (Array.isArray(approval.paths) && approval.paths.length > 0) { + lines.push("Paths: " + approval.paths.join(", ")); + } + if (Array.isArray(approval.riskReasons) && approval.riskReasons.length > 0) { + lines.push("Reasons: " + approval.riskReasons.join(", ")); + } + return lines.join("\\n"); + } + + function selectedHunkIds(inputs) { + return inputs + .filter((input) => input.checked && typeof input.dataset.hunkId === "string") + .map((input) => input.dataset.hunkId); + } + + function hunkLabel(hunk) { + const index = typeof hunk.hunkIndex === "number" ? hunk.hunkIndex + 1 : 1; + const oldStart = typeof hunk.oldStart === "number" ? hunk.oldStart : 0; + const oldCount = typeof hunk.oldCount === "number" ? hunk.oldCount : 0; + const newStart = typeof hunk.newStart === "number" ? hunk.newStart : 0; + const newCount = typeof hunk.newCount === "number" ? hunk.newCount : 0; + const section = typeof hunk.section === "string" && hunk.section.length > 0 + ? " - " + hunk.section + : ""; + return hunk.filePath + " hunk " + index + " (-" + oldStart + "," + oldCount + " +" + newStart + "," + newCount + ")" + section; + } + function renderSubmission(submission) { const state = submission && typeof submission === "object" ? submission : initialSubmission; + currentSubmission = state; const status = typeof state.status === "string" ? state.status : "idle"; const busy = state.busy === true; const runId = typeof state.runId === "string" ? state.runId : ""; @@ -1684,6 +2260,7 @@ function renderChatViewHtml( if (status === "running") { promptInput.value = ""; } + syncConversationChrome(); } function renderSubmissionAction(action) { @@ -1724,10 +2301,34 @@ function renderChatViewHtml( cancelButton.disabled = cancelable !== true; } + function syncConversationChrome() { + const runId = activeConversationRunId(); + const active = runId.length > 0; + shellRoot.classList.toggle("conversation-active", active); + conversationTitleRoot.textContent = "Conversation"; + showRunsButton.disabled = currentSubmission && currentSubmission.busy === true; + } + + function activeConversationRunId() { + const submissionRunId = + currentSubmission && typeof currentSubmission.runId === "string" ? currentSubmission.runId : ""; + const latestRunId = + currentSnapshot && typeof currentSnapshot.latestRunId === "string" ? currentSnapshot.latestRunId : ""; + const selectedRunId = + currentRuns && typeof currentRuns.selectedRunId === "string" ? currentRuns.selectedRunId : ""; + if (submissionRunId || latestRunId || selectedRunId) { + return submissionRunId || latestRunId || selectedRunId; + } + return currentSubmission && currentSubmission.busy === true ? "pending" : ""; + } + function renderItem(item) { const collapsed = item.defaultCollapsed === true; const article = document.createElement(collapsed ? "details" : "article"); article.className = "item " + item.kind + " " + item.tone; + if (collapsed) { + article.open = false; + } const meta = document.createElement("div"); meta.className = "meta"; @@ -1764,10 +2365,6 @@ function renderChatViewHtml( `; } -function safeScriptJson(value: unknown): string { - return JSON.stringify(value).replaceAll("<", "\\u003c"); -} - function nonceValue(): string { return randomUUID().replaceAll("-", ""); } @@ -1881,6 +2478,10 @@ function isSelectDeepSeekModelMessage(message: unknown): boolean { return isRecord(message) && message["type"] === "selectDeepSeekModel"; } +function isShowRunsMessage(message: unknown): boolean { + return isRecord(message) && message["type"] === "showRuns"; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 96d1755..3c5d541 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -145,11 +145,12 @@ export async function activate(context: vscode.ExtensionContext): Promise registerTestCommands(context, chatView); if (rpcServer !== undefined && workspaceRoot !== undefined) { const patchDiffPreviewController = createPatchDiffPreviewController(context, rpcServer, workspaceRoot); + const approvalRequester = testApprovalRequester(context) ?? inlineChatApprovalRequester(chatView); const approvalController = new ApprovalEventController( rpcServer, vscode.window, notifier, - testApprovalRequester(context), + approvalRequester, patchDiffPreviewController, ); context.subscriptions.push(patchDiffPreviewController, approvalController); @@ -241,3 +242,7 @@ function testApprovalRequester(context: vscode.ExtensionContext): ApprovalReques persist: "never", }); } + +function inlineChatApprovalRequester(chatView: ProleChatViewProvider): ApprovalRequester { + return (_window, request) => chatView.requestApproval(request); +} diff --git a/vscode/extension/src/webviewSerialization.ts b/vscode/extension/src/webviewSerialization.ts new file mode 100644 index 0000000..6378864 --- /dev/null +++ b/vscode/extension/src/webviewSerialization.ts @@ -0,0 +1,4 @@ +export function safeScriptJson(value: unknown): string { + const serialized = JSON.stringify(value); + return (serialized === undefined ? "undefined" : serialized).replaceAll("<", "\\u003c"); +} diff --git a/vscode/extension/test/chatApprovals.test.ts b/vscode/extension/test/chatApprovals.test.ts new file mode 100644 index 0000000..5b3a2ac --- /dev/null +++ b/vscode/extension/test/chatApprovals.test.ts @@ -0,0 +1,177 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + approvalDecisionFromWebviewMessage, + chatApprovalSnapshotFromRequest, +} from "../src/chatApprovals.js"; +import { APPROVAL_REJECTED_REASON, type ApprovalPromptRequest } from "../src/commands.js"; + +test("chat approval snapshots expose request details without mutating hunks", () => { + const request = sampleApprovalRequest(); + + const snapshot = chatApprovalSnapshotFromRequest(request); + + assert.deepEqual(snapshot, request); + assert.notEqual(snapshot.hunks, request.hunks); + assert.deepEqual(snapshot.hunks, request.hunks); +}); + +test("chat approval messages map approve and reject decisions", () => { + const request = sampleApprovalRequest({}, false); + + assert.deepEqual( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "approval_1", + decision: "approve", + }, + request, + ), + { + kind: "approve", + approvalId: "approval_1", + persist: "never", + }, + ); + + assert.deepEqual( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "approval_1", + decision: "reject", + }, + request, + ), + { + kind: "reject", + approvalId: "approval_1", + reason: APPROVAL_REJECTED_REASON, + }, + ); +}); + +test("chat approval hunk selection returns partial hunk approvals", () => { + const request = sampleApprovalRequest(); + + assert.deepEqual( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "approval_1", + decision: "approve", + approvedHunks: ["hunk_2", "unknown", "hunk_2"], + }, + request, + ), + { + kind: "approve", + approvalId: "approval_1", + persist: "never", + hunks: { + approved: ["hunk_2"], + }, + }, + ); +}); + +test("chat approval hunk selection approves whole patch when all hunks are selected", () => { + const request = sampleApprovalRequest(); + + assert.deepEqual( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "approval_1", + decision: "approve", + approvedHunks: ["hunk_1", "hunk_2"], + }, + request, + ), + { + kind: "approve", + approvalId: "approval_1", + persist: "never", + }, + ); +}); + +test("chat approval ignores unrelated messages and rejects empty hunk approval", () => { + const request = sampleApprovalRequest(); + + assert.equal( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "other", + decision: "approve", + }, + request, + ), + undefined, + ); + assert.deepEqual( + approvalDecisionFromWebviewMessage( + { + type: "approvalDecision", + approvalId: "approval_1", + decision: "approve", + approvedHunks: [], + }, + request, + ), + { + kind: "reject", + approvalId: "approval_1", + reason: "no patch hunks selected in VS Code", + }, + ); +}); + +function sampleApprovalRequest( + overrides: Partial = {}, + includeHunks = true, +): ApprovalPromptRequest { + return { + approvalId: "approval_1", + toolCallId: "tool_call_1", + toolName: "apply_patch", + risk: "write", + title: "Apply patch", + detail: "Modify README.md", + persistable: true, + command: "apply patch", + cwd: ".", + outputSummary: "patch preview ready", + paths: ["README.md"], + riskReasons: ["modifies files"], + ...(includeHunks + ? { + hunks: [ + { + id: "hunk_1", + filePath: "README.md", + hunkIndex: 0, + oldStart: 1, + oldCount: 2, + newStart: 1, + newCount: 3, + }, + { + id: "hunk_2", + filePath: "README.md", + hunkIndex: 1, + oldStart: 8, + oldCount: 1, + newStart: 9, + newCount: 2, + section: "next block", + }, + ], + } + : {}), + ...overrides, + }; +} diff --git a/vscode/extension/test/electron/index.ts b/vscode/extension/test/electron/index.ts index 40c935f..787d482 100644 --- a/vscode/extension/test/electron/index.ts +++ b/vscode/extension/test/electron/index.ts @@ -100,6 +100,10 @@ async function exerciseChatSendTurnDiagnosticsAndApproval(): Promise { } async function exerciseChatCancel(): Promise { + await postChatMessage({ + type: "showRuns", + }); + await postChatMessage({ type: "submitTurn", message: "integration cancel flow", diff --git a/vscode/extension/test/webviewSerialization.test.ts b/vscode/extension/test/webviewSerialization.test.ts new file mode 100644 index 0000000..d859992 --- /dev/null +++ b/vscode/extension/test/webviewSerialization.test.ts @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { safeScriptJson } from "../src/webviewSerialization.js"; + +test("safeScriptJson serializes undefined as a script-safe literal", () => { + assert.equal(safeScriptJson(undefined), "undefined"); +}); + +test("safeScriptJson escapes script-breaking less-than characters", () => { + assert.equal( + safeScriptJson({ + value: "", + }), + '{"value":"\\u003c/script>"}', + ); +}); From 4ee301ca4d4b66fc9ec61f0f83443ece28499716 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 20:41:37 +0800 Subject: [PATCH 54/84] Updates documentation to reflect Sidebar inline approval and UX improvements --- README.md | 18 +++++++++--------- docs/approval-model.md | 6 +++--- docs/phase-tasks.md | 18 +++++++++--------- docs/roadmap.md | 8 ++++---- docs/testing.md | 6 +++--- docs/vscode-extension.md | 26 +++++++++++++------------- 6 files changed, 41 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 7329fee..e138420 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认和 Enter/Shift+Enter 输入回归,但持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -597,7 +597,7 @@ extension.ts - [x] Agent Turn Loop:串联 Context Builder、provider、reasoning、工具执行、审批、验证和 run log,并通过本地 fixture 端到端 smoke test。 - [x] CLI `run` / `rpc` 最小闭环:支持 fixture/deepseek provider、工作区参数、JSON event、审批、验证命令、JSON-RPC error 输出、进程级 CLI fixture smoke 和小型真实仓库 CLI 联网验收。 - [x] Agent RPC Server:实现 stdio 事件桥接、双向 request loop、真实 Turn Loop handler、实时事件输出、pending approval 队列、审批超时和取消语义。 -- [x] 审批前端基础:实现 CLI prompt、RPC approve/reject/cancel 分发、TypeScript 协议类型、TUI prompt 状态机和 VS Code modal approval adapter。 +- [x] 审批前端基础:实现 CLI prompt、RPC approve/reject/cancel 分发、TypeScript 协议类型、TUI prompt 状态机和 VS Code legacy modal approval adapter;后续默认 UX 已改为 Sidebar 内联审批。 - [x] Phase 1 合并前第一轮测试增强:完成 `pnpm run check` 基线验证、patch 失败恢复、reasoning 边界、CancellationToken 并发和 CLI event stream 顺序测试。 - [x] 合并前测试基础设施收敛:提取共享 `agent-core::test_helpers::TestWorkspace`,统一当前分散在 agent-core、agent-rpc、cli、demo/live 测试中的临时工作区 helper。 - [x] 合并前 live 测试配置收敛:统一 live API key 测试 helper,测试侧按 `PROLE_CODER_DEEPSEEK_API_KEY`、`DEEPSEEK_API_KEY`、`.secrets/deepseek-api-key` 的顺序读取。 @@ -647,7 +647,7 @@ extension.ts - [x] VS Code RPC/commands 边界测试补齐:已覆盖 `RpcServerManager` 启动异常、stdio 缺失、无效 JSON、process error、stop、onEvent dispose、stderr preview、sendRequest 写入失败等路径,以及 openChat 启动失败、非 Error 错误、不可持久审批 approve 和审批消息 paths 拼接。 - [x] Sidebar Chat 与 `agent.event` 渲染:已贡献 ProleCoder Activity Bar view 和 Webview Sidebar Chat,订阅 `RpcServerManager.onEvent()` 并展示 assistant delta、tool lifecycle、审批、context/provider 和 terminal event;assistant delta 会按 run/turn 合并为一条消息。 - [x] 文本输入发送 turn,并通过 `agent.sendTurn` 驱动真实 Agent 回合:Sidebar Chat 已提供 prompt 输入和 mode 选择,Webview 通过 typed `RpcServerManager.sendTurn()` 调用真实 `agent.sendTurn`,并在 accepted 后等待同一 run 的 terminal event 收口输入状态。 -- [x] VS Code 审批 UI 接入真实 RPC pending queue:`ApprovalEventController` 订阅 `tool.approvalRequired`,复用 VS Code modal approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 回传到 `agent.approve` / `agent.reject`。 +- [x] VS Code 审批 UI 接入真实 RPC pending queue:`ApprovalEventController` 订阅 `tool.approvalRequired`,复用 VS Code approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 回传到 `agent.approve` / `agent.reject`;后续默认 UX 已改为 Sidebar 内联审批卡片。 - [x] 命令风险分类器和动态风险升级:Agent Core 已在 shell 审批前识别依赖安装、网络访问、远程 git、删除和发布命令,升级 `tool.requested` / `tool.approvalRequired` 的风险,并通过 `riskReasons` 在 CLI/TUI/VS Code 审批展示升级原因。 - [x] 更强进程树清理策略:命令类工具启动时建立可收束的进程树边界,Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底,取消和超时会清理 shell/search/git 等工具的子进程树。 - [x] Native diff editor 展示 patch,并为 hunk 级审批预留交互边界:VS Code 侧缓存 `tool.requested` 的 `apply_patch` unified diff,在对应 `tool.approvalRequired` 前用 VS Code 原生 diff editor 展示虚拟补丁结果,并生成稳定 hunk approval boundary 供后续细粒度审批复用。 @@ -661,7 +661,7 @@ extension.ts - 关闭 stdin、writer 失败或插件停用会取消 active run,并在 run log 中收口到 terminal event。 - 更强进程树清理策略完成并通过可执行测试或清晰的手动验收说明。已新增 descendant process 取消回归测试。 - Sidebar Chat 能展示 `assistant.delta`、tool lifecycle 和 terminal event;Chat 输入能发送真实 `agent.sendTurn` 并收到最终结果。已完成首版输入发送和事件流收口。 -- VS Code 审批弹窗能消费 `tool.approvalRequired`,并把 approve/reject 回传到 `agent.approve` / `agent.reject`。已完成首版真实 RPC pending queue 接入。 +- VS Code 审批 UI 能消费 `tool.approvalRequired`,并把 approve/reject 回传到 `agent.approve` / `agent.reject`。已完成真实 RPC pending queue 接入,当前默认使用 Sidebar 内联审批卡片。 - Sidebar Chat 能展示最近 run 列表,并通过 `agent.resume` 回放历史事件。已完成首版 Run List / resume 接入。 - Sidebar Chat 能可视化 `context.built` 的 token 分段、来源纳入/省略和 manifest/cache/estimator metadata。已完成首版 Context Capsule 可视化。 - `docs/phase-tasks.md` 的 Phase 3 条目全部标记为 `[x]` 后,README 才能把 Phase 3 表述为整阶段完成。当前 Phase 3 已满足该条件。 @@ -675,10 +675,10 @@ extension.ts - [x] P4-5:RPC 高频事件输出节流与批量发送策略,实时 wire 层支持 `agent.eventBatch`,Run Log 与 replay 仍保持逐事件 `seq` 事实来源。 - [x] P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已新增 `RpcServerManager.cancel()`、Cancel 按钮和运行中 composer 状态收口。 - [x] P4-7:Problems 面板 diagnostics 通过 diagnostic attachments 进入 Context Builder,VS Code 发送 turn 时会采集当前 Problems 快照,按协议 attachment 上限裁剪并优先保留 error。 -- [x] P4-8:Terminal command approval,审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要和持久化语义;P5-2 后主审批 modal 不再暴露复杂持久化选项。 +- [x] P4-8:Terminal command approval,审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要和持久化语义;P5-2 后主审批 UI 不再暴露复杂持久化选项。 - [x] P4-9:审批持久化存储,RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 - [x] P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已新增 Open Settings 命令,打开 VS Code 设置并展示 RPC server capability、默认模型、预算、审批能力、RPC command/state;配置只包含非敏感 RPC/FIM 选项,不保存 API Key。 -- [x] P4-11:真实 hunk 级 patch 审批,首版限定 `apply_patch`,Core/RPC 支持 selected hunk 决策、校验未知/重复 hunk、Run Log 记录 selected/all 范围,VS Code modal 可选择 hunks 并通过 `agent.approve.hunks` 回传;审批事件 payload 已同步协议 fixture。 +- [x] P4-11:真实 hunk 级 patch 审批,首版限定 `apply_patch`,Core/RPC 支持 selected hunk 决策、校验未知/重复 hunk、Run Log 记录 selected/all 范围,VS Code 审批 UI 可选择 hunks 并通过 `agent.approve.hunks` 回传;审批事件 payload 已同步协议 fixture。 - [x] P4-12:FIM completion preview,已新增 `agent.previewFim` RPC、DeepSeek beta FIM adapter、fixture provider 预览和 VS Code inline completion provider,模型选择只依赖 server capability 的 `supportsFim`。 - [x] P4-13:VSIX alpha / pre-release 打包与插件安装说明,已新增 `pnpm run vsix:alpha`,在 `target/vsix/` 生成可安装 pre-release VSIX 与 SHA-256 校验和,并在 `docs/release.md` 记录 clean 环境安装验收路径。 - [x] P4-14:补齐 end-to-end 集成测试覆盖,已在 `pnpm run vscode:test-electron` 中接入本地 JSON-RPC fixture server,覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume 和隔离 VS Code profile 启动;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的 P4-13 路径手动验收。 @@ -695,7 +695,7 @@ extension.ts ### Phase 5:VS Code Codex-like UX 与开发工作流 - [x] P5-1:原生 VS Code Chat Participant `@prole`,让常规入口默认打开 VS Code Chat 侧栏体验;保留 Activity Bar Webview 作为 Run List / Context Capsule / diff 等高级面板。 -- [x] P5-2:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 Select Hunks 边界;持久化策略继续由后端策略控制,不在主弹窗里暴露复杂选项。 +- [x] P5-2:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 hunk 选择边界;持久化策略继续由后端策略控制,不在主审批 UI 里暴露复杂选项。 - [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 - [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 - [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 @@ -707,14 +707,14 @@ extension.ts - [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 - [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX:`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar resume 后继续同一会话发送多轮 turn;Run List 支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder` 便于 debug。 - [x] P5-13:结构化 provider 配置错误码与恢复动作:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回结构化 `data.recoverableAction`,Sidebar/原生 Chat 依据恢复动作展示或触发 Configure API Key 入口,不再依赖后端英文错误消息;run failed payload 也可携带同一恢复动作。 -- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 +- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口;已根据真实试用补齐 Sidebar 对话专属视图、内联 approval/delete 确认、过程事件强制默认折叠以及 Enter 发送 / Shift+Enter 换行,后续继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 验收标准: - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- 主审批弹窗保持简单;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 +- 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 - `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/approval-model.md b/docs/approval-model.md index 500a924..4f694b8 100644 --- a/docs/approval-model.md +++ b/docs/approval-model.md @@ -108,12 +108,12 @@ pending - TypeScript:`packages/protocol/src/index.ts`、`vscode/extension/src/approvalFlow.ts`、`vscode/extension/src/commands.ts`。 - JSON-RPC 事件:`docs/json-rpc-protocol.md` 中的 `tool.requested`、`tool.approvalRequired`、`tool.approvalResolved`、`agent.approve`、`agent.reject`、`agent.cancel`。 -当前 Rust 和 TypeScript 已定义风险等级、审批要求、持久化枚举和状态机转换规则。Agent Turn Loop 已能在工具执行前写入 `tool.approvalRequired`,根据审批策略等待批准、拒绝、取消、过期或持久批准复用,并写入 `tool.approvalResolved`。CLI 二进制已有 stdin/stderr prompt;`agent-rpc` request loop 已能分发 `agent.approve` / `agent.reject` / `agent.cancel`;`AgentTurnLoopRpcHandler` 已实现单 active run 的 pending approval 队列和 session/workspace 持久批准存储。Agent Core 已在 shell 工具审批前执行命令风险分类:依赖安装、网络访问、远程 git 和发布命令会升级到 `network`,删除、强制 push、git reset/clean 等会升级到 `destructive`,并在 `tool.requested` / `tool.approvalRequired` 中写入 `riskReasons`、`command`、`cwd` 和上一条 shell 输出摘要。VS Code 插件已有 modal approval adapter 并接入真实 RPC pending queue;`apply_patch` 审批会先打开 VS Code 原生 diff 预览,可选择 hunk 后通过 `agent.approve.hunks` 回传,RPC/Core 会校验并只应用已批准 hunks;TUI 已有可测试的 prompt 状态机。 +当前 Rust 和 TypeScript 已定义风险等级、审批要求、持久化枚举和状态机转换规则。Agent Turn Loop 已能在工具执行前写入 `tool.approvalRequired`,根据审批策略等待批准、拒绝、取消、过期或持久批准复用,并写入 `tool.approvalResolved`。CLI 二进制已有 stdin/stderr prompt;`agent-rpc` request loop 已能分发 `agent.approve` / `agent.reject` / `agent.cancel`;`AgentTurnLoopRpcHandler` 已实现单 active run 的 pending approval 队列和 session/workspace 持久批准存储。Agent Core 已在 shell 工具审批前执行命令风险分类:依赖安装、网络访问、远程 git 和发布命令会升级到 `network`,删除、强制 push、git reset/clean 等会升级到 `destructive`,并在 `tool.requested` / `tool.approvalRequired` 中写入 `riskReasons`、`command`、`cwd` 和上一条 shell 输出摘要。VS Code 插件已有 legacy modal approval adapter 和默认 Sidebar 内联 approval card,并接入真实 RPC pending queue;`apply_patch` 审批会先打开 VS Code 原生 diff 预览,可选择 hunk 后通过 `agent.approve.hunks` 回传,RPC/Core 会校验并只应用已批准 hunks;TUI 已有可测试的 prompt 状态机。 ## 后续增强 - 扩展 RPC 审批队列到多 active run、跨进程恢复和前端断连后的自动取消;当前实现只支持单 active run 的内存等待队列。 - 扩展 patch 的动态风险升级;`shell` 的动态风险升级已覆盖下载依赖、访问网络、删除文件、发布和远程 git 操作,`apply_patch` 已支持 hunk 级批准。 -- 扩展持久批准的可审计 metadata、清理入口和高级 UI 管理;`network` 和 `destructive` 仍不允许持久化,VS Code 主审批弹窗不重新暴露 session/workspace 选项。 +- 扩展持久批准的可审计 metadata、清理入口和高级 UI 管理;`network` 和 `destructive` 仍不允许持久化,VS Code 主审批 UI 不重新暴露 session/workspace 选项。 - 继续增强 TUI 的真实 RPC pending 队列接入;VS Code 已能消费 `tool.approvalRequired` 并发送 `agent.approve` / `agent.reject`。 -- 增加跨前端一致性测试,确保同一工具请求在 CLI、TUI 和 VS Code 中展示的风险、路径、命令、风险原因和持久化能力语义一致;VS Code 主审批弹窗保持简化的 Approve / Reject。 +- 增加跨前端一致性测试,确保同一工具请求在 CLI、TUI 和 VS Code 中展示的风险、路径、命令、风险原因和持久化能力语义一致;VS Code 主审批 UI 保持简化的 Approve / Reject。 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index d3c95fd..51e420a 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成,P5-14 持续 UX 测试与体验改进占位仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 已补充一批真实试用 UX 回归修复,但持续 UX 测试与体验改进占位仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -42,7 +42,7 @@ | [x] | `TurnProvider` async / streaming 边界和 `TurnEventSink` 实时事件出口 | `docs/turn-loop.md` | CLI `--json` 和 `StdioEventBridge` 已接入。 | | [x] | CLI `run` / `rpc` 最小闭环和 JSON-RPC 错误输出 | `docs/cli.md` | 已有库级、进程级和 fixture smoke 测试。 | | [x] | Agent RPC Server request loop、真实 Turn Loop handler、pending approval、取消、超时、EOF shutdown 和 `agent.listRuns` | `docs/rpc-server.md`、`docs/json-rpc-protocol.md` | 已覆盖审批批准/拒绝/取消/超时、并发拒绝、EOF shutdown、resume/listRuns。 | -| [x] | CLI/TUI/VS Code 审批前端基础原语 | `docs/approval-model.md`、`docs/tui.md`、`docs/vscode-extension.md` | CLI prompt、TUI prompt 状态机、VS Code modal adapter 已实现;完整 UI 接入归入后续阶段。 | +| [x] | CLI/TUI/VS Code 审批前端基础原语 | `docs/approval-model.md`、`docs/tui.md`、`docs/vscode-extension.md` | CLI prompt、TUI prompt 状态机、VS Code legacy modal adapter 已实现;后续默认 UX 已改为 Sidebar 内联审批。 | | [x] | Rust/TypeScript 工具注册表和错误码协议交叉校验 | `docs/tool-system.md`、`docs/json-rpc-protocol.md`、`packages/protocol` | 工具 registry fixture 与错误码表已进入默认测试。 | | [x] | 合并前测试基础设施、live 配置收敛和离线最终验收 | `docs/testing.md`、`docs/demos.md` | `pnpm run check`、测试清单、离线 demo、diff/sensitive scan 已完成。 | | [x] | VS Code RPC server 管理和 JSON-RPC request client 前置实现 | `docs/vscode-extension.md`、`docs/roadmap.md` | 属于 Phase 3 前置项,已提前完成;不作为 Phase 1 阻塞验收条件。 | @@ -82,10 +82,10 @@ | [x] | VS Code RPC/commands 边界测试补齐 | `docs/vscode-extension.md`、`.agents/communication/daily/2026-05-28/code_review.md` | 已完成:`rpcServer.test.ts` 覆盖 spawn throw、stdio 缺失、invalid JSON、process error、stop pending startup、onEvent dispose、stderrPreview、sendRequest 写入失败和非 agent.event 通知;`commands.test.ts` 覆盖 openChat 启动失败提示、非 Error fallback、`persistable: false` approve 和 paths 拼接。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | Sidebar Chat 和 `agent.event` 渲染 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 贡献 ProleCoder Activity Bar view 和 Webview Sidebar Chat;`ProleChatViewProvider` 订阅 `RpcServerManager.onEvent()`,通过 `ChatEventTimeline` 渲染 assistant delta、tool lifecycle、审批、context/provider 和 terminal event,并合并同一 run/turn 的 assistant delta。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | 文本输入发送 turn 并接收真实 Agent 响应 | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 提供 prompt 输入和 mode 选择,Webview submit 经过 `chatInput` 校验后调用 typed `RpcServerManager.sendTurn()`,accepted 后通过同一 run 的 `agent.event` terminal event 收口输入状态。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | VS Code 审批 UI 接入真实 RPC pending queue | `docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:新增 `ApprovalEventController` 订阅 `tool.approvalRequired`,校验 protocol payload 后调用 VS Code modal approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 发送 `agent.approve` / `agent.reject`;重复 approvalId 不会重复弹窗。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | VS Code 审批 UI 接入真实 RPC pending queue | `docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:新增 `ApprovalEventController` 订阅 `tool.approvalRequired`,校验 protocol payload 后调用 VS Code approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 发送 `agent.approve` / `agent.reject`;重复 approvalId 不会重复提示。后续 P5-14 已把默认 UX 从系统 modal 切换为 Sidebar 内联审批卡片。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | 命令风险分类器和动态风险升级 | `README.md`、`docs/approval-model.md`、`docs/tool-system.md`、`docs/turn-loop.md` | 已完成:Agent Core 对 shell 命令做词法分段和显式命令族分类,递归检查 shell 包装器、`$(...)` 和传统反引号子命令,识别依赖安装、网络访问、远程 git、删除和发布命令,升级 `tool.requested` / `tool.approvalRequired` 风险并输出 `riskReasons`;VS Code/CLI/TUI 展示升级原因。验收:`cargo fmt --check`、`cargo test -p prole-coder-agent-core command_risk`、`cargo test -p prole-coder-agent-core turn_loop_upgrades_shell_approval_risk`、`cargo test`、`cargo clippy --all-targets -- -D warnings`、`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`git diff --check`。 | | [x] | 更强进程树清理策略 | `docs/tool-system.md`、`docs/security-model.md`、`docs/roadmap.md` | 已完成:命令类工具启动时建立可收束的进程树边界,Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底;取消和超时会清理 shell/search/git 等工具的子进程树。验收:`cargo test -p prole-coder-agent-core shell_cancels_descendant_processes`、`cargo test -p prole-coder-agent-core shell_timeout_cleans_descendant_processes`、`cargo test -p prole-coder-agent-core shell_cancels_running_command`。 | -| [x] | Native diff editor 与 hunk 级审批边界 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 侧新增 patch preview controller,缓存 `tool.requested.argumentsPreview.unifiedDiff`,在 `apply_patch` 审批 modal 前打开 VS Code 原生 diff editor;纯 TS parser 会生成稳定 hunk approval boundary,当前仍以 whole-patch approve/reject 回传,为后续 hunk 级决策预留结构。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | Native diff editor 与 hunk 级审批边界 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 侧新增 patch preview controller,缓存 `tool.requested.argumentsPreview.unifiedDiff`,在 `apply_patch` 审批提示前打开 VS Code 原生 diff editor;纯 TS parser 会生成稳定 hunk approval boundary,当前仍以 whole-patch approve/reject 回传,为后续 hunk 级决策预留结构。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | Run List / resume | `README.md`、`docs/vscode-extension.md`、`docs/rpc-server.md` | 已完成:VS Code Sidebar Chat 通过 typed `RpcServerManager.listRuns()` 拉取最近 run summary,Run List 保留 loading/failed/selected 状态;点击历史 run 会调用 `agent.resume` 并清空当前事件视图,随后消费 replay 的 `agent.event`。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | | [x] | Context Capsule 可视化 | `README.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:VS Code Sidebar Chat 新增 Context Capsule 面板,消费 `context.built` metadata,展示 StablePrefix / DynamicPrelude / TurnSuffix token 分布、input/stable budget、cache/estimator 摘要、included/omitted source 预览和 manifest 摘要。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | @@ -100,21 +100,21 @@ | [x] | P4-5:RPC 高频事件输出节流与批量发送策略 | `docs/rpc-server.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:实时 live event wire 层支持 `agent.eventBatch` 批量发送,VS Code manager 按序分发;Run Log 仍逐事件写入并保持 `seq` 为事实来源,`agent.resume` replay 仍按单事件结构输出。 | | [x] | P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:新增 `RpcServerManager.cancel()` typed helper、Cancel 按钮和运行中 composer 状态;覆盖 typed cancel RPC 边界测试。 | | [x] | P4-7:Problems 面板诊断进入 Context Builder | `README.md`、`docs/vscode-extension.md`、`docs/context-capsule.md` | 已完成:VS Code 发送 turn 时采集 Problems 快照,并通过 `agent.sendTurn.attachments` 的 diagnostic attachment 注入;前端按协议 attachment 上限裁剪、优先保留 error,Core/Context Builder 继续负责 attachment 校验和 token 预算。 | -| [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批弹窗不再暴露复杂持久化选项。 | +| [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批 UI 不再暴露复杂持久化选项。 | | [x] | P4-9:审批持久化存储 | `docs/approval-model.md`、`docs/tool-system.md`、`docs/vscode-extension.md` | 已完成:RPC pending queue 支持 session 内存复用和 workspace `.prole-coder/approvals.v1.json` 存储;继续在 Core 和 RPC 层禁止 network/destructive 风险持久化。 | | [x] | P4-10:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 不进入 VS Code settings。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | P4-11:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code modal 提供 selected hunk quick pick;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | +| [x] | P4-11:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code 审批 UI 提供 selected hunk 选择;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | | [x] | P4-12:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | | [x] | P4-13:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | | [x] | P5-1:原生 VS Code Chat Participant `@prole` | `README.md`、`docs/vscode-extension.md` | 已完成:贡献 `contributes.chatParticipants` 和 `vscode.chat.createChatParticipant`,让 `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 侧栏并填入 `@prole`;Activity Bar Webview 保留为高级状态面板。验收:`pnpm -r typecheck`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`。 | -| [x] | P5-2:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批弹窗收敛为 Approve / Reject;`apply_patch` 多 hunk 继续提供 Select Hunks quick pick;持久化批准能力保留在后端策略与 RPC 队列,不在主审批弹窗里暴露复杂选项。验收:`commands.test.ts` 覆盖简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | +| [x] | P5-2:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批动作收敛为 Approve / Reject;`apply_patch` 多 hunk 继续保留 hunk 选择边界;持久化批准能力保留在后端策略与 RPC 队列,不在主审批 UI 里暴露复杂选项。验收:`commands.test.ts` 覆盖 legacy modal adapter 的简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | | [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | | [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | @@ -126,7 +126,7 @@ | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | | [x] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | -| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已登记 extension-host E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、原生确认后的 run 删除与 Run List 刷新。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | +| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已补齐:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板,approval 和 run delete 改为就地确认 UI,tool/provider/request 等过程事件强制默认折叠,输入框 Enter 发送且 Shift+Enter 换行;已登记后续 E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片和输入快捷键。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 3da6418..8345790 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -41,7 +41,7 @@ - streaming tool call 增量拼装验证:adapter 已区分 `ChatToolCallDelta` 与完整 `ChatToolCall`,`ChatToolCallAccumulator` 会按 `index` 拼接 arguments 并拒绝缺失或冲突元数据;`live_streaming_tool_call_accumulator_smoke_test` 已用真实 DeepSeek streaming 验收工具调用 delta 形态。 - Agent RPC Server 双向 request loop:`agent-rpc` 已支持 newline-delimited JSON-RPC request 读取、初始化顺序检查、`agent.initialize` / `agent.sendTurn` / `agent.resume` 分发、response/error 写回、EOF shutdown,以及 handler 返回事件的 `agent.event` 有序输出。 - RPC/CLI 实时事件输出:`AgentTurnLoop::run_turn_with_event_sink` 会在 Run Log 事件追加成功后立即调用 `TurnEventSink`;`StdioEventBridge` 已实现该接口,CLI `--json` 输出顺序与本地 `events.jsonl` 的 `seq` 一致,不再等 run 完成后批量回放。 -- CLI/RPC/TUI/VS Code 审批基础:Turn Loop 会写入 `tool.approvalRequired` 和 `tool.approvalResolved`;CLI 二进制支持 stdin/stderr 交互式 y/n 审批;RPC request loop 已分发 `agent.approve` / `agent.reject`;TypeScript 协议类型已补齐;TUI prompt 状态机和 VS Code modal approval adapter 已有测试覆盖。 +- CLI/RPC/TUI/VS Code 审批基础:Turn Loop 会写入 `tool.approvalRequired` 和 `tool.approvalResolved`;CLI 二进制支持 stdin/stderr 交互式 y/n 审批;RPC request loop 已分发 `agent.approve` / `agent.reject`;TypeScript 协议类型已补齐;TUI prompt 状态机、VS Code legacy modal adapter 和默认 Sidebar 内联审批入口已有测试覆盖。 - 真实 RPC Turn Loop handler:`AgentTurnLoopRpcHandler` 已通过 provider factory 复用 Core Turn Loop,`agent.sendTurn` 会创建 run log、驱动 provider 和工具执行,并把结果事件交给 request loop;CLI `rpc` 子命令已提供 stdio 入口。 - RPC 真实审批等待队列:`AgentTurnLoopRpcHandler` 会在 `tool.approvalRequired` 处登记 pending approval,后台 Turn Loop worker 等待 `agent.approve` / `agent.reject`,批准后继续执行工具,拒绝后记录 `tool.approvalResolved` 和 `run.failed`。 - RPC 审批超时/取消:pending approval 已记录过期时间;`agent.cancel` 和 request loop EOF shutdown 会取消等待审批的 active run,超时会自动解析为 expired,这些路径都会记录 `tool.approvalResolved` 和 `run.canceled`。 @@ -105,7 +105,7 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: @@ -116,12 +116,12 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 - `agent.cancel` 类型化 helper 与 Chat Cancel UI 已接入,并与 Terminal approval 做轻量 composer UX review。 - Problems 面板诊断已通过 diagnostic attachments 进入 Context Builder,插件不新增独立 diagnostics 状态同步 RPC。 -- Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P5-2 后 VS Code 主审批弹窗保持 Approve / Reject。 +- Terminal command approval 已支持命令、cwd、风险等级、风险原因、输出摘要字段和持久化语义;P5-2 后 VS Code 主审批动作保持 Approve / Reject,P5-14 默认改为 Sidebar 内联审批卡片。 - 审批持久化存储已支持 session/workspace,继续禁止 network/destructive 风险持久化。 - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 - P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 -- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归。 +- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归;已补齐 Sidebar 对话专属视图、webview 内联 approval/delete 确认、过程事件默认折叠和 Enter / Shift+Enter 输入行为。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 diff --git a/docs/testing.md b/docs/testing.md index d3bc733..37b45a4 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -75,7 +75,7 @@ Phase 5 P5-1 到 P5-5 的 Codex-like UX 与诊断收敛继续复用 Phase 4 exte - `chatParticipantCore.test.ts` 覆盖原生 `@prole` Chat Participant turn runner、命令到 run mode 的映射、sendTurn response 前早到事件缓冲、assistant delta streaming、缺少 RPC client 的错误和自动上下文进度提示。 - `logging.test.ts` 覆盖 `ProleCoder` Output Channel 日志格式与输出分发;`chatParticipantCore.test.ts` 还覆盖 RPC turn 失败写入 logger,确保完整错误可在 Output 面板诊断。 -- `commands.test.ts` 覆盖简化后的审批 choices:主弹窗只暴露 `Approve` / `Reject`,`Approve` 映射一次性批准,`apply_patch` 多 hunk 走 `Select Hunks`。 +- `commands.test.ts` 覆盖 legacy modal adapter 的简化审批 choices:主动作只暴露 `Approve` / `Reject`,`Approve` 映射一次性批准;`chatApprovals.test.ts` 覆盖 Sidebar 内联 approval card 的 approve/reject、部分 hunk、全 hunk 和空 hunk 选择解析。 - `test/electron/index.ts` 覆盖 VS Code manifest 中的 `contributes.chatParticipants`,并通过 `ProleCoder: Open Chat` 入口验证原生 Chat 入口不会依赖手动拖动 Activity Bar view。 - `pnpm run vsix:smoke` 和 `pnpm run vsix:alpha` 会校验 VSIX manifest 中的 `onChatParticipant:prole-coder.chatParticipant` activation event 以及 `@prole` Chat Participant 贡献点。 @@ -87,10 +87,10 @@ Phase 5 P5-6 到 P5-13 的 API key/model、Git 工作流、Sidebar 连续会话 - `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、trash 按钮删除非 active key、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 - `rpcServer.test.ts` 覆盖 RPC child env 注入、key 轮换后重启使用新 env,以及 typed `agent.deleteRun` request;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 -- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 +- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 -P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、通过原生确认完成 run 删除以及 Run List 刷新。 +P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、过程事件默认折叠和 Enter / Shift+Enter 输入行为。 ## 新增测试的协作要求 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index f2321f3..7d26049 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、审批弹窗 adapter、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 持续 UX 测试与体验改进占位仍未完成。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -53,16 +53,16 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server `vscode/extension/src/commands.ts` 还提供 `requestApproval`: -- 使用 VS Code modal warning 展示审批摘要。 -- 主审批弹窗只展示 `Approve` / `Reject`;`Approve` 映射为一次性批准,关闭弹窗映射为拒绝。 -- `apply_patch` 多 hunk 审批继续提供 `Select Hunks` quick pick,持久化批准能力保留在 Core/RPC 策略与队列中,不在主弹窗暴露复杂选项。 +- 默认使用 Sidebar 内联审批卡片展示审批摘要。 +- 主审批动作只展示 `Approve` / `Reject`;`Approve` 映射为一次性批准,`Reject` 映射为拒绝。 +- `apply_patch` 多 hunk 审批在内联卡片中提供 hunk checkbox,持久化批准能力保留在 Core/RPC 策略与队列中,不在主审批 UI 暴露复杂选项。 `vscode/extension/src/approvalFlow.ts` 当前接入真实 RPC pending approval: - 订阅 `RpcServerManager.onEvent()`,只处理 `tool.approvalRequired`。 - 校验 approval payload 的 `approvalId`、`toolCallId`、`toolName`、`risk`、`title`、`detail`、`persistable`、`command`、`cwd`、`outputSummary` 和 `paths`。 -- 复用 `requestApproval` 打开 VS Code modal,并把 approve/reject 结果发送为 typed `RpcServerManager.approve()` / `reject()`。 -- 记录已处理的 approvalId,避免重复事件触发重复弹窗。 +- 复用 `ApprovalEventController` 的顺序和去重逻辑,默认通过 `ProleChatViewProvider.requestApproval()` 打开 Sidebar 内联卡片,并把 approve/reject 结果发送为 typed `RpcServerManager.approve()` / `reject()`。 +- 记录已处理的 approvalId,避免重复事件触发重复提示。 ## 配置 @@ -99,9 +99,9 @@ Phase 3 P0 顺序: 3. 渲染 `agent.event` 事件流。已完成 Sidebar Chat 首版,能消费 manager 转发的事件。 4. 支持文本输入并通过 `agent.sendTurn` 发送真实 turn。已完成首版 Sidebar Chat 输入发送。 5. 通过 JSON-RPC request client 回传用户动作。已完成 approval approve/reject 回传。 -6. 展示审批请求和命令输出摘要。已完成首版 `tool.approvalRequired` modal 接入真实 RPC pending queue。 -7. 接入命令风险分类器输出,在审批 UI 中展示动态升级后的风险等级和原因。已完成:approval modal 和 Sidebar Chat 时间线都会展示 `riskReasons`。 -8. 使用 VS Code 原生 diff editor 展示 patch,并为 hunk 级审批预留交互边界。已完成:`PatchDiffPreviewController` 缓存 `tool.requested` 中的 `apply_patch` unified diff,在审批 modal 前打开虚拟 after 文档与 workspace before 文档的原生 diff,并保存 whole-patch 模式下的稳定 hunk boundary。 +6. 展示审批请求和命令输出摘要。已完成 `tool.approvalRequired` 接入真实 RPC pending queue;默认 UX 已从系统 modal 收敛为 Sidebar 内联审批卡片。 +7. 接入命令风险分类器输出,在审批 UI 中展示动态升级后的风险等级和原因。已完成:approval UI 和 Sidebar Chat 时间线都会展示 `riskReasons`。 +8. 使用 VS Code 原生 diff editor 展示 patch,并为 hunk 级审批预留交互边界。已完成:`PatchDiffPreviewController` 缓存 `tool.requested` 中的 `apply_patch` unified diff,在审批提示前打开虚拟 after 文档与 workspace before 文档的原生 diff,并保存 whole-patch 模式下的稳定 hunk boundary。 9. 展示 Run List / resume。已完成:Sidebar Chat 顶部 Run List 调用 `agent.listRuns` 展示最近 run summary,点击历史 run 后调用 `agent.resume`,清空当前事件视图并消费 replay 的 `agent.event`。 10. 展示 Context Capsule 可视化。已完成:Sidebar Chat 消费 `context.built` metadata,展示三层 token 分布、input/stable budget、cache/estimator 摘要、included/omitted sources 和 manifest 摘要。 @@ -113,7 +113,7 @@ Phase 3 P0 验收标准: - stdin EOF、writer BrokenPipe 或插件停用会取消 active run;run log 最终出现 `run.canceled` 或已有 terminal event。 - Sidebar Chat 能消费 `agent.event` 并展示 `assistant.delta`、tool lifecycle 和 terminal event。已完成首版事件渲染。 - Chat 输入能发送真实 `agent.sendTurn`,并通过事件流收到最终结果。已完成首版输入发送和事件流收口。 -- `tool.approvalRequired` 触发 VS Code modal,approve/reject 能回传到 `agent.approve` / `agent.reject`。已完成真实 RPC pending queue 接入;`apply_patch` 首版支持 selected hunk quick pick 并通过 `agent.approve.hunks` 回传。 +- `tool.approvalRequired` 触发 Sidebar 内联审批卡片,approve/reject 能回传到 `agent.approve` / `agent.reject`。已完成真实 RPC pending queue 接入;`apply_patch` 支持 selected hunk 并通过 `agent.approve.hunks` 回传。 - Sidebar Chat 能通过 `agent.listRuns` 展示最近 run,并用 `agent.resume` 回放历史事件。已完成首版 Run List / resume 接入。 - Sidebar Chat 能把 `context.built` 渲染为 Context Capsule 面板,展示 token 分段、来源和 manifest/cache/estimator metadata。已完成首版 Context Capsule 可视化。 - `ProleCoder: Open Settings` 能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key,DeepSeek model ID 作为非敏感配置保存。 @@ -128,7 +128,7 @@ Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: 5. P4-5:RPC 高频事件输出节流与批量发送策略,已完成:实时 live event 支持 `agent.eventBatch`,保持 Run Log `seq` 与 replay 语义稳定。 6. P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已完成:`RpcServerManager.cancel()` 和 Sidebar Chat Cancel 按钮接入真实 RPC。 7. P4-7:通过 diagnostic attachments 读取 Problems 面板诊断并交给 Agent Core,已完成:发送 turn 时采集 Problems 快照,并按协议 attachment 上限裁剪。 -8. P4-8:Terminal command approval 展示命令、cwd、风险等级、上一条 shell 输出摘要和持久化语义,已完成:shared protocol payload 和后端策略已支持;P5-2 后主审批弹窗不再暴露持久化选项。 +8. P4-8:Terminal command approval 展示命令、cwd、风险等级、上一条 shell 输出摘要和持久化语义,已完成:shared protocol payload 和后端策略已支持;P5-2 后主审批 UI 不再暴露持久化选项。 9. P4-9:审批持久化存储,已完成:RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 10. P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已完成:Open Settings 命令展示 `agent.initialize` 返回的 capability data、RPC command/state 和 API Key 不落 VS Code settings 的边界。 11. P4-11:真实 hunk 级 patch 审批,已完成:`apply_patch` 可选择 hunks,RPC/Core 校验 hunk id 并只应用已批准 hunks,审批事件 payload 已同步 fixture。 @@ -138,7 +138,7 @@ Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐: 1. P5-1:原生 Chat 入口,已完成:贡献 `@prole` Chat Participant,并让 `ProleCoder: Open Chat` 优先打开 VS Code Chat 侧栏;普通 Activity Bar Webview 继续承载 Run List、Context Capsule 和更详细事件视图。 -2. P5-2:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 Select Hunks;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主弹窗。 +2. P5-2:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 hunk 选择边界;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主审批 UI。 3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 @@ -150,7 +150,7 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX,已完成:Sidebar resume 后继续同一 run 发送多轮 turn,Run List 可通过 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder`。 13. P5-13:结构化 provider 配置错误码与恢复动作,已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`,Sidebar/原生 Chat Participant 依据 recoverable action 展示或自动触发 Configure API Key,不再解析后端英文错误消息;run failed payload 也支持同一恢复动作。 -14. P5-14:持续 UX 测试与体验改进占位,未完成:根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 +14. P5-14:持续 UX 测试与体验改进占位,未完成:已补齐进入 run 后的 Sidebar 对话专属视图、内联 approval/delete 确认、过程事件强制默认折叠、Enter 发送 / Shift+Enter 换行;后续继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From fcb982ddaabef5cd8d45d07d381a346421ba1255 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 21:25:40 +0800 Subject: [PATCH 55/84] Refines chat timeline to separate user/assistant messages from work log events --- vscode/extension/src/chatEvents.ts | 54 ++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 7 deletions(-) diff --git a/vscode/extension/src/chatEvents.ts b/vscode/extension/src/chatEvents.ts index c9497bf..490c27a 100644 --- a/vscode/extension/src/chatEvents.ts +++ b/vscode/extension/src/chatEvents.ts @@ -11,7 +11,7 @@ export type ChatTimelineKind = | "run" | "terminal" | "tool" - | "turn"; + | "user"; export type ChatTimelineTone = "danger" | "neutral" | "running" | "success" | "warning"; @@ -34,6 +34,8 @@ export interface ChatTimelineItem { export interface ChatTimelineSnapshot { readonly eventCount: number; readonly items: readonly ChatTimelineItem[]; + readonly visibleItems?: readonly ChatTimelineItem[]; + readonly workItems?: readonly ChatTimelineItem[]; readonly latestRunId?: string; readonly latestStatus?: string; } @@ -80,9 +82,13 @@ export class ChatEventTimeline { } snapshot(): ChatTimelineSnapshot { + const items = this.items.map((item) => ({ ...item })); + const presentation = presentTimelineItems(items); return { eventCount: this.eventCount, - items: this.items.map((item) => ({ ...item })), + items, + visibleItems: presentation.visibleItems, + workItems: presentation.workItems, ...(this.latestRunId === undefined ? {} : { latestRunId: this.latestRunId }), ...(this.latestStatus === undefined ? {} : { latestStatus: this.latestStatus }), }; @@ -159,7 +165,7 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem id: `assistant:${event.runId}:${event.turnId ?? "run"}:${event.seq}`, kind: "assistant", tone: "neutral", - title: "Assistant", + title: "DeepSeek", body: textField(payload, "text") ?? textField(payload, "delta") ?? compactJson(event.payload), detail: `seq ${event.seq}`, }; @@ -175,11 +181,10 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem case "turn.started": return { ...base, - kind: "turn", - tone: "running", - title: "Turn started", + kind: "user", + tone: "neutral", + title: "You", body: textField(payload, "userTask") ?? textField(payload, "prompt") ?? compactJson(event.payload), - defaultCollapsed: true, }; case "context.built": return { @@ -322,6 +327,41 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem } } +export function presentTimelineItems(items: readonly ChatTimelineItem[]): { + readonly visibleItems: readonly ChatTimelineItem[]; + readonly workItems: readonly ChatTimelineItem[]; +} { + const hasAssistantMessage = items.some((item) => item.kind === "assistant"); + const visibleItems: ChatTimelineItem[] = []; + const workItems: ChatTimelineItem[] = []; + + for (const item of items) { + if (isWorkLogItem(item, hasAssistantMessage)) { + workItems.push(item); + } else { + visibleItems.push(item); + } + } + + return { visibleItems, workItems }; +} + +export function isWorkLogItem(item: ChatTimelineItem, hasAssistantMessage = false): boolean { + if (item.kind === "user" || item.kind === "assistant") { + return false; + } + + if (item.type === "run.failed" || item.type === "run.canceled") { + return false; + } + + if (item.type === "run.completed" && !hasAssistantMessage) { + return false; + } + + return true; +} + function assistantKey(event: AgentEventEnvelope): string { return `${event.runId}:${event.turnId ?? ""}`; } From 7e32bd7cab0d366289cc53a6c48b7c687fb970d9 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 21:25:40 +0800 Subject: [PATCH 56/84] Updates chat view rendering to support work log and conversation-only display --- vscode/extension/src/chatView.ts | 230 ++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 2 deletions(-) diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index ff201f8..28c83e4 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -1163,11 +1163,39 @@ function renderChatViewHtml( background: var(--vscode-editorWidget-background); } + .item.user, .item.assistant { border-radius: 6px; + } + + .item.user { + margin-left: 24px; + color: var(--vscode-button-secondaryForeground); + background: var(--vscode-button-secondaryBackground); + border-color: transparent; + } + + .item.assistant { background: var(--vscode-input-background); } + .item.user .meta, + .item.assistant .meta { + display: none; + } + + .item.user .title, + .item.assistant .title { + color: var(--vscode-descriptionForeground); + font-size: 11px; + } + + .item.work-log { + background: transparent; + border-style: dashed; + opacity: 0.9; + } + details.item > summary { display: grid; grid-template-columns: auto minmax(0, 1fr); @@ -1241,6 +1269,45 @@ function renderChatViewHtml( display: none; } + .work-log-body { + display: grid; + gap: 0; + } + + .work-log-row { + display: grid; + gap: 3px; + padding: 6px 0; + border-top: 1px solid var(--vscode-editorWidget-border); + } + + .work-log-row:first-child { + border-top: 0; + padding-top: 0; + } + + .work-log-row-meta { + display: flex; + gap: 6px; + min-width: 0; + color: var(--vscode-descriptionForeground); + font-size: 11px; + } + + .work-log-row-title { + min-width: 0; + overflow-wrap: anywhere; + font-weight: 600; + } + + .work-log-row-body { + color: var(--vscode-descriptionForeground); + font-size: 12px; + line-height: 1.35; + white-space: pre-wrap; + overflow-wrap: anywhere; + } + .approval-host { display: none; padding: 8px 10px 0; @@ -1549,6 +1616,8 @@ function renderChatViewHtml( const initialApproval = ${initialApproval}; const runModes = ${safeScriptJson(CHAT_RUN_MODES)}; const defaultMode = ${safeScriptJson(DEFAULT_CHAT_MODE)}; + const WORK_LOG_RENDER_LIMIT = 80; + const WORK_LOG_STATUS_IGNORED_TYPES = new Set(["run.completed"]); const vscodeApi = acquireVsCodeApi(); const shellRoot = document.querySelector(".shell"); const showRunsButton = document.getElementById("show-runs"); @@ -1674,6 +1743,8 @@ function renderChatViewHtml( function render(snapshot) { currentSnapshot = snapshot && typeof snapshot === "object" ? snapshot : initialSnapshot; const items = Array.isArray(currentSnapshot.items) ? currentSnapshot.items : []; + const visibleItems = timelineVisibleItems(currentSnapshot, items); + const workItems = timelineWorkItems(currentSnapshot, items); statusTitle.textContent = currentSnapshot.latestStatus || "ProleCoder"; statusSubtitle.textContent = currentSnapshot.latestRunId ? currentSnapshot.latestRunId + " - " + currentSnapshot.eventCount + " events" @@ -1681,7 +1752,7 @@ function renderChatViewHtml( eventsRoot.replaceChildren(); syncConversationChrome(); - if (items.length === 0) { + if (visibleItems.length === 0 && workItems.length === 0) { const empty = document.createElement("div"); empty.className = "empty"; empty.textContent = "No run events yet."; @@ -1689,9 +1760,12 @@ function renderChatViewHtml( return; } - for (const item of items) { + for (const item of visibleItems) { eventsRoot.append(renderItem(item)); } + if (workItems.length > 0) { + eventsRoot.append(renderWorkLog(workItems)); + } } function renderRuns(snapshot) { @@ -2261,6 +2335,7 @@ function renderChatViewHtml( promptInput.value = ""; } syncConversationChrome(); + syncWorkLogSummary(); } function renderSubmissionAction(action) { @@ -2322,6 +2397,157 @@ function renderChatViewHtml( return currentSubmission && currentSubmission.busy === true ? "pending" : ""; } + function timelineVisibleItems(snapshot, items) { + if (snapshot && Array.isArray(snapshot.visibleItems)) { + return snapshot.visibleItems; + } + + // Fallback only; chatEvents.presentTimelineItems is the authoritative grouping source. + const hasAssistant = items.some((item) => item && item.kind === "assistant"); + return items.filter((item) => !isWorkLogItem(item, hasAssistant)); + } + + function timelineWorkItems(snapshot, items) { + if (snapshot && Array.isArray(snapshot.workItems)) { + return snapshot.workItems; + } + + // Fallback only; chatEvents.presentTimelineItems is the authoritative grouping source. + const hasAssistant = items.some((item) => item && item.kind === "assistant"); + return items.filter((item) => isWorkLogItem(item, hasAssistant)); + } + + function isWorkLogItem(item, hasAssistant) { + if (!item || typeof item !== "object") { + return true; + } + if (item.kind === "user" || item.kind === "assistant") { + return false; + } + if (item.type === "run.failed" || item.type === "run.canceled") { + return false; + } + if (item.type === "run.completed" && hasAssistant !== true) { + return false; + } + return true; + } + + function renderWorkLog(items) { + const details = document.createElement("details"); + details.className = "item work-log neutral"; + details.open = false; + + const summary = document.createElement("summary"); + const meta = document.createElement("div"); + meta.className = "meta"; + const count = document.createElement("span"); + count.className = "seq"; + count.textContent = items.length + " events"; + const latest = latestWorkItem(items); + const latestType = document.createElement("span"); + latestType.className = "type"; + latestType.textContent = latest && typeof latest.type === "string" ? latest.type : "work"; + meta.append(count, latestType); + + const title = document.createElement("div"); + title.className = "title"; + title.textContent = workLogTitle(items); + summary.append(meta, title); + details.append(summary); + + const body = document.createElement("div"); + body.className = "body work-log-body"; + const displayedItems = workLogDisplayItems(items); + const hiddenCount = items.length - displayedItems.length; + if (hiddenCount > 0) { + body.append(renderWorkLogNotice(hiddenCount)); + } + for (const item of displayedItems) { + body.append(renderWorkLogRow(item)); + } + details.append(body); + return details; + } + + function workLogDisplayItems(items) { + return items.length <= WORK_LOG_RENDER_LIMIT ? items : items.slice(items.length - WORK_LOG_RENDER_LIMIT); + } + + function renderWorkLogNotice(hiddenCount) { + const row = document.createElement("div"); + row.className = "work-log-row neutral"; + const meta = document.createElement("div"); + meta.className = "work-log-row-meta"; + meta.textContent = "work log"; + const title = document.createElement("div"); + title.className = "work-log-row-title"; + title.textContent = hiddenCount + " earlier events hidden"; + row.append(meta, title); + return row; + } + + function renderWorkLogRow(item) { + const row = document.createElement("div"); + row.className = "work-log-row " + (typeof item.tone === "string" ? item.tone : "neutral"); + + const meta = document.createElement("div"); + meta.className = "work-log-row-meta"; + const seq = document.createElement("span"); + seq.textContent = item.seq === item.lastSeq ? "#" + item.seq : "#" + item.seq + "-" + item.lastSeq; + const type = document.createElement("span"); + type.textContent = typeof item.type === "string" ? item.type : "event"; + meta.append(seq, type); + + const title = document.createElement("div"); + title.className = "work-log-row-title"; + title.textContent = typeof item.title === "string" ? item.title : "Work event"; + row.append(meta, title); + + if (typeof item.body === "string" && item.body.length > 0) { + const body = document.createElement("div"); + body.className = "work-log-row-body"; + body.textContent = item.body; + row.append(body); + } + + return row; + } + + function workLogTitle(items) { + const latest = latestWorkItem(items); + const busy = currentSubmission && currentSubmission.busy === true; + if (busy && latest && typeof latest.title === "string" && latest.title.length > 0) { + return "Working: " + latest.title; + } + return "Work log"; + } + + function latestWorkItem(items) { + for (let index = items.length - 1; index >= 0; index -= 1) { + const item = items[index]; + if (shouldUseWorkItemForStatus(item)) { + return item; + } + } + return items.length > 0 ? items[items.length - 1] : undefined; + } + + function shouldUseWorkItemForStatus(item) { + return item && WORK_LOG_STATUS_IGNORED_TYPES.has(item.type) !== true; + } + + function syncWorkLogSummary() { + const title = document.querySelector(".item.work-log > summary .title"); + if (!title) { + return; + } + + const snapshot = currentSnapshot && typeof currentSnapshot === "object" ? currentSnapshot : initialSnapshot; + const items = Array.isArray(snapshot.items) ? snapshot.items : []; + title.textContent = workLogTitle(timelineWorkItems(snapshot, items)); + } + function renderItem(item) { const collapsed = item.defaultCollapsed === true; const article = document.createElement(collapsed ? "details" : "article"); From 2519bd79c339677e599d03ae494bc30f8cd3ade3 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 21:25:40 +0800 Subject: [PATCH 57/84] Updates tests to reflect new chat timeline kind and title values --- .../extension/test/automaticContext.test.ts | 8 +-- vscode/extension/test/chatEvents.test.ts | 53 ++++++++++++++++++- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/vscode/extension/test/automaticContext.test.ts b/vscode/extension/test/automaticContext.test.ts index 382558a..82cd2a1 100644 --- a/vscode/extension/test/automaticContext.test.ts +++ b/vscode/extension/test/automaticContext.test.ts @@ -61,9 +61,9 @@ test("automatic context can be built from sidebar timeline snapshots", () => { time: "1970-01-01T00:00:00.000Z", type: "turn.started", runId: "run_1", - kind: "turn", - tone: "running", - title: "Turn started", + kind: "user", + tone: "neutral", + title: "You", body: "Fix README", }, { @@ -75,7 +75,7 @@ test("automatic context can be built from sidebar timeline snapshots", () => { runId: "run_1", kind: "assistant", tone: "neutral", - title: "Assistant", + title: "DeepSeek", body: "README updated.", }, ], diff --git a/vscode/extension/test/chatEvents.test.ts b/vscode/extension/test/chatEvents.test.ts index 0ae25fa..fe85c58 100644 --- a/vscode/extension/test/chatEvents.test.ts +++ b/vscode/extension/test/chatEvents.test.ts @@ -1,7 +1,12 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { ChatEventTimeline, createTimelineItem } from "../src/chatEvents.js"; +import { + ChatEventTimeline, + createTimelineItem, + isWorkLogItem, + presentTimelineItems, +} from "../src/chatEvents.js"; import type { AgentEventEnvelope } from "../src/rpcServer.js"; test("chat timeline merges assistant delta events for the same turn", () => { @@ -31,6 +36,52 @@ test("chat timeline keeps assistant delta events for different turns separate", ); }); +test("chat timeline presents user and DeepSeek messages while folding work events", () => { + const timeline = new ChatEventTimeline(); + + timeline.append(agentEvent(1, "run.started", { mode: "ask" })); + timeline.append(agentEvent(2, "turn.started", { userTask: "Read the code" })); + timeline.append(agentEvent(3, "context.built", { inputTokens: 123 })); + timeline.append(agentEvent(4, "assistant.delta", { text: "I read it." })); + timeline.append(agentEvent(5, "tool.requested", { name: "read_file" })); + const snapshot = timeline.append(agentEvent(6, "run.completed", { summary: "I read it." })); + + assert.deepEqual( + snapshot.visibleItems?.map((item) => item.title), + ["You", "DeepSeek"], + ); + assert.deepEqual( + snapshot.workItems?.map((item) => item.type), + ["run.started", "context.built", "tool.requested", "run.completed"], + ); + assert.equal(snapshot.items.find((item) => item.type === "turn.started")?.kind, "user"); +}); + +test("chat timeline keeps completed summary visible when no assistant message exists", () => { + const completed = createTimelineItem(agentEvent(1, "run.completed", { summary: "done" })); + const presentation = presentTimelineItems([completed]); + + assert.deepEqual( + presentation.visibleItems.map((item) => item.type), + ["run.completed"], + ); + assert.equal(presentation.workItems.length, 0); +}); + +test("chat timeline keeps failure and cancellation visible outside work log", () => { + const failure = createTimelineItem(agentEvent(1, "run.failed", { message: "boom" })); + const canceled = createTimelineItem(agentEvent(2, "run.canceled", { reason: "stop" })); + const presentation = presentTimelineItems([failure, canceled]); + + assert.equal(isWorkLogItem(failure, true), false); + assert.equal(isWorkLogItem(canceled, true), false); + assert.deepEqual( + presentation.visibleItems.map((item) => item.type), + ["run.failed", "run.canceled"], + ); + assert.equal(presentation.workItems.length, 0); +}); + test("chat timeline renders tool lifecycle and terminal events", () => { const timeline = new ChatEventTimeline(); From e53e718afc3f97e9c74fb5a56daadc990337116f Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 21:25:40 +0800 Subject: [PATCH 58/84] Updates documentation to describe new conversation-only display and Work log --- README.md | 4 ++-- docs/phase-tasks.md | 4 ++-- docs/roadmap.md | 4 ++-- docs/testing.md | 4 ++-- docs/vscode-extension.md | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index e138420..01b433d 100644 --- a/README.md +++ b/README.md @@ -568,7 +568,7 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认和 Enter/Shift+Enter 输入回归,但持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和 Enter/Shift+Enter 输入回归,但持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 @@ -707,7 +707,7 @@ extension.ts - [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 - [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX:`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar resume 后继续同一会话发送多轮 turn;Run List 支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder` 便于 debug。 - [x] P5-13:结构化 provider 配置错误码与恢复动作:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回结构化 `data.recoverableAction`,Sidebar/原生 Chat 依据恢复动作展示或触发 Configure API Key 入口,不再依赖后端英文错误消息;run failed payload 也可携带同一恢复动作。 -- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口;已根据真实试用补齐 Sidebar 对话专属视图、内联 approval/delete 确认、过程事件强制默认折叠以及 Enter 发送 / Shift+Enter 换行,后续继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 +- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口;已根据真实试用补齐 Sidebar 对话专属视图、内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示以及 Enter 发送 / Shift+Enter 换行,后续继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 验收标准: diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 51e420a..1d792d6 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -109,7 +109,7 @@ | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -126,7 +126,7 @@ | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | | [x] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | -| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已补齐:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板,approval 和 run delete 改为就地确认 UI,tool/provider/request 等过程事件强制默认折叠,输入框 Enter 发送且 Shift+Enter 换行;已登记后续 E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片和输入快捷键。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | +| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已补齐:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板,approval 和 run delete 改为就地确认 UI,默认对话流只显示用户消息与 DeepSeek 回复,tool/provider/context/run completed 等过程事件收敛到默认折叠的 Work log,Work log 摘要只暴露当前工作状态,输入框 Enter 发送且 Shift+Enter 换行;已登记后续 E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片、输入快捷键和 Work log 折叠展示。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 8345790..81a841c 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,7 +105,7 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: @@ -121,7 +121,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 - P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 -- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归;已补齐 Sidebar 对话专属视图、webview 内联 approval/delete 确认、过程事件默认折叠和 Enter / Shift+Enter 输入行为。 +- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归;已补齐 Sidebar 对话专属视图、webview 内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、默认折叠 Work log 和 Enter / Shift+Enter 输入行为。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 diff --git a/docs/testing.md b/docs/testing.md index 37b45a4..521ec45 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -87,10 +87,10 @@ Phase 5 P5-6 到 P5-13 的 API key/model、Git 工作流、Sidebar 连续会话 - `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、trash 按钮删除非 active key、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 - `rpcServer.test.ts` 覆盖 RPC child env 注入、key 轮换后重启使用新 env,以及 typed `agent.deleteRun` request;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 -- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、terminal summary 保持展开;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 +- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、用户消息/DeepSeek 回复默认可见、过程事件收敛到 Work log,以及无 assistant 时 `run.completed` 摘要仍可见;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 -P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、过程事件默认折叠和 Enter / Shift+Enter 输入行为。 +P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 折叠展示和 Enter / Shift+Enter 输入行为。 ## 新增测试的协作要求 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 7d26049..deb5042 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认、默认对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -45,7 +45,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server - 在 Activity Bar 暴露 ProleCoder view container 和 Chat view。 - 通过 `RpcServerManager.onEvent()` 订阅 live `agent.event`。 -- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item;tool/provider/request 等过程事件默认折叠,assistant 文本和 terminal summary 默认可见。 +- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item;默认对话流只显示 `You` 用户消息、`DeepSeek` 回复和失败/取消错误,tool/provider/context/run completed 等过程事件收敛进默认折叠的 Work log,Work log 摘要在运行中只暴露当前工作状态。 - 同一 run/turn 的连续 `assistant.delta` 会合并为一条 assistant 消息,避免流式输出刷屏。 - 提供 prompt 输入、mode 选择和运行中 Cancel 按钮;通过 Webview `submitTurn` 消息调用 typed `RpcServerManager.sendTurn()`,发送时把 Problems 快照转换为 diagnostic attachments,并按协议 attachment 上限优先保留 error;如果当前已 resume/发送过 run,会复用该 `runId` 继续同一会话并由后端递增 `turn_N`;accepted 后等待同一 run 的 terminal event 收口输入状态,Cancel 会调用 typed `RpcServerManager.cancel()`。 - Run List 支持 `agent.listRuns` / `agent.resume` / `agent.deleteRun`,可回放历史 run、继续多轮会话,也可删除 inactive run。 @@ -150,7 +150,7 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX,已完成:Sidebar resume 后继续同一 run 发送多轮 turn,Run List 可通过 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder`。 13. P5-13:结构化 provider 配置错误码与恢复动作,已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`,Sidebar/原生 Chat Participant 依据 recoverable action 展示或自动触发 Configure API Key,不再解析后端英文错误消息;run failed payload 也支持同一恢复动作。 -14. P5-14:持续 UX 测试与体验改进占位,未完成:已补齐进入 run 后的 Sidebar 对话专属视图、内联 approval/delete 确认、过程事件强制默认折叠、Enter 发送 / Shift+Enter 换行;后续继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 +14. P5-14:持续 UX 测试与体验改进占位,未完成:已补齐进入 run 后的 Sidebar 对话专属视图、内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、默认折叠 Work log、Enter 发送 / Shift+Enter 换行;后续继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From a09d6fe79ee97f46e0f835bfd2a95ce4db78dfbf Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Tue, 2 Jun 2026 21:25:41 +0800 Subject: [PATCH 59/84] Adds line_has_agent_event helper to CLI tests for eventBatch support --- crates/cli/src/lib.rs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 3726e2b..956f80d 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -2039,9 +2039,9 @@ mod tests { ); output.wait_for_line( |line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["runId"] == "run_cli_rpc" + line_has_agent_event(line, |event| { + event["type"] == "run.completed" && event["runId"] == "run_cli_rpc" + }) }, Duration::from_secs(30), ); @@ -2054,10 +2054,11 @@ mod tests { assert_eq!(lines[1]["id"], "turn_1"); assert_eq!(lines[1]["result"]["accepted"], true); assert!(lines.iter().any(|line| { - line["method"] == "agent.event" - && line["params"]["type"] == "run.completed" - && line["params"]["payload"]["summary"] - == "Fixture provider completed without tool calls." + line_has_agent_event(line, |event| { + event["type"] == "run.completed" + && event["payload"]["summary"] + == "Fixture provider completed without tool calls." + }) })); let store = RunLogStore::new(workspace.path()).expect("run log store should open"); @@ -2245,6 +2246,20 @@ mod tests { } } + fn line_has_agent_event(line: &Value, predicate: impl Fn(&Value) -> bool) -> bool { + if line["method"] == "agent.event" { + return predicate(&line["params"]); + } + + if line["method"] == "agent.eventBatch" + && let Some(events) = line["params"]["events"].as_array() + { + return events.iter().any(predicate); + } + + false + } + #[test] fn deepseek_stream_wrapper_aggregates_deltas() { let stream = Box::pin(stream::iter([ From 998be64e3ab4b97815955cae0cd81951b9c6b991 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 06:44:48 +0800 Subject: [PATCH 60/84] Adds diagnostic file support for malformed tool arguments in Run Log --- crates/agent-core/src/run_log.rs | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/crates/agent-core/src/run_log.rs b/crates/agent-core/src/run_log.rs index 3b3b7e0..60e24d0 100644 --- a/crates/agent-core/src/run_log.rs +++ b/crates/agent-core/src/run_log.rs @@ -219,6 +219,15 @@ pub trait RunLogWriter { turn_id: Option, payload: Value, ) -> Result; + + fn write_diagnostic_file( + &mut self, + relative_path: &Path, + contents: &str, + ) -> Result, RunLogError> { + let _ = (relative_path, contents); + Ok(None) + } } /// `RunLog` is a single-writer append handle. @@ -295,6 +304,14 @@ impl RunLog { pub fn load(&self) -> Result, RunLogError> { read_events(&self.run_id, &self.events_path) } + + pub fn write_run_diagnostic_file( + &mut self, + relative_path: &Path, + contents: &str, + ) -> Result { + write_run_diagnostic_file(&self.events_path, relative_path, contents) + } } impl RunLogWriter for RunLog { @@ -310,6 +327,15 @@ impl RunLogWriter for RunLog { ) -> Result { self.append(event_type, turn_id, payload) } + + fn write_diagnostic_file( + &mut self, + relative_path: &Path, + contents: &str, + ) -> Result, RunLogError> { + self.write_run_diagnostic_file(relative_path, contents) + .map(Some) + } } #[derive(Debug, Clone)] @@ -359,6 +385,15 @@ impl SerializedRunLog { self.lock()?.load() } + pub fn write_run_diagnostic_file( + &self, + relative_path: &Path, + contents: &str, + ) -> Result { + self.lock()? + .write_run_diagnostic_file(relative_path, contents) + } + fn lock(&self) -> Result, RunLogError> { self.inner .lock() @@ -381,6 +416,14 @@ impl RunLogWriter for SerializedRunLog { ) -> Result { self.append(event_type, turn_id, payload) } + + fn write_diagnostic_file( + &mut self, + relative_path: &Path, + contents: &str, + ) -> Result, RunLogError> { + SerializedRunLog::write_run_diagnostic_file(self, relative_path, contents).map(Some) + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -594,6 +637,49 @@ fn append_event(path: &Path, event: &RunLogEvent) -> Result<(), RunLogError> { Ok(()) } +fn write_run_diagnostic_file( + events_path: &Path, + relative_path: &Path, + contents: &str, +) -> Result { + let relative_path = normalize_workspace_relative_path(relative_path)?; + let run_dir = events_path + .parent() + .ok_or_else(|| RunLogError::InvalidStatePath { + path: events_path.to_path_buf(), + })?; + let path = run_dir.join(relative_path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|source| RunLogError::Io { + path: parent.to_path_buf(), + source, + })?; + } + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&path) + .map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + file.write_all(contents.as_bytes()) + .map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + file.write_all(b"\n").map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + file.flush().map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + Ok(path) +} + fn write_summary(path: &Path, summary: &RunSummary) -> Result<(), RunLogError> { let mut file = OpenOptions::new() .write(true) @@ -1084,6 +1170,32 @@ mod tests { assert_eq!(payload["stdout"], format!("visible {REDACTED_VALUE}")); } + #[test] + fn run_log_writes_diagnostic_files_inside_run_directory() { + let workspace = TestWorkspace::new("run-log"); + let store = RunLogStore::new(workspace.path()).expect("store should open"); + let mut run = store + .create_run("run_diagnostic") + .expect("run should be created"); + + let path = run + .write_run_diagnostic_file( + std::path::Path::new("diagnostics/invalid-tool-arguments.json"), + "hello diagnostic", + ) + .expect("diagnostic file should be written"); + + assert!(path.starts_with(store.runs_dir().join("run_diagnostic"))); + assert_eq!( + fs::read_to_string(path).expect("diagnostic file should be readable"), + "hello diagnostic\n" + ); + assert!(matches!( + run.write_run_diagnostic_file(std::path::Path::new("../outside.txt"), "bad"), + Err(RunLogError::InvalidStatePath { .. }) + )); + } + #[test] fn run_log_truncates_large_payloads_and_records_boundaries() { let workspace = TestWorkspace::new("run-log"); From f6423b1143bf8b992f2e5aaea839bdfe5322ea0d Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 06:44:48 +0800 Subject: [PATCH 61/84] Writes diagnostic file for invalid tool arguments in Turn Loop --- crates/agent-core/src/turn_loop.rs | 221 +++++++++++++++++++++++++++-- 1 file changed, 209 insertions(+), 12 deletions(-) diff --git a/crates/agent-core/src/turn_loop.rs b/crates/agent-core/src/turn_loop.rs index 32e6035..87c64c8 100644 --- a/crates/agent-core/src/turn_loop.rs +++ b/crates/agent-core/src/turn_loop.rs @@ -1,4 +1,10 @@ -use std::{collections::HashSet, future::Future, path::Path, pin::Pin, time::Instant}; +use std::{ + collections::HashSet, + future::Future, + path::{Path, PathBuf}, + pin::Pin, + time::Instant, +}; use futures_util::{Stream, StreamExt}; use serde::{Serialize, de::DeserializeOwned}; @@ -34,6 +40,7 @@ const DEFAULT_MAX_ATTACHMENTS: usize = 32; const DEFAULT_MAX_ATTACHMENT_BYTES: u64 = 256 * 1024; const SHELL_APPROVAL_OUTPUT_SUMMARY_MAX_LINES: usize = 8; const SHELL_APPROVAL_OUTPUT_SUMMARY_MAX_BYTES: usize = 2 * 1024; +const TOOL_ARGUMENT_DIAGNOSTIC_MAX_BYTES: usize = 128 * 1024; const FINAL_RESPONSE_SUMMARY_INSTRUCTION: &str = concat!( "When the task is complete, make the final assistant message a concise work summary for the user. ", "Mention what changed, important files, verification or tests, and any blockers. ", @@ -125,7 +132,9 @@ where AgentTurnLoopError::RunLog(_) | AgentTurnLoopError::EventSink(_) ) { - let (event_type, payload) = terminal_error_event(error); + let diagnostic_file = + write_invalid_tool_arguments_diagnostic_file(run_log, turn_id.as_str(), error); + let (event_type, payload) = terminal_error_event(error, diagnostic_file.as_deref()); let append_result = append_turn_event(run_log, event_sink, event_type, Some(turn_id), payload); if let Err(append_error) = append_result { @@ -1627,6 +1636,7 @@ pub enum AgentTurnLoopError { tool_call_id: String, name: String, source: serde_json::Error, + raw_arguments: Option, }, #[error("tool call `{tool_call_id}` for `{name}` failed JSON Schema validation: {source}")] InvalidToolArgumentSchema { @@ -1761,6 +1771,7 @@ fn parse_tool_arguments_value(tool_call: &ChatToolCall) -> Result( tool_call_id: tool_call.id.clone(), name: tool_call.function.name.clone(), source, + raw_arguments: None, } }) } @@ -2123,7 +2135,112 @@ fn provider_error_or_canceled( } } -fn terminal_error_event(error: &AgentTurnLoopError) -> (&'static str, Value) { +fn write_invalid_tool_arguments_diagnostic_file( + run_log: &mut (impl RunLogWriter + ?Sized), + turn_id: &str, + error: &AgentTurnLoopError, +) -> Option { + let run_id = run_log.run_id().to_owned(); + let (relative_path, payload) = invalid_tool_arguments_diagnostic(&run_id, turn_id, error)?; + let contents = match serde_json::to_string_pretty(&payload) { + Ok(contents) => contents, + Err(source) => { + eprintln!( + "failed to serialize diagnostic for `{}`: {source}", + error.code() + ); + return None; + } + }; + + match run_log.write_diagnostic_file(&relative_path, &contents) { + Ok(Some(path)) => Some(path.display().to_string()), + Ok(None) => None, + Err(source) => { + eprintln!( + "failed to write diagnostic for `{}`: {source}", + error.code() + ); + None + } + } +} + +fn invalid_tool_arguments_diagnostic( + run_id: &str, + turn_id: &str, + error: &AgentTurnLoopError, +) -> Option<(PathBuf, Value)> { + let AgentTurnLoopError::InvalidToolArguments { + tool_call_id, + name, + source, + raw_arguments: Some(raw_arguments), + } = error + else { + return None; + }; + let (raw_arguments, raw_arguments_truncated) = + diagnostic_raw_arguments(redact_text(raw_arguments)); + let file_name = diagnostic_file_name("invalid-tool-arguments", tool_call_id); + + Some(( + Path::new("diagnostics").join(file_name), + json!({ + "type": "invalid_tool_arguments", + "code": error.code(), + "message": error.to_string(), + "runId": run_id, + "turnId": turn_id, + "toolCallId": tool_call_id, + "toolName": name, + "jsonError": source.to_string(), + "rawArguments": raw_arguments, + "rawArgumentsTruncated": raw_arguments_truncated, + }), + )) +} + +fn diagnostic_raw_arguments(text: String) -> (String, bool) { + if text.len() <= TOOL_ARGUMENT_DIAGNOSTIC_MAX_BYTES { + return (text, false); + } + + let mut end = TOOL_ARGUMENT_DIAGNOSTIC_MAX_BYTES; + while !text.is_char_boundary(end) { + end -= 1; + } + let omitted = text.len() - end; + ( + format!("{}\n[truncated {omitted} bytes]", &text[..end]), + true, + ) +} + +fn diagnostic_file_name(prefix: &str, id: &str) -> String { + let mut safe = id + .chars() + .map(|ch| { + if ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.') { + ch + } else { + '_' + } + }) + .take(80) + .collect::(); + if safe.is_empty() { + safe.push_str("tool_call"); + } + let hash = crate::hashing::sha256_hex(id.as_bytes()); + let short_hash = hash.get(..12).unwrap_or(hash.as_str()); + format!("{prefix}-{safe}-{short_hash}.json") +} + +fn terminal_error_event( + error: &AgentTurnLoopError, + diagnostic_file: Option<&str>, +) -> (&'static str, Value) { match error { AgentTurnLoopError::ApprovalCanceled { approval_id, @@ -2152,19 +2269,21 @@ fn terminal_error_event(error: &AgentTurnLoopError) -> (&'static str, Value) { "reason": reason, }), ), - _ => ( - "run.failed", - json!({ - "code": error.code(), - "message": error.to_string(), - }), - ), + _ => { + let mut payload = Map::new(); + payload.insert("code".to_owned(), json!(error.code())); + payload.insert("message".to_owned(), json!(error.to_string())); + if let Some(diagnostic_file) = diagnostic_file { + payload.insert("diagnosticFile".to_owned(), json!(diagnostic_file)); + } + ("run.failed", Value::Object(payload)) + } } } #[cfg(test)] mod tests { - use std::{collections::VecDeque, fs}; + use std::{collections::VecDeque, fs, path::PathBuf}; use futures_util::stream; use serde_json::json; @@ -2173,7 +2292,7 @@ mod tests { context::ContextItem, provider::deepseek_api::ChatToolCall, reasoning::ReasoningContentMode, - run_log::{RunLogEvent, RunLogStore}, + run_log::{REDACTED_VALUE, RunLogEvent, RunLogStore}, test_helpers::TestWorkspace, }; @@ -2187,6 +2306,18 @@ mod tests { TurnProviderStreamingSummary, TurnProviderUsage, turn_provider_response_stream, }; + #[test] + fn diagnostic_file_names_include_hash_for_sanitized_collisions() { + let left = super::diagnostic_file_name("invalid-tool-arguments", "call/a"); + let right = super::diagnostic_file_name("invalid-tool-arguments", "call:a"); + + assert!(left.starts_with("invalid-tool-arguments-call_a-")); + assert!(right.starts_with("invalid-tool-arguments-call_a-")); + assert_ne!(left, right); + assert!(left.ends_with(".json")); + assert!(right.ends_with(".json")); + } + #[tokio::test] async fn turn_loop_runs_read_tool_and_continues_to_final_answer() { let workspace = TestWorkspace::new("turn-loop"); @@ -2779,6 +2910,72 @@ mod tests { })); } + #[tokio::test] + async fn turn_loop_writes_diagnostic_file_for_malformed_tool_arguments() { + let workspace = TestWorkspace::new("turn-loop"); + workspace.write("README.md", "hello\n"); + let store = RunLogStore::new(workspace.path()).expect("run log store should open"); + let mut run = store + .create_run("run_turn_malformed_tool_args") + .expect("run should be created"); + let raw_secret = format!("sk-{}", "this-malformed-secret-123"); + let malformed_arguments = + format!("{{\"unifiedDiff\":\"--- a/README.md\\n+++ b/README.md\\n{raw_secret}"); + let provider = ScriptedProvider::new(vec![TurnProviderResponse::tool_calls( + None, + Some("I should edit the README.".to_owned()), + vec![ChatToolCall::function( + "call_bad_json", + "apply_patch", + malformed_arguments, + )], + )]); + let mut loop_runner = + AgentTurnLoop::new(workspace.path(), provider).expect("turn loop should initialize"); + + let error = loop_runner + .run_turn(AgentTurnInput::new("turn_1", "Edit README"), &mut run) + .await + .expect_err("malformed tool JSON should fail the turn"); + + assert!(matches!( + error, + AgentTurnLoopError::InvalidToolArguments { + raw_arguments: Some(_), + .. + } + )); + let events = store + .load_run("run_turn_malformed_tool_args") + .expect("events should load"); + assert!( + !events + .iter() + .any(|event| event.event_type == "tool.requested") + ); + let failed = events + .iter() + .find(|event| { + event.event_type == "run.failed" + && event.payload["code"] == "E_INVALID_TOOL_ARGUMENTS" + }) + .expect("run.failed should be recorded"); + let diagnostic_file = failed.payload["diagnosticFile"] + .as_str() + .expect("run.failed should include diagnosticFile"); + let diagnostic_path = PathBuf::from(diagnostic_file); + assert!(diagnostic_path.is_file()); + + let contents = + fs::read_to_string(&diagnostic_path).expect("diagnostic file should be readable"); + assert!(contents.contains("invalid_tool_arguments")); + assert!(contents.contains("call_bad_json")); + assert!(contents.contains("apply_patch")); + assert!(contents.contains("unifiedDiff")); + assert!(!contents.contains(&raw_secret)); + assert!(contents.contains(REDACTED_VALUE)); + } + #[tokio::test] async fn turn_loop_executes_approved_patch_and_tracks_changed_files() { let workspace = TestWorkspace::new("turn-loop"); From 7f761672de8c650b1765b435fc9a37ac33068c38 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 06:44:48 +0800 Subject: [PATCH 62/84] Documents diagnostic file support in protocol and design docs --- docs/json-rpc-protocol.md | 5 ++++- docs/run-log.md | 6 +++++- docs/tool-system.md | 1 + docs/turn-loop.md | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/json-rpc-protocol.md b/docs/json-rpc-protocol.md index 638b904..5bb3b4f 100644 --- a/docs/json-rpc-protocol.md +++ b/docs/json-rpc-protocol.md @@ -557,9 +557,12 @@ interface RunFailed { code: string; message: string; details?: unknown; + diagnosticFile?: string; } ``` +当 tool-call `function.arguments` 本身不是合法 JSON 时,Agent Core 会把脱敏后的原始累计 arguments 写入当前 run 目录下的诊断文件,并在 `diagnosticFile` 返回该本地文件路径。该字段只用于本地排查,不要求前端把文件内容重新发送给模型。 + ### `run.canceled` ```ts @@ -900,7 +903,7 @@ JSON-RPC 标准错误保留标准语义。项目特定错误使用 `-32000` 到 | -32002 | `E_WORKSPACE_UNTRUSTED` | 当前 workspace 未信任,请求操作被禁用。 | | -32003 | `E_RUN_NOT_FOUND` | 请求的 run 不存在于本地状态。 | | -32004 | `E_RUN_ALREADY_ACTIVE` | 已存在冲突的 active run。 | -| -32010 | `E_INVALID_TOOL_ARGUMENTS` | tool-call 参数未通过 schema 校验。 | +| -32010 | `E_INVALID_TOOL_ARGUMENTS` | tool-call 参数不是合法 JSON,或未通过 schema 校验。 | | -32011 | `E_APPROVAL_NOT_FOUND` | approval id 未知、已过期或已使用。 | | -32012 | `E_APPROVAL_DENIED` | 审批被拒绝,操作无法继续。 | | -32020 | `E_CONTEXT_BUDGET_EXCEEDED` | 必需上下文无法放入配置的预算。 | diff --git a/docs/run-log.md b/docs/run-log.md index a81915e..7fc821b 100644 --- a/docs/run-log.md +++ b/docs/run-log.md @@ -19,6 +19,7 @@ Run Log 是 Agent Core 的本地审计记录。它记录一次 run 中发生的 ```text /.prole-coder/runs//events.jsonl /.prole-coder/runs//summary.json +/.prole-coder/runs//diagnostics/*.json ``` `.prole-coder/` 已在 `.gitignore` 中排除,run log 不应进入 Git 仓库。 @@ -47,7 +48,9 @@ crates/agent-core/src/run_log.rs `SerializedRunLog` 用于 RPC 等跨线程场景。它把同一个 `RunLog` 放入 `Mutex`,所有 clone 共享同一个 `next_seq` 和文件句柄状态;每次 append 都先拿锁,写入完成并推进 `seq` 后释放。`load` 也走同一把锁,避免 active run 正在写入时,`agent.resume` 从磁盘读到半条事件或不一致的序列。 -Run Log 本身只负责 append/load 串行化,不直接管理 stdout。Phase 3 的 RPC live event queue 建在 `TurnEventSink` 之上:事件先成功追加到 Run Log,再投递给 request loop 的单 writer 输出为 `agent.event` notification。因此前端看到的 live notification 与后续 `agent.resume` 回放共享同一组 `seq` 和 payload。 +Run Log 本身负责 append/load 串行化,并允许 Turn Loop 在同一 run 目录下写入受控诊断文件。Phase 3 的 RPC live event queue 建在 `TurnEventSink` 之上:事件先成功追加到 Run Log,再投递给 request loop 的单 writer 输出为 `agent.event` notification。因此前端看到的 live notification 与后续 `agent.resume` 回放共享同一组 `seq` 和 payload。 + +当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments、JSON 解析错误和 run/turn/tool metadata;对应 `run.failed` payload 会带 `diagnosticFile` 路径,便于 VS Code Output 或 Sidebar failure card 直接定位。文件名中的短哈希来自原始 tool call id,用于避免 sanitize 或截断后的名称碰撞。诊断文件继续留在 `.prole-coder/`,不应上传或同步。 ## Summary Metadata @@ -110,6 +113,7 @@ Run Log 本身只负责 append/load 串行化,不直接管理 stdout。Phase 3 - 拒绝不安全的 run id 和 state dir。 - 读取时发现序列缺口会失败。 - 写入前脱敏敏感字段和明显密钥片段。 +- malformed tool-call arguments 会写入 run-scoped 诊断文件,并在 `run.failed.diagnosticFile` 暴露本地路径。 - 超长字符串和数组会被截断,并记录 `runLogTruncation` 元数据。 - `SerializedRunLog` 多线程 clone 并发追加时,仍生成连续 `seq`,并可被重新打开为正确的下一条序号。 - summary metadata 随事件追加更新,并可按最近更新时间列出。 diff --git a/docs/tool-system.md b/docs/tool-system.md index 2afdc3b..ef2cf47 100644 --- a/docs/tool-system.md +++ b/docs/tool-system.md @@ -358,6 +358,7 @@ Schema 校验不能只作为 typed deserialization 失败后的补救,因为 R - 当前实现只支持受限 unified diff;后续需要支持更完整的 git patch 语法,包括 rename、copy、mode change 和更严格的 no-newline 语义。 - 已增加 VS Code patch 预览和 `apply_patch` hunk 级审批;后续继续增强冲突诊断、失败时的精确 hunk mismatch 信息,以及 rename/copy/mode change 等更完整 patch 语法下的审批边界。 +- 为大 patch / 大文件写入设计分块式工具参数协议:provider 可以流式产生 chunk,本地写入 run-scoped 临时文件,最后通过 commit/apply 调用统一进入 schema 校验、路径安全、审批和 hunk 边界;不要在 chunk 过程中边接收边执行写入。 - 用修改前快照生成 reverse patch,并在 run log 中保存 patch id、审批 id 和可审计回滚信息。 - 如果需要抵抗磁盘写入中途失败,应进一步引入临时文件、原子替换或备份恢复机制;当前 staging 主要保证解析和 hunk 校验失败不会产生半应用 patch。 - 明确二进制文件和生成文件策略,避免文本 patch 意外改写不可审计内容。 diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 8ab84f8..574cec6 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -72,7 +72,7 @@ provider stream 中的 content delta 会立即写入 `assistant.delta`,payload `Completed.content` 是最终 assistant 消息文本的权威来源,用于 `run.completed.summary` 或 assistant tool-call replay。`assistant.delta` 是展示和 run log 增量事件,不反向推断最终文本;当 provider 已经发送过可见 content delta 时,Turn Loop 不会在 `Completed` 时重复写一份完整 `assistant.delta`。因此 reasoning delta 不进入用户可见 summary,tool call 前的可见文本如果存在,应由 provider 同时保留在最终 `Completed.content` 中。 -DeepSeek streaming tool call delta 在 CLI provider wrapper 内通过 `ChatToolCallAccumulator` 拼装为完整 `ChatToolCall` 后才进入 `Completed.tool_calls`。Turn Loop 不直接处理 provider 私有 delta 形态,只要求 provider 在 `Completed` 中提供完整、可校验、可执行的工具调用列表。 +DeepSeek streaming tool call delta 在 CLI provider wrapper 内通过 `ChatToolCallAccumulator` 拼装为完整 `ChatToolCall` 后才进入 `Completed.tool_calls`。Turn Loop 不直接处理 provider 私有 delta 形态,只要求 provider 在 `Completed` 中提供完整、可校验、可执行的工具调用列表。如果累计后的 `function.arguments` 不是合法 JSON,Turn Loop 会以 `E_INVALID_TOOL_ARGUMENTS` 失败,并把脱敏后的累计 arguments 写入当前 run 的 `diagnostics/invalid-tool-arguments--.json`,同时在 `run.failed.diagnosticFile` 暴露该本地路径。 `run.started` 记录规范化后的 workspace root,用于本地审计和前端展示当前 run 绑定的工作区。该路径只应进入本地 run log 和本机前端事件流,不应被上传到公开仓库或远程日志。 @@ -105,7 +105,7 @@ RPC active run 的 Run Log 使用 `SerializedRunLog`:后台 Turn Loop worker - Turn Loop 每次成功追加 Run Log 事件后,会把同一条事件交给 `TurnEventSink`,sink 看到的事件序列与本地 `events.jsonl` 一致。 - `SerializedRunLog` 并发 append 测试验证多个 clone 同时写同一 run 时仍生成连续 `seq`。 - DeepSeek wrapper 能把 streaming tool call delta 拼成完整工具调用,并在缺少必要 metadata 时失败。 -- tool call arguments 会先按工具注册表 JSON Schema 校验,再进入 typed deserialization;未知字段和错误类型会返回 `E_INVALID_TOOL_ARGUMENTS`。 +- tool call arguments 会先解析为 JSON,再按工具注册表 JSON Schema 校验,最后进入 typed deserialization;malformed JSON、未知字段和错误类型会返回 `E_INVALID_TOOL_ARGUMENTS`,malformed JSON 还会写入脱敏诊断文件。 这些测试验证的是模块集成骨架,不需要真实 DeepSeek API Key,也不会联网。真实 tool call delta 形态由 `live_streaming_tool_call_accumulator_smoke_test` 作为手动 opt-in live test 验收。 From 6a802d42ffa403b4b65564d54d4efb1a5c0f0831 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 06:44:48 +0800 Subject: [PATCH 63/84] Updates VS Code extension to display diagnostic file in failure events --- vscode/extension/src/chatEvents.ts | 6 +++++- vscode/extension/test/chatEvents.test.ts | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/vscode/extension/src/chatEvents.ts b/vscode/extension/src/chatEvents.ts index 490c27a..9dccfeb 100644 --- a/vscode/extension/src/chatEvents.ts +++ b/vscode/extension/src/chatEvents.ts @@ -305,7 +305,11 @@ export function createTimelineItem(event: AgentEventEnvelope): ChatTimelineItem kind: "terminal", tone: "danger", title: "Run failed", - body: joinParts([label("Code", textField(payload, "code")), textField(payload, "message")]), + body: joinParts([ + label("Code", textField(payload, "code")), + textField(payload, "message"), + label("Diagnostic file", textField(payload, "diagnosticFile")), + ]), }; case "run.canceled": return { diff --git a/vscode/extension/test/chatEvents.test.ts b/vscode/extension/test/chatEvents.test.ts index fe85c58..f808e73 100644 --- a/vscode/extension/test/chatEvents.test.ts +++ b/vscode/extension/test/chatEvents.test.ts @@ -133,6 +133,7 @@ test("chat timeline renders approval and failure events with warning or danger t agentEvent(2, "run.failed", { code: "E_INVALID_TOOL_ARGUMENTS", message: "invalid tool call", + diagnosticFile: "C:\\workspace\\.prole-coder\\runs\\run_1\\diagnostics\\invalid-tool-arguments-call_1.json", }), ); @@ -143,6 +144,7 @@ test("chat timeline renders approval and failure events with warning or danger t assert.equal(failure.kind, "terminal"); assert.equal(failure.tone, "danger"); assert.ok(failure.body?.includes("invalid tool call")); + assert.ok(failure.body?.includes("Diagnostic file:")); }); test("chat timeline trims old items while preserving event count", () => { From 4b140ab35a56fad1c8e4dee83c582b43700ec7b1 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 06:44:48 +0800 Subject: [PATCH 64/84] Refactors README and docs to use concise task summaries and delegate details to phase-tasks.md --- README.md | 152 +++++++++++++++++++-------------------- docs/phase-tasks.md | 14 +++- docs/roadmap.md | 8 +-- docs/vscode-extension.md | 67 ++++++++--------- 4 files changed, 125 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 01b433d..1247e07 100644 --- a/README.md +++ b/README.md @@ -568,45 +568,45 @@ extension.ts ## 开发计划 -当前进度:Phase 1 Agent Core MVP 功能闭环、Phase 2 的 1M Context Capsule 核心收敛、Phase 3 的 VS Code 插件核心与共享 RPC 交互管线以及 Phase 4 的 VS Code 深度集成均已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 4 保留 14 项深度集成能力;Phase 5 已完成原生 `@prole` Chat Participant、默认右侧 Chat 入口、简化审批 UX、自动上下文压缩、UX 验收、`ProleCoder` Output Channel 错误诊断、插件内 SecretStorage 多 API key 管理器、侧边栏 API key/model 选择入口、统一 redaction 与错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除、过程事件默认折叠,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和 Enter/Shift+Enter 输入回归,但持续 UX 测试与体验改进占位仍未完成。DeepSeek provider、基础工具执行、Context Builder、Run Log、Turn Loop、CLI、RPC、审批、取消、真实 DeepSeek streaming/tool-call 验收、本地 fixture smoke、进程级 CLI smoke、小型真实仓库 CLI 联网验收、合并前测试收敛、Context Capsule、manifest、token estimator、attachments、provider summary、Run Log 体积控制、tool call JSON Schema 校验、200K/500K/900K 离线大上下文验收入口和 Phase 2e 展示型 demo 扩展均已完成;VS Code RPC server 启动监管、JSON-RPC request client、RPC 全双工 reader/writer 与事件发送队列、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、命令风险动态升级、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、命令子进程树清理、VSIX alpha 打包、extension-host 端到端验收、原生 `@prole` Chat Participant、自动上下文压缩、简化审批 UX、Output Channel 错误诊断、API key/model 配置、Git 工作流和结构化 provider 配置错误恢复均已完成。完成 P5-14 后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 到 Phase 4 已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 5 已完成原生 Chat、简化审批、自动上下文压缩、Output Channel、API key/model 配置、Git 工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX 和结构化 provider 配置错误恢复;P5-14 作为持续 UX 测试与体验改进总项,详细子项和完成口径见 `docs/phase-tasks.md`。完成 P5-14 全部子项后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 ### Phase 0:项目章程 -- [x] 确定 `ProleCoder` 名称和 AGPL-3.0-or-later 许可证:项目显示名为 ProleCoder,仓库名和包名前缀固定为 `prole-coder`,许可证从 MIT 调整为更符合自由软件网络服务场景的 AGPL-3.0-or-later。 -- [x] 编写 README 技术方案、架构、开发计划和注意事项:README 已作为项目入口,覆盖定位、架构、环境配置、阶段计划和安全注意事项。 +- [x] 确定 `ProleCoder` 名称和 AGPL-3.0-or-later 许可证。 +- [x] 编写 README 技术方案、架构、开发计划和注意事项。 - [x] 建立 Rust workspace:`agent-core`、`agent-rpc`、`cli`、`tui`。 - [x] 建立 TypeScript/pnpm workspace:`packages/protocol` 和 `vscode/extension`。 - [x] 建立基础环境配置:`rust-toolchain.toml`、`rustfmt.toml`、`tsconfig.base.json`、`.editorconfig`、`.gitattributes`、`.env.example`。 - [x] 更新 `.gitignore`,排除本地状态、依赖目录、构建产物和密钥文件。 -- [x] 生成并保留 `Cargo.lock` 与 `pnpm-lock.yaml`:Rust 与 TypeScript 依赖解析结果已锁定,便于本机和 CI 复现。 -- [x] 在 Windows 本机跑通 `pnpm run check`:确认 Rust、TypeScript 和 VS Code 插件骨架在 Windows 开发环境中可完整检查。 -- [x] 建立 CI 骨架:GitHub Actions 已覆盖 Rust 格式化、Clippy、测试和 TypeScript workspace 检查。 -- [x] 建立 `CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md`:补齐开源协作、社区行为和漏洞报告的基础治理文件。 -- [x] 建立 `docs/` 设计文档目录和 `docs/adr/` 架构决策记录:README 保留总览,详细模块设计和关键取舍进入 docs。 -- [x] 设计正式 JSON-RPC 事件协议:定义 `agent.initialize`、`agent.sendTurn`、`agent.event`、审批、取消和错误响应等核心消息。 -- [x] 定义工具 schema、风险等级和审批模型:建立工具注册表、默认审批要求和 read/write/exec/network/destructive 风险分类。 +- [x] 生成并保留 `Cargo.lock` 与 `pnpm-lock.yaml`。 +- [x] 在 Windows 本机跑通 `pnpm run check`。 +- [x] 建立 CI 骨架。 +- [x] 建立 `CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md`。 +- [x] 建立 `docs/` 设计文档目录和 `docs/adr/` 架构决策记录。 +- [x] 设计正式 JSON-RPC 事件协议。 +- [x] 定义工具 schema、风险等级和审批模型。 ### Phase 1:Agent Core MVP -- [x] DeepSeek provider 与 streaming 基础:实现 API adapter、data-only SSE parser、TurnProvider async/streaming 边界、CLI streaming wrapper、tool-call delta accumulator,并通过真实 streaming 与 forced tool-call live test 验收。 -- [x] Reasoning 与 Context Builder:实现 `reasoning_content` replay 状态机、基础 Context Builder 和 token 统计,并覆盖 replay 边界、token budget 与 `context.built` payload 测试。 -- [x] Workspace 工具执行层:实现 read/search/apply_patch/shell/git,覆盖路径约束、敏感路径拒绝、命令超时、结构化结果、`apply_patch` staging 失败恢复和工具取消信号。 -- [x] Run Log 与 summary:实现 `events.jsonl`、`summary.json`、基础脱敏、写入串行化和 `agent.listRuns`。 -- [x] Agent Turn Loop:串联 Context Builder、provider、reasoning、工具执行、审批、验证和 run log,并通过本地 fixture 端到端 smoke test。 -- [x] CLI `run` / `rpc` 最小闭环:支持 fixture/deepseek provider、工作区参数、JSON event、审批、验证命令、JSON-RPC error 输出、进程级 CLI fixture smoke 和小型真实仓库 CLI 联网验收。 -- [x] Agent RPC Server:实现 stdio 事件桥接、双向 request loop、真实 Turn Loop handler、实时事件输出、pending approval 队列、审批超时和取消语义。 -- [x] 审批前端基础:实现 CLI prompt、RPC approve/reject/cancel 分发、TypeScript 协议类型、TUI prompt 状态机和 VS Code legacy modal approval adapter;后续默认 UX 已改为 Sidebar 内联审批。 -- [x] Phase 1 合并前第一轮测试增强:完成 `pnpm run check` 基线验证、patch 失败恢复、reasoning 边界、CancellationToken 并发和 CLI event stream 顺序测试。 -- [x] 合并前测试基础设施收敛:提取共享 `agent-core::test_helpers::TestWorkspace`,统一当前分散在 agent-core、agent-rpc、cli、demo/live 测试中的临时工作区 helper。 -- [x] 合并前 live 测试配置收敛:统一 live API key 测试 helper,测试侧按 `PROLE_CODER_DEEPSEEK_API_KEY`、`DEEPSEEK_API_KEY`、`.secrets/deepseek-api-key` 的顺序读取。 -- [x] 合并前 RPC/CLI/protocol 验收补齐:补 RPC request loop pending approval 并发拒绝与 EOF shutdown 取消测试、CLI `rpc` 模式进程级 stdio smoke,以及 Rust/TypeScript/协议文档错误码交叉校验。 -- [x] 合并前最终验收:已运行 `pnpm run check`、`cargo test --workspace -- --list`、离线展示 demo、`git diff --check` 和敏感信息扫描;本轮 RPC/CLI/protocol 离线变更未新增必须阻塞合并的 DeepSeek live suite。 +- [x] DeepSeek provider 与 streaming 基础。 +- [x] Reasoning 与 Context Builder。 +- [x] Workspace 工具执行层。 +- [x] Run Log 与 summary。 +- [x] Agent Turn Loop。 +- [x] CLI `run` / `rpc` 最小闭环。 +- [x] Agent RPC Server。 +- [x] 审批前端基础。 +- [x] Phase 1 合并前第一轮测试增强。 +- [x] 合并前测试基础设施收敛。 +- [x] 合并前 live 测试配置收敛。 +- [x] 合并前 RPC/CLI/protocol 验收补齐。 +- [x] 合并前最终验收。 说明:VS Code RPC server 启动监管与 JSON-RPC request client 已提前完成,归入 Phase 3 前置项;Agent Core MVP 验收不依赖完整 VS Code UI。 -细任务维护规则:高层阶段条目完成时,同步检查并更新 `docs/phase-tasks.md` 中对应的详细任务状态;详细设计文档新增后续任务时,也先在该索引里确定阶段。 +细任务维护规则:README 开发计划只保留阶段级和任务名级摘要;每个任务的实现细节、验收命令、审查来源和子任务拆分统一维护在 `docs/phase-tasks.md`。高层阶段条目标记完成前,必须确认该阶段在 `docs/phase-tasks.md` 的所有父项和子项都已完成。 验收标准: @@ -617,14 +617,14 @@ extension.ts ### Phase 2:1M Context Capsule -- [x] Phase 2a-1:`read_file` 文件摘要元数据。已在 `read_file` 结果中加入完整文件的 `sha256` 和 `sizeBytes`,并同步 Rust/TypeScript 工具 result schema 与单元测试,为 manifest 文件摘要和工具结果一致性校验打底。 -- [x] Phase 2a-2:Context Capsule 数据模型与稳定 renderer。已定义 `ContextCapsule`、`ContextSection`、`ContextSectionItem`、`CachePlacement` 和 `context_capsule.v1` 稳定渲染流程;现有 provider 输入继续使用兼容字段 `content`,其值与 `rendered` 保持一致。 -- [x] Phase 2a-3:Workspace Manifest v0。已实现结构化 JSON、canonical `manifestHash`、默认 `maxEntries=500`、硬安全排除、默认工程排除、`.gitignore` + `.prole-coderignore`,并把 `workspace_manifest` 工具切换为可执行。 -- [x] Phase 2a-4:Context Builder manifest 接入。已在 Turn Loop 中自动生成 manifest summary 并放入 `StablePrefix`,同时扩展 `context.built` 事件输出 section token、manifest hash 和截断原因。 -- [x] Phase 2b:TokenEstimator 与稳定前缀。已建立 `TokenEstimator` trait、默认 `utf8_bytes` 估算器和 `CalibratedEstimator`,并在 `context.built` 中输出 `stablePrefixHash`、稳定前缀预算和校准 metadata;修改 `TurnSuffix` 不改变 `StablePrefix` 已有离线测试覆盖。 -- [x] Phase 2c:Attachments、provider summary 和 cache 实验。已让 `agent.sendTurn.attachments` 接入 file/selection/explicit_content/diagnostic 等来源,并加入数量、大小、路径和重复来源校验;Turn Loop 会写入 `provider.completed`,记录模型、duration、usage、cache hit/miss 和 streaming 摘要;DeepSeek streaming usage/cache 解析已有离线和 live 基础。 -- [x] Phase 2d:大仓库验收、超预算解释、Run Log 体积控制和 JSON Schema 校验层。已新增 200K、500K、900K ignored/manual Context Capsule benchmark;Context Builder 继续对 required context 超预算显式失败、optional context 写入 omitted reason;Run Log 写入入口统一执行脱敏和字符串/数组截断,并用 `runLogTruncation` 记录边界;tool call arguments 会先按工具注册表 JSON Schema 校验,再进入 typed deserialization、审批和执行。 -- [x] Phase 2e:合并主线前展示型 demo 扩展。已补齐 `demo-context`、`demo-truncation`、`demo-schema`、`demo-context-visual`、`demo-attachment` 和 `demo-live` provider summary 展示;这些入口默认 ignored,不进入普通 CI,作为人工观察和合并前验收使用。详细清单见 `docs/demos.md` 和 `docs/phase-tasks.md`。 +- [x] Phase 2a-1:`read_file` 文件摘要元数据。 +- [x] Phase 2a-2:Context Capsule 数据模型与稳定 renderer。 +- [x] Phase 2a-3:Workspace Manifest v0。 +- [x] Phase 2a-4:Context Builder manifest 接入。 +- [x] Phase 2b:TokenEstimator 与稳定前缀。 +- [x] Phase 2c:Attachments、provider summary 和 cache 实验。 +- [x] Phase 2d:大仓库验收、超预算解释、Run Log 体积控制和 JSON Schema 校验层。 +- [x] Phase 2e:合并主线前展示型 demo 扩展。 验收标准: @@ -638,21 +638,21 @@ extension.ts ### Phase 3:VS Code 插件核心与共享 RPC 交互管线 -- [x] RPC 全双工 reader/writer 与事件发送队列:作为 VS Code/TUI 共享前置,已支持 `agent.sendTurn` 创建 run 后立即返回、后台有界队列持续事件推送和 active run 断连取消;已通过 `cargo test` 与 `cargo clippy --all-targets -- -D warnings`。 -- [x] 长 provider request 期间的 client 断连取消:stdio EOF / shutdown 会取消 active run,writer 失败会触发断连取消句柄。 -- [x] TypeScript extension scaffold:建立 VS Code 插件 TypeScript 工程、激活入口、基础命令和测试骨架。 -- [x] RPC server 管理:插件可启动 `prole rpc`,发送 `agent.initialize`,转发 `agent.event`,并在退出或错误时更新状态和提示。 -- [x] JSON-RPC request client:统一 request id、pending response、error response 和进程退出时的 pending request 清理。 -- [x] VS Code/protocol TypeScript 类型共享收敛:extension 已通过 workspace 依赖消费 `@prole-coder/protocol`,`rpcServer.ts` 的 `AgentEventEnvelope` 改为 protocol 类型 alias,并由 extension build/typecheck/test 脚本先构建 protocol 声明。 -- [x] VS Code RPC/commands 边界测试补齐:已覆盖 `RpcServerManager` 启动异常、stdio 缺失、无效 JSON、process error、stop、onEvent dispose、stderr preview、sendRequest 写入失败等路径,以及 openChat 启动失败、非 Error 错误、不可持久审批 approve 和审批消息 paths 拼接。 -- [x] Sidebar Chat 与 `agent.event` 渲染:已贡献 ProleCoder Activity Bar view 和 Webview Sidebar Chat,订阅 `RpcServerManager.onEvent()` 并展示 assistant delta、tool lifecycle、审批、context/provider 和 terminal event;assistant delta 会按 run/turn 合并为一条消息。 -- [x] 文本输入发送 turn,并通过 `agent.sendTurn` 驱动真实 Agent 回合:Sidebar Chat 已提供 prompt 输入和 mode 选择,Webview 通过 typed `RpcServerManager.sendTurn()` 调用真实 `agent.sendTurn`,并在 accepted 后等待同一 run 的 terminal event 收口输入状态。 -- [x] VS Code 审批 UI 接入真实 RPC pending queue:`ApprovalEventController` 订阅 `tool.approvalRequired`,复用 VS Code approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 回传到 `agent.approve` / `agent.reject`;后续默认 UX 已改为 Sidebar 内联审批卡片。 -- [x] 命令风险分类器和动态风险升级:Agent Core 已在 shell 审批前识别依赖安装、网络访问、远程 git、删除和发布命令,升级 `tool.requested` / `tool.approvalRequired` 的风险,并通过 `riskReasons` 在 CLI/TUI/VS Code 审批展示升级原因。 -- [x] 更强进程树清理策略:命令类工具启动时建立可收束的进程树边界,Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底,取消和超时会清理 shell/search/git 等工具的子进程树。 -- [x] Native diff editor 展示 patch,并为 hunk 级审批预留交互边界:VS Code 侧缓存 `tool.requested` 的 `apply_patch` unified diff,在对应 `tool.approvalRequired` 前用 VS Code 原生 diff editor 展示虚拟补丁结果,并生成稳定 hunk approval boundary 供后续细粒度审批复用。 -- [x] Run List / resume:Sidebar Chat 已通过 typed `RpcServerManager.listRuns()` 拉取 run summary,并可点击历史 run 调用 `agent.resume` 清空当前事件视图后按 Run Log `seq` 重放。 -- [x] Context Capsule 可视化:Sidebar Chat 已消费 `context.built` metadata,展示 StablePrefix / DynamicPrelude / TurnSuffix token 分布、input/stable budget、cache/estimator 摘要、included/omitted sources 和 manifest 摘要。 +- [x] RPC 全双工 reader/writer 与事件发送队列。 +- [x] 长 provider request 期间的 client 断连取消。 +- [x] TypeScript extension scaffold。 +- [x] RPC server 管理。 +- [x] JSON-RPC request client。 +- [x] VS Code/protocol TypeScript 类型共享收敛。 +- [x] VS Code RPC/commands 边界测试补齐。 +- [x] Sidebar Chat 与 `agent.event` 渲染。 +- [x] 文本输入发送 turn,并通过 `agent.sendTurn` 驱动真实 Agent 回合。 +- [x] VS Code 审批 UI 接入真实 RPC pending queue。 +- [x] 命令风险分类器和动态风险升级。 +- [x] 更强进程树清理策略。 +- [x] Native diff editor 展示 patch,并为 hunk 级审批预留交互边界。 +- [x] Run List / resume。 +- [x] Context Capsule 可视化。 验收标准: @@ -668,20 +668,20 @@ extension.ts ### Phase 4:VS Code 深度集成 -- [x] P4-1:VSIX dry-run packaging smoke,已通过 `pnpm run vsix:smoke` 验证 `.vscodeignore`、`workspace:*` 依赖边界、media asset、compiled `out/` 和 activationEvents;该 smoke 会临时生成并检查 VSIX,随后清理产物,不代表 P4-13 完成。 -- [x] P4-2:`@vscode/test-electron` 最小 harness,覆盖 extension activation、trusted workspace 和 Chat view 基础加载;`pnpm run vscode:test-electron` 可运行 smoke。 -- [x] P4-3:Provider capability model data contract,已通过 ADR 0006 和 `agent.initialize.capabilities.provider` 显式表达 thinking、tool calls/tool choice、FIM、stream/cache usage、上下文和输出限制。 -- [x] P4-4:事件 payload schema 与协议 fixture 对齐,已新增共享 fixture 与 Rust/TypeScript 测试,并补齐协议版本不匹配的 VS Code 提示边界。 -- [x] P4-5:RPC 高频事件输出节流与批量发送策略,实时 wire 层支持 `agent.eventBatch`,Run Log 与 replay 仍保持逐事件 `seq` 事实来源。 -- [x] P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已新增 `RpcServerManager.cancel()`、Cancel 按钮和运行中 composer 状态收口。 -- [x] P4-7:Problems 面板 diagnostics 通过 diagnostic attachments 进入 Context Builder,VS Code 发送 turn 时会采集当前 Problems 快照,按协议 attachment 上限裁剪并优先保留 error。 -- [x] P4-8:Terminal command approval,审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要和持久化语义;P5-2 后主审批 UI 不再暴露复杂持久化选项。 -- [x] P4-9:审批持久化存储,RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 -- [x] P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已新增 Open Settings 命令,打开 VS Code 设置并展示 RPC server capability、默认模型、预算、审批能力、RPC command/state;配置只包含非敏感 RPC/FIM 选项,不保存 API Key。 -- [x] P4-11:真实 hunk 级 patch 审批,首版限定 `apply_patch`,Core/RPC 支持 selected hunk 决策、校验未知/重复 hunk、Run Log 记录 selected/all 范围,VS Code 审批 UI 可选择 hunks 并通过 `agent.approve.hunks` 回传;审批事件 payload 已同步协议 fixture。 -- [x] P4-12:FIM completion preview,已新增 `agent.previewFim` RPC、DeepSeek beta FIM adapter、fixture provider 预览和 VS Code inline completion provider,模型选择只依赖 server capability 的 `supportsFim`。 -- [x] P4-13:VSIX alpha / pre-release 打包与插件安装说明,已新增 `pnpm run vsix:alpha`,在 `target/vsix/` 生成可安装 pre-release VSIX 与 SHA-256 校验和,并在 `docs/release.md` 记录 clean 环境安装验收路径。 -- [x] P4-14:补齐 end-to-end 集成测试覆盖,已在 `pnpm run vscode:test-electron` 中接入本地 JSON-RPC fixture server,覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume 和隔离 VS Code profile 启动;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的 P4-13 路径手动验收。 +- [x] P4-1:VSIX dry-run packaging smoke。 +- [x] P4-2:`@vscode/test-electron` 最小 harness。 +- [x] P4-3:Provider capability model data contract。 +- [x] P4-4:事件 payload schema 与协议 fixture 对齐。 +- [x] P4-5:RPC 高频事件输出节流与批量发送策略。 +- [x] P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI。 +- [x] P4-7:Problems 面板 diagnostics 通过 diagnostic attachments 进入 Context Builder。 +- [x] P4-8:Terminal command approval。 +- [x] P4-9:审批持久化存储。 +- [x] P4-10:provider、model、预算、审批策略和 RPC 命令配置界面。 +- [x] P4-11:真实 hunk 级 patch 审批。 +- [x] P4-12:FIM completion preview。 +- [x] P4-13:VSIX alpha / pre-release 打包与插件安装说明。 +- [x] P4-14:补齐 end-to-end 集成测试覆盖。 验收标准: @@ -694,20 +694,20 @@ extension.ts ### Phase 5:VS Code Codex-like UX 与开发工作流 -- [x] P5-1:原生 VS Code Chat Participant `@prole`,让常规入口默认打开 VS Code Chat 侧栏体验;保留 Activity Bar Webview 作为 Run List / Context Capsule / diff 等高级面板。 -- [x] P5-2:简化审批 UX,主审批动作收敛为 Approve / Reject,`apply_patch` 多 hunk 时保留 hunk 选择边界;持久化策略继续由后端策略控制,不在主审批 UI 里暴露复杂选项。 -- [x] P5-3:Sidebar Chat 和原生 Chat Participant 自动注入压缩后的对话历史,作为 `explicit_content` attachment 进入已有 Context Capsule 管线,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。 -- [x] P5-4:补齐 UX 收敛单元测试、extension-host E2E、VSIX smoke/alpha 打包验证和文档说明;已通过 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`,并补充 Chat Participant 早到 terminal event 缓冲回归测试。 -- [x] P5-5:VS Code Output Channel 错误诊断:插件创建 `ProleCoder` Output Channel,Sidebar Chat、Run List、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误会写入 Output;activation 层使用统一 notifier 分发日志与 VS Code toast;侧边栏继续显示短状态并通过 hover title 暴露完整文本。 -- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status:插件提供 Configure/Clear API Key、Select DeepSeek Model 和 provider status 入口;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 修改 alias 和 trash 删除指定 key;status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 -- [x] P5-7:统一 redaction 与 API key 错误恢复 UX:notifier/logger 统一脱敏 SecretStorage/env key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并保留修复按钮;配置/清除 key 或切换 model 后 idle 状态自动重启 RPC,active run 保守提示稍后生效。 -- [x] P5-8:Git context 只读采集与大 diff attachment 管线:优先使用 VS Code Git API 采集 branch/upstream 和 staged diff;git CLI 只作受控 fallback 采集 unstaged diff、diff stat、branch diff、commit summary 和 ref 检测,cwd 来自 repository root。 -- [x] P5-9:Generate Commit Message 写入 Source Control inputBox:基于 staged diff 生成 Conventional Commit 风格候选 message 并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context,不自动 commit。 -- [x] P5-10:Generate PR Description markdown 生成:基于 upstream/main/master/用户选择的 base 采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版只提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 -- [x] P5-11:Phase 5 UX 工作流验收与文档收敛:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证、`git diff --check`、敏感信息扫描和文档一致性检查;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并需接入审批模型。 -- [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX:`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar resume 后继续同一会话发送多轮 turn;Run List 支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder` 便于 debug。 -- [x] P5-13:结构化 provider 配置错误码与恢复动作:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回结构化 `data.recoverableAction`,Sidebar/原生 Chat 依据恢复动作展示或触发 Configure API Key 入口,不再依赖后端英文错误消息;run failed payload 也可携带同一恢复动作。 -- [ ] P5-14:持续 UX 测试与体验改进占位:保留 Phase 5 的持续验收入口;已根据真实试用补齐 Sidebar 对话专属视图、内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示以及 Enter 发送 / Shift+Enter 换行,后续继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题。 +- [x] P5-1:原生 VS Code Chat Participant `@prole`。 +- [x] P5-2:简化审批 UX。 +- [x] P5-3:自动上下文压缩。 +- [x] P5-4:UX 收敛测试与打包验收。 +- [x] P5-5:VS Code Output Channel 错误诊断。 +- [x] P5-6:DeepSeek API key SecretStorage、model selector 与 provider status。 +- [x] P5-7:统一 redaction 与 API key 错误恢复 UX。 +- [x] P5-8:Git context 只读采集与大 diff attachment 管线。 +- [x] P5-9:Generate Commit Message 写入 Source Control inputBox。 +- [x] P5-10:Generate PR Description markdown 生成。 +- [x] P5-11:Phase 5 UX 工作流验收与文档收敛。 +- [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX。 +- [x] P5-13:结构化 provider 配置错误码与恢复动作。 +- [ ] P5-14:持续 UX 测试与体验改进:持续收敛真实 VS Code 插件试用中的对话、审批、Runs、Key/Model、Output 日志、上下文压缩和大工具参数稳定性问题;具体子项与完成口径见 `docs/phase-tasks.md`。 验收标准: @@ -715,7 +715,7 @@ extension.ts - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 - 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 +- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 子项未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 1d792d6..c45dc31 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 已补充一批真实试用 UX 回归修复,但持续 UX 测试与体验改进占位仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 拆分为子项管理,其中 P5-14a 到 P5-14c 已完成,P5-14d 起仍有 UX E2E 回归、大工具参数协议和真实试用 backlog 未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -9,6 +9,8 @@ - 新增任何预期实现项时,必须在本文件登记阶段和状态。 - README 开发计划中的阶段条目标记完成前,应检查本文件中对应细任务是否已经完成。 - 如果一个 README 条目完成了它蕴含的细任务,应同步把本文件对应行标记为 `[x]`,并在说明中写清验收方式。 +- README 开发计划只写阶段和任务摘要;实现细节、验收命令、审查来源、后续拆分统一写在本文件。 +- 复杂任务使用父项加子项编号维护,例如 `P5-14` 作为总项,`P5-14a` / `P5-14b` 记录可独立开发和验收的子任务;父项只有在所有子项完成后才能标记完成。 - 详细模块文档仍保留设计说明;本文件只记录阶段、状态和追踪入口。 ## 审计结论 @@ -109,7 +111,7 @@ | [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 作为持续 UX 测试与体验改进总项,已拆分为 P5-14a 到 P5-14f。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | @@ -126,7 +128,13 @@ | [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | | [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | | [x] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | -| [ ] | P5-14:持续 UX 测试与体验改进占位 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:保留 Phase 5 的持续验收入口,用于根据真实 VS Code 插件试用继续收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题;已补齐:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板,approval 和 run delete 改为就地确认 UI,默认对话流只显示用户消息与 DeepSeek 回复,tool/provider/context/run completed 等过程事件收敛到默认折叠的 Work log,Work log 摘要只暴露当前工作状态,输入框 Enter 发送且 Shift+Enter 换行;已登记后续 E2E 增强项:覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片、输入快捷键和 Work log 折叠展示。只有这些手动/自动回归项稳定后,Phase 5 才能整体标记完成。 | +| [ ] | P5-14:持续 UX 测试与体验改进总项 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 父项:用于承接真实 VS Code 插件试用后的体验修复、稳定性增强和回归验收。只有 P5-14a 到 P5-14f 全部完成后,P5-14 才能标记完成,Phase 5 才能整体标记完成。 | +| [x] | P5-14a:Sidebar 对话专属视图与就地确认 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板;approval 和 run delete 改为 webview 就地确认 UI,避免系统 modal 打断对话流。验收:已补充相关 Sidebar 状态和消息处理测试。 | +| [x] | P5-14b:默认对话流、Work log 折叠和输入快捷键 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:默认只显示用户消息与 DeepSeek 回复;tool/provider/context/run completed 等过程事件收敛到默认折叠的 Work log;Work log 摘要只暴露当前工作状态;输入框 Enter 发送且 Shift+Enter 换行。验收:已补充 timeline / chat input 回归测试。 | +| [x] | P5-14c:malformed tool-call 本地诊断文件 | `docs/json-rpc-protocol.md`、`docs/run-log.md`、`docs/turn-loop.md` | 已完成:当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在当前 run 的 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments,并在 `run.failed.diagnosticFile` / Sidebar failure card 指向该文件。验收:`cargo test -p prole-coder-agent-core diagnostic`、`pnpm --filter prole-coder-vscode test`。 | +| [ ] | P5-14d:Sidebar UX extension-host E2E 回归 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:用 extension-host 覆盖真实 webview 流程,包括 resume 后继续发送新 turn、默认事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片、输入快捷键和 Work log 折叠展示。完成后再更新 Phase 5 验收记录。 | +| [ ] | P5-14e:大 patch / 大文件分块式工具参数协议 | `docs/tool-system.md`、`docs/turn-loop.md` | 未完成:为大 patch / 大文件写入设计 chunked tool payload 协议,允许 provider 分块流式输出到 run-scoped 临时文件,最后通过 commit/apply 调用统一进入 schema 校验、路径安全、审批和 hunk 边界;chunk 过程中不得边接收边执行写入。 | +| [ ] | P5-14f:真实试用 UX backlog 收敛 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,并把可执行项登记为 P5-14 子项或后续 Phase 任务。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index 81a841c..a3cebe3 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 已补充对话专属视图、内联审批/删除确认、默认只显示用户消息与 DeepSeek 回复的对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 作为持续 UX 测试与体验改进总项,子项状态以 `docs/phase-tasks.md` 为准。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-13 Codex-like UX 与开发工作流已完成:原生 `@prole` Chat Participant、`Open Chat` 右侧 Chat 入口、简化审批 choices、自动上下文压缩 attachment、Output Channel 错误诊断、DeepSeek API key SecretStorage 多 key 管理器/provider status(含 add/select/rename/delete)、Sidebar Key/Model 入口、DeepSeek model selector、统一 redaction 与错误恢复、Git context 只读采集、Generate Commit Message、Generate PR Description、Sidebar 连续会话、Run 删除、过程事件默认折叠、结构化 provider 配置错误码与恢复动作以及测试/文档验收收敛;P5-14 持续 UX 测试与体验改进占位仍未完成。 +- Phase 5 的 P5-1 到 P5-13 Codex-like UX 与开发工作流已完成;P5-14 持续 UX 测试与体验改进已拆分为 P5-14a 等子项,仍有未完成项。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 @@ -121,7 +121,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 - P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 -- P5-14:持续 UX 测试与体验改进占位,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩问题回归;已补齐 Sidebar 对话专属视图、webview 内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、默认折叠 Work log 和 Enter / Shift+Enter 输入行为。 +- P5-14:持续 UX 测试与体验改进总项,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志、上下文压缩和大 patch / 大文件工具参数问题回归;具体 P5-14a 到 P5-14f 子项见 `docs/phase-tasks.md`。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 @@ -135,7 +135,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-14 未完成,README 不能把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-14 子项未完成,README 不能把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index deb5042..7ea6312 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 已补充 Sidebar 对话专属视图、内联审批/删除确认、默认对话流、Work log 折叠展示和输入快捷键回归,但持续 UX 测试与体验改进占位仍未完成。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 持续 UX 测试与体验改进已拆分为子项,仍有未完成项。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -119,38 +119,39 @@ Phase 3 P0 验收标准: - `ProleCoder: Open Settings` 能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key,DeepSeek model ID 作为非敏感配置保存。 - Inline completion 首版通过 `agent.previewFim` 请求 RPC server 的 FIM preview,只有 server capability 明确标记 `supportsFim` 的模型会被使用。 -Phase 4 深度集成权威清单与 `docs/phase-tasks.md` 对齐: - -1. P4-1:VSIX dry-run packaging smoke,已完成:`pnpm run vsix:smoke` 会构建 extension,临时生成 VSIX,检查 `.vscodeignore`、`workspace:*` 运行时边界、media asset、compiled `out/` 和 activationEvents,并清理产物;不标记最终 VSIX 交付完成。 -2. P4-2:`@vscode/test-electron` 最小 harness,已完成:`pnpm run vscode:test-electron` 覆盖 activation、trusted workspace、Chat view focus 和命令注册,测试工作区禁用 RPC autoStart。 -3. P4-3:Provider capability model data contract,已完成:`agent.initialize.capabilities.provider` 暴露 DeepSeek V4 model capability,首版不引入 heavy trait。 -4. P4-4:事件 payload schema 与协议 fixture 对齐,已完成:共享 fixture 覆盖 `provider.requested`、`tool.completed`、`run.completed`,并处理协议版本不匹配提示。 -5. P4-5:RPC 高频事件输出节流与批量发送策略,已完成:实时 live event 支持 `agent.eventBatch`,保持 Run Log `seq` 与 replay 语义稳定。 -6. P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI,已完成:`RpcServerManager.cancel()` 和 Sidebar Chat Cancel 按钮接入真实 RPC。 -7. P4-7:通过 diagnostic attachments 读取 Problems 面板诊断并交给 Agent Core,已完成:发送 turn 时采集 Problems 快照,并按协议 attachment 上限裁剪。 -8. P4-8:Terminal command approval 展示命令、cwd、风险等级、上一条 shell 输出摘要和持久化语义,已完成:shared protocol payload 和后端策略已支持;P5-2 后主审批 UI 不再暴露持久化选项。 -9. P4-9:审批持久化存储,已完成:RPC 队列支持 session/workspace 持久批准,并继续禁止 network/destructive 风险持久化。 -10. P4-10:provider、model、预算、审批策略和 RPC 命令配置界面,已完成:Open Settings 命令展示 `agent.initialize` 返回的 capability data、RPC command/state 和 API Key 不落 VS Code settings 的边界。 -11. P4-11:真实 hunk 级 patch 审批,已完成:`apply_patch` 可选择 hunks,RPC/Core 校验 hunk id 并只应用已批准 hunks,审批事件 payload 已同步 fixture。 -12. P4-12:FIM completion preview,已完成:VS Code 原生 inline completion 通过 `agent.previewFim` 获取 preview,模型选择只依赖 server capability。 -13. P4-13:VSIX alpha / pre-release 打包与安装说明,已完成:`pnpm run vsix:alpha` 会生成 `target/vsix/prole-coder-vscode-0.1.0-alpha.vsix` 和 `.sha256` 校验和,VSIX manifest 标记为 VS Code pre-release;`docs/release.md` 记录 clean user-data/extensions 目录下的安装验收步骤。 -14. P4-14:补齐 end-to-end 集成测试覆盖,已完成:`pnpm run vscode:test-electron` 使用本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批回传、Run List / resume,并使用隔离 VS Code profile 避免本机状态影响测试;VSIX 安装后基础交互按 `docs/release.md` 的 clean 环境路径验收。 -Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐: - -1. P5-1:原生 Chat 入口,已完成:贡献 `@prole` Chat Participant,并让 `ProleCoder: Open Chat` 优先打开 VS Code Chat 侧栏;普通 Activity Bar Webview 继续承载 Run List、Context Capsule 和更详细事件视图。 -2. P5-2:简化审批,已完成:主审批按钮保持 Approve / Reject;多 hunk patch 继续保留 hunk 选择边界;持久化审批能力仍由 Core/RPC 策略约束,不把复杂策略放进主审批 UI。 -3. P5-3:自动上下文压缩,已完成:Sidebar Chat 和原生 Chat Participant 会把历史对话/事件摘要压缩为 `explicit_content` attachment,交给 Context Capsule 处理,让连续对话自然承接上下文。 -4. P5-4:UX 收敛测试与打包验收,已完成:已覆盖 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke` 和 `pnpm run vsix:alpha`。 -5. P5-5:VS Code Output Channel 错误诊断,已完成:创建 `ProleCoder` Output Channel,记录 Sidebar Chat、Run List、原生 Chat Participant 和 RPC 启动/运行 warning 的完整错误;activation 层使用统一 notifier 分发日志与 VS Code toast,避免侧边栏短状态截断关键诊断。 -6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status,已完成:插件内配置/清除 key、选择 DeepSeek model 与查看 provider status;API key 配置入口是 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 按钮修改 alias 和 trash 按钮删除指定 key;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback,RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`。 -7. P5-7:统一 redaction 与 API key 错误恢复 UX,已完成:notifier/logger 统一脱敏 SecretStorage/env key;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口,Sidebar 错误状态保留 Configure API Key 修复按钮;API key 配置或 model 切换后 idle 状态自动重启 RPC,active run 场景保守提示稍后生效。 -8. P5-8:Git context 只读采集与大 diff attachment 管线,已完成:优先使用 VS Code Git API,git CLI 仅作受控 fallback,commit/PR 命令把 diff context 作为 `explicit_content` attachment 进入 Context Capsule 管线。 -9. P5-9:Generate Commit Message,已完成:从 staged diff 生成候选 commit message 并写入 Source Control inputBox,不自动 commit;staged 为空时才询问是否使用 unstaged diff。 -10. P5-10:Generate PR Description,已完成:根据 upstream/main/master/用户选择的 base、diff/stat 和 commit summary 生成 PR title/body markdown,用带标题的 untitled markdown 预览承载结果,不自动创建 PR。 -11. P5-11:Phase 5 UX 工作流验收,已完成:补齐 P5-6 到 P5-10 的测试、VSIX 验证和文档收敛;Git workflow agent 终态事件已补幂等保护,G4 自动 commit / push / create PR 留作后续增强并接入审批模型。 -12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX,已完成:Sidebar resume 后继续同一 run 发送多轮 turn,Run List 可通过 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,assistant 文本和最终摘要保持可见,完整事件 payload 写入 `Output > ProleCoder`。 -13. P5-13:结构化 provider 配置错误码与恢复动作,已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`,Sidebar/原生 Chat Participant 依据 recoverable action 展示或自动触发 Configure API Key,不再解析后端英文错误消息;run failed payload 也支持同一恢复动作。 -14. P5-14:持续 UX 测试与体验改进占位,未完成:已补齐进入 run 后的 Sidebar 对话专属视图、内联 approval/delete 确认、默认只显示用户消息与 DeepSeek 回复的对话流、默认折叠 Work log、Enter 发送 / Shift+Enter 换行;后续继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,作为 Phase 5 整体完成前的持续验收入口。 +Phase 4 深度集成清单与 `docs/phase-tasks.md` 对齐;实现细节和验收命令以任务索引为准: + +1. P4-1:VSIX dry-run packaging smoke。 +2. P4-2:`@vscode/test-electron` 最小 harness。 +3. P4-3:Provider capability model data contract。 +4. P4-4:事件 payload schema 与协议 fixture 对齐。 +5. P4-5:RPC 高频事件输出节流与批量发送策略。 +6. P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI。 +7. P4-7:Problems 面板诊断进入 Context Builder。 +8. P4-8:Terminal command approval。 +9. P4-9:审批持久化存储。 +10. P4-10:provider、model、预算、审批策略和 RPC 命令配置界面。 +11. P4-11:真实 hunk 级 patch 审批。 +12. P4-12:FIM completion preview。 +13. P4-13:VSIX alpha / pre-release 打包与安装说明。 +14. P4-14:补齐 end-to-end 集成测试覆盖。 + +Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐;复杂项在任务索引里继续拆子项: + +1. P5-1:原生 Chat 入口。 +2. P5-2:简化审批。 +3. P5-3:自动上下文压缩。 +4. P5-4:UX 收敛测试与打包验收。 +5. P5-5:VS Code Output Channel 错误诊断。 +6. P5-6:DeepSeek API key SecretStorage、model selector 与 provider status。 +7. P5-7:统一 redaction 与 API key 错误恢复 UX。 +8. P5-8:Git context 只读采集与大 diff attachment 管线。 +9. P5-9:Generate Commit Message。 +10. P5-10:Generate PR Description。 +11. P5-11:Phase 5 UX 工作流验收。 +12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX。 +13. P5-13:结构化 provider 配置错误码与恢复动作。 +14. P5-14:持续 UX 测试与体验改进。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 From 48f7172cc626a4e12e66aa1d0bf21f63e7131348 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:15:24 +0800 Subject: [PATCH 65/84] Adds run-scoped payload file support to Run Log --- crates/agent-core/src/run_log.rs | 173 ++++++++++++++++++++++++++++++- 1 file changed, 170 insertions(+), 3 deletions(-) diff --git a/crates/agent-core/src/run_log.rs b/crates/agent-core/src/run_log.rs index 60e24d0..5d79cc8 100644 --- a/crates/agent-core/src/run_log.rs +++ b/crates/agent-core/src/run_log.rs @@ -228,6 +228,20 @@ pub trait RunLogWriter { let _ = (relative_path, contents); Ok(None) } + + fn read_payload_file(&mut self, relative_path: &Path) -> Result, RunLogError> { + let _ = relative_path; + Ok(None) + } + + fn append_payload_chunk( + &mut self, + relative_path: &Path, + chunk: &str, + ) -> Result, RunLogError> { + let _ = (relative_path, chunk); + Ok(None) + } } /// `RunLog` is a single-writer append handle. @@ -310,7 +324,27 @@ impl RunLog { relative_path: &Path, contents: &str, ) -> Result { - write_run_diagnostic_file(&self.events_path, relative_path, contents) + write_run_text_file(&self.events_path, relative_path, contents, true) + } + + pub fn write_run_payload_file( + &mut self, + relative_path: &Path, + contents: &str, + ) -> Result { + write_run_text_file(&self.events_path, relative_path, contents, false) + } + + pub fn append_run_payload_chunk( + &mut self, + relative_path: &Path, + chunk: &str, + ) -> Result { + append_run_text_file(&self.events_path, relative_path, chunk) + } + + pub fn read_run_payload_file(&self, relative_path: &Path) -> Result { + read_run_text_file(&self.events_path, relative_path) } } @@ -336,6 +370,19 @@ impl RunLogWriter for RunLog { self.write_run_diagnostic_file(relative_path, contents) .map(Some) } + + fn read_payload_file(&mut self, relative_path: &Path) -> Result, RunLogError> { + self.read_run_payload_file(relative_path).map(Some) + } + + fn append_payload_chunk( + &mut self, + relative_path: &Path, + chunk: &str, + ) -> Result, RunLogError> { + self.append_run_payload_chunk(relative_path, chunk) + .map(Some) + } } #[derive(Debug, Clone)] @@ -394,6 +441,26 @@ impl SerializedRunLog { .write_run_diagnostic_file(relative_path, contents) } + pub fn write_run_payload_file( + &self, + relative_path: &Path, + contents: &str, + ) -> Result { + self.lock()?.write_run_payload_file(relative_path, contents) + } + + pub fn append_run_payload_chunk( + &self, + relative_path: &Path, + chunk: &str, + ) -> Result { + self.lock()?.append_run_payload_chunk(relative_path, chunk) + } + + pub fn read_run_payload_file(&self, relative_path: &Path) -> Result { + self.lock()?.read_run_payload_file(relative_path) + } + fn lock(&self) -> Result, RunLogError> { self.inner .lock() @@ -424,6 +491,18 @@ impl RunLogWriter for SerializedRunLog { ) -> Result, RunLogError> { SerializedRunLog::write_run_diagnostic_file(self, relative_path, contents).map(Some) } + + fn read_payload_file(&mut self, relative_path: &Path) -> Result, RunLogError> { + SerializedRunLog::read_run_payload_file(self, relative_path).map(Some) + } + + fn append_payload_chunk( + &mut self, + relative_path: &Path, + chunk: &str, + ) -> Result, RunLogError> { + SerializedRunLog::append_run_payload_chunk(self, relative_path, chunk).map(Some) + } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -637,10 +716,11 @@ fn append_event(path: &Path, event: &RunLogEvent) -> Result<(), RunLogError> { Ok(()) } -fn write_run_diagnostic_file( +fn write_run_text_file( events_path: &Path, relative_path: &Path, contents: &str, + append_newline: bool, ) -> Result { let relative_path = normalize_workspace_relative_path(relative_path)?; let run_dir = events_path @@ -669,10 +749,62 @@ fn write_run_diagnostic_file( path: path.clone(), source, })?; - file.write_all(b"\n").map_err(|source| RunLogError::Io { + if append_newline { + file.write_all(b"\n").map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + } + file.flush().map_err(|source| RunLogError::Io { path: path.clone(), source, })?; + Ok(path) +} + +fn read_run_text_file(events_path: &Path, relative_path: &Path) -> Result { + let relative_path = normalize_workspace_relative_path(relative_path)?; + let run_dir = events_path + .parent() + .ok_or_else(|| RunLogError::InvalidStatePath { + path: events_path.to_path_buf(), + })?; + let path = run_dir.join(relative_path); + fs::read_to_string(&path).map_err(|source| RunLogError::Io { path, source }) +} + +fn append_run_text_file( + events_path: &Path, + relative_path: &Path, + chunk: &str, +) -> Result { + let relative_path = normalize_workspace_relative_path(relative_path)?; + let run_dir = events_path + .parent() + .ok_or_else(|| RunLogError::InvalidStatePath { + path: events_path.to_path_buf(), + })?; + let path = run_dir.join(relative_path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|source| RunLogError::Io { + path: parent.to_path_buf(), + source, + })?; + } + let mut file = OpenOptions::new() + .write(true) + .create(true) + .append(true) + .open(&path) + .map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; + file.write_all(chunk.as_bytes()) + .map_err(|source| RunLogError::Io { + path: path.clone(), + source, + })?; file.flush().map_err(|source| RunLogError::Io { path: path.clone(), source, @@ -1194,6 +1326,41 @@ mod tests { run.write_run_diagnostic_file(std::path::Path::new("../outside.txt"), "bad"), Err(RunLogError::InvalidStatePath { .. }) )); + + let payload_path = run + .write_run_payload_file( + std::path::Path::new("payloads/apply_patch/patch.diff"), + "diff", + ) + .expect("payload file should be written"); + assert_eq!( + fs::read_to_string(&payload_path).expect("payload file should be readable"), + "diff" + ); + assert_eq!( + run.read_run_payload_file(std::path::Path::new("payloads/apply_patch/patch.diff")) + .expect("payload file should load"), + "diff" + ); + run.append_run_payload_chunk( + std::path::Path::new("payloads/apply_patch/chunked.diff"), + "di", + ) + .expect("first payload chunk should append"); + run.append_run_payload_chunk( + std::path::Path::new("payloads/apply_patch/chunked.diff"), + "ff", + ) + .expect("second payload chunk should append"); + assert_eq!( + run.read_run_payload_file(std::path::Path::new("payloads/apply_patch/chunked.diff")) + .expect("chunked payload file should load"), + "diff" + ); + assert!(matches!( + run.read_run_payload_file(std::path::Path::new("../outside.txt")), + Err(RunLogError::InvalidStatePath { .. }) + )); } #[test] From d36b2d641e29a4582a4bbbc2d5f91b209f91db6f Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:15:24 +0800 Subject: [PATCH 66/84] Adds payloadRef support to apply_patch tool schema and execution --- crates/agent-core/src/tool.rs | 33 +++- crates/agent-core/src/turn_loop.rs | 269 ++++++++++++++++++++++++++++- 2 files changed, 295 insertions(+), 7 deletions(-) diff --git a/crates/agent-core/src/tool.rs b/crates/agent-core/src/tool.rs index 1372079..25fc32c 100644 --- a/crates/agent-core/src/tool.rs +++ b/crates/agent-core/src/tool.rs @@ -172,9 +172,20 @@ const SEARCH_ARGUMENT_SCHEMA: &str = r#"{ const APPLY_PATCH_ARGUMENT_SCHEMA: &str = r#"{ "type": "object", "additionalProperties": false, - "required": ["unifiedDiff", "expectedFiles"], + "required": ["expectedFiles"], "properties": { "unifiedDiff": { "type": "string", "minLength": 1 }, + "payloadRef": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "path"], + "properties": { + "kind": { "type": "string", "enum": ["run_file"] }, + "path": { "type": "string", "minLength": 1 }, + "sha256": { "type": "string", "minLength": 64 }, + "sizeBytes": { "type": "integer", "minimum": 0 } + } + }, "expectedFiles": { "type": "array", "minItems": 1, @@ -669,6 +680,26 @@ mod tests { assert!(error.detail().contains("expected array")); } + #[test] + fn apply_patch_schema_accepts_run_scoped_payload_refs() { + let apply_patch = find_builtin_tool(ToolName::ApplyPatch.as_str()) + .expect("apply_patch tool must be registered"); + + validate_tool_arguments( + apply_patch, + &json!({ + "payloadRef": { + "kind": "run_file", + "path": "payloads/apply_patch/patch.diff", + "sha256": "0".repeat(64), + "sizeBytes": 1024, + }, + "expectedFiles": ["README.md"], + }), + ) + .expect("payloadRef apply_patch arguments should pass schema validation"); + } + #[test] fn tool_argument_schema_validator_accepts_nested_objects() { let plan_update = find_builtin_tool(ToolName::PlanUpdate.as_str()) diff --git a/crates/agent-core/src/turn_loop.rs b/crates/agent-core/src/turn_loop.rs index 87c64c8..28cdf71 100644 --- a/crates/agent-core/src/turn_loop.rs +++ b/crates/agent-core/src/turn_loop.rs @@ -633,6 +633,8 @@ where })?; let arguments_preview = parse_tool_arguments_value(tool_call)?; validate_tool_call_arguments(definition, tool_call, &arguments_preview)?; + let executable_arguments = + executable_tool_arguments(definition, tool_call, &arguments_preview, run_log)?; let risk_assessment = tool_risk_assessment(definition, &arguments_preview); append_turn_event( @@ -693,7 +695,7 @@ where ) } ToolName::ApplyPatch => { - let args: ApplyPatchArgs = parse_tool_arguments(tool_call, &arguments_preview)?; + let args: ApplyPatchArgs = parse_tool_arguments(tool_call, &executable_arguments)?; let approval_hunks = patch_approval_hunks(&args.unified_diff)?; let approval_scope = self.ensure_approval( definition, @@ -733,7 +735,7 @@ where ) } ToolName::Shell => { - let args: ShellArgs = parse_tool_arguments(tool_call, &arguments_preview)?; + let args: ShellArgs = parse_tool_arguments(tool_call, &executable_arguments)?; self.ensure_approval( definition, &risk_assessment, @@ -1644,6 +1646,12 @@ pub enum AgentTurnLoopError { name: String, source: ToolArgumentSchemaError, }, + #[error("tool call `{tool_call_id}` for `{name}` has invalid payload reference: {detail}")] + InvalidToolPayloadReference { + tool_call_id: String, + name: String, + detail: String, + }, #[error("tool result serialization failed: {0}")] Serialization(#[from] serde_json::Error), #[error("too many attachments: got {count}, max {max_attachments}")] @@ -1703,9 +1711,9 @@ impl AgentTurnLoopError { Self::Canceled { .. } => "E_RUN_CANCELED", Self::UnknownTool { .. } => "E_UNKNOWN_TOOL", Self::UnsupportedTool { .. } => "E_UNSUPPORTED_TOOL", - Self::InvalidToolArguments { .. } | Self::InvalidToolArgumentSchema { .. } => { - "E_INVALID_TOOL_ARGUMENTS" - } + Self::InvalidToolArguments { .. } + | Self::InvalidToolArgumentSchema { .. } + | Self::InvalidToolPayloadReference { .. } => "E_INVALID_TOOL_ARGUMENTS", Self::Serialization(_) => "E_SERIALIZATION", Self::TooManyAttachments { .. } | Self::DuplicateAttachment { .. } @@ -1790,6 +1798,119 @@ fn validate_tool_call_arguments( }) } +fn executable_tool_arguments( + definition: &ToolDefinition, + tool_call: &ChatToolCall, + arguments: &Value, + run_log: &mut (impl RunLogWriter + ?Sized), +) -> Result { + if definition.name != ToolName::ApplyPatch { + return Ok(arguments.clone()); + } + + apply_patch_arguments_for_execution(tool_call, arguments, run_log) +} + +fn apply_patch_arguments_for_execution( + tool_call: &ChatToolCall, + arguments: &Value, + run_log: &mut (impl RunLogWriter + ?Sized), +) -> Result { + let object = arguments.as_object().ok_or_else(|| { + invalid_payload_ref(tool_call, "apply_patch arguments must be a JSON object") + })?; + let has_unified_diff = object.get("unifiedDiff").is_some(); + let payload_ref = object.get("payloadRef"); + + match (has_unified_diff, payload_ref) { + (true, None) => Ok(arguments.clone()), + (false, Some(payload_ref)) => { + let unified_diff = read_run_payload_ref(tool_call, payload_ref, run_log)?; + let mut materialized = object.clone(); + materialized.remove("payloadRef"); + materialized.insert("unifiedDiff".to_owned(), Value::String(unified_diff)); + Ok(Value::Object(materialized)) + } + (true, Some(_)) => Err(invalid_payload_ref( + tool_call, + "apply_patch must provide either unifiedDiff or payloadRef, not both", + )), + (false, None) => Err(invalid_payload_ref( + tool_call, + "apply_patch must provide unifiedDiff or payloadRef", + )), + } +} + +fn read_run_payload_ref( + tool_call: &ChatToolCall, + payload_ref: &Value, + run_log: &mut (impl RunLogWriter + ?Sized), +) -> Result { + let object = payload_ref + .as_object() + .ok_or_else(|| invalid_payload_ref(tool_call, "payloadRef must be an object"))?; + let kind = object.get("kind").and_then(Value::as_str); + if kind != Some("run_file") { + return Err(invalid_payload_ref( + tool_call, + "payloadRef.kind must be `run_file`", + )); + } + + let path = object + .get("path") + .and_then(Value::as_str) + .ok_or_else(|| invalid_payload_ref(tool_call, "payloadRef.path must be a string"))?; + let slash_path = path.replace('\\', "/"); + if !slash_path.starts_with("payloads/") { + return Err(invalid_payload_ref( + tool_call, + "payloadRef.path must be under payloads/", + )); + } + + let contents = run_log + .read_payload_file(Path::new(path)) + .map_err(|source| { + invalid_payload_ref(tool_call, format!("payloadRef.path is invalid: {source}")) + })? + .ok_or_else(|| { + invalid_payload_ref( + tool_call, + "active run log does not support payload references", + ) + })?; + if let Some(expected_size) = object.get("sizeBytes").and_then(Value::as_u64) { + let actual_size = contents.len() as u64; + if actual_size != expected_size { + return Err(invalid_payload_ref( + tool_call, + format!("payloadRef.sizeBytes expected {expected_size}, got {actual_size}"), + )); + } + } + if let Some(expected_sha256) = object.get("sha256").and_then(Value::as_str) { + let actual_sha256 = crate::hashing::sha256_hex(contents.as_bytes()); + if actual_sha256 != expected_sha256 { + return Err(invalid_payload_ref( + tool_call, + "payloadRef.sha256 does not match payload file", + )); + } + } + + Ok(contents) +} + +fn invalid_payload_ref(tool_call: &ChatToolCall, detail: impl Into) -> AgentTurnLoopError { + AgentTurnLoopError::InvalidToolPayloadReference { + tool_call_id: tool_call.id.clone(), + name: tool_call.function.name.clone(), + detail: detail.into(), + } +} + fn parse_tool_arguments( tool_call: &ChatToolCall, arguments: &Value, @@ -2283,7 +2404,11 @@ fn terminal_error_event( #[cfg(test)] mod tests { - use std::{collections::VecDeque, fs, path::PathBuf}; + use std::{ + collections::VecDeque, + fs, + path::{Path, PathBuf}, + }; use futures_util::stream; use serde_json::json; @@ -3033,6 +3158,138 @@ mod tests { assert_eq!(completed.payload["changedFiles"], json!(["README.md"])); } + #[tokio::test] + async fn turn_loop_applies_patch_from_run_scoped_payload_ref() { + let workspace = TestWorkspace::new("turn-loop"); + workspace.write("README.md", "old\n"); + let store = RunLogStore::new(workspace.path()).expect("run log store should open"); + let mut run = store + .create_run("run_turn_patch_payload_ref") + .expect("run should be created"); + let patch = "--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n"; + let split_at = patch + .find("@@") + .expect("fixture patch should contain a hunk"); + run.append_run_payload_chunk( + Path::new("payloads/apply_patch/patch.diff"), + &patch[..split_at], + ) + .expect("first payload chunk should append"); + run.append_run_payload_chunk( + Path::new("payloads/apply_patch/patch.diff"), + &patch[split_at..], + ) + .expect("second payload chunk should append"); + let provider = ScriptedProvider::new(vec![ + TurnProviderResponse::tool_calls( + None, + Some("I should edit the README.".to_owned()), + vec![ChatToolCall::function( + "call_1", + "apply_patch", + json!({ + "payloadRef": { + "kind": "run_file", + "path": "payloads/apply_patch/patch.diff", + "sha256": crate::hashing::sha256_hex(patch.as_bytes()), + "sizeBytes": patch.len(), + }, + "expectedFiles": ["README.md"], + }) + .to_string(), + )], + ), + TurnProviderResponse::final_text("Updated README."), + ]); + let mut loop_runner = + AgentTurnLoop::with_approval_policy(workspace.path(), provider, AutoApprovePolicy) + .expect("turn loop should initialize"); + + loop_runner + .run_turn(AgentTurnInput::new("turn_1", "Update README"), &mut run) + .await + .expect("payload-referenced patch should complete"); + + assert_eq!(workspace.read("README.md"), "new\n"); + let events = store + .load_run("run_turn_patch_payload_ref") + .expect("events should load"); + let requested = events + .iter() + .find(|event| event.event_type == "tool.requested") + .expect("tool.requested should be recorded"); + assert!( + requested.payload["argumentsPreview"] + .get("unifiedDiff") + .is_none() + ); + assert_eq!( + requested.payload["argumentsPreview"]["payloadRef"]["path"], + "payloads/apply_patch/patch.diff" + ); + let approval = events + .iter() + .find(|event| event.event_type == "tool.approvalRequired") + .expect("approval should be required"); + assert_eq!(approval.payload["hunks"].as_array().map(Vec::len), Some(1)); + } + + #[tokio::test] + async fn turn_loop_rejects_patch_payload_ref_hash_mismatch() { + let workspace = TestWorkspace::new("turn-loop"); + workspace.write("README.md", "old\n"); + let store = RunLogStore::new(workspace.path()).expect("run log store should open"); + let mut run = store + .create_run("run_turn_patch_payload_ref_hash_mismatch") + .expect("run should be created"); + let patch = "--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n"; + run.write_run_payload_file(Path::new("payloads/apply_patch/patch.diff"), patch) + .expect("payload file should be written"); + let provider = ScriptedProvider::new(vec![TurnProviderResponse::tool_calls( + None, + Some("I should edit the README.".to_owned()), + vec![ChatToolCall::function( + "call_1", + "apply_patch", + json!({ + "payloadRef": { + "kind": "run_file", + "path": "payloads/apply_patch/patch.diff", + "sha256": "0".repeat(64), + "sizeBytes": patch.len(), + }, + "expectedFiles": ["README.md"], + }) + .to_string(), + )], + )]); + let mut loop_runner = + AgentTurnLoop::with_approval_policy(workspace.path(), provider, AutoApprovePolicy) + .expect("turn loop should initialize"); + + let error = loop_runner + .run_turn(AgentTurnInput::new("turn_1", "Update README"), &mut run) + .await + .expect_err("hash mismatch should fail before tool execution"); + + assert!(matches!( + error, + AgentTurnLoopError::InvalidToolPayloadReference { .. } + )); + assert_eq!(workspace.read("README.md"), "old\n"); + let events = store + .load_run("run_turn_patch_payload_ref_hash_mismatch") + .expect("events should load"); + assert!(events.iter().any(|event| { + event.event_type == "run.failed" && event.payload["code"] == "E_INVALID_TOOL_ARGUMENTS" + })); + assert!( + !events + .iter() + .any(|event| event.event_type == "tool.requested") + ); + } + #[tokio::test] async fn turn_loop_applies_only_approved_patch_hunks() { let workspace = TestWorkspace::new("turn-loop"); From 8a8020c3cb356334618d333bb2c284b534b80e22 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:15:24 +0800 Subject: [PATCH 67/84] Updates documentation for run-scoped payload files and payloadRef --- docs/phase-tasks.md | 6 +++--- docs/run-log.md | 4 ++++ docs/testing.md | 2 +- docs/tool-system.md | 9 +++++---- docs/turn-loop.md | 4 +++- docs/vscode-extension.md | 2 +- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index c45dc31..229d5c3 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 拆分为子项管理,其中 P5-14a 到 P5-14c 已完成,P5-14d 起仍有 UX E2E 回归、大工具参数协议和真实试用 backlog 未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 拆分为子项管理,其中 P5-14a 到 P5-14e 已完成,P5-14f 真实试用 UX backlog 仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -132,8 +132,8 @@ | [x] | P5-14a:Sidebar 对话专属视图与就地确认 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板;approval 和 run delete 改为 webview 就地确认 UI,避免系统 modal 打断对话流。验收:已补充相关 Sidebar 状态和消息处理测试。 | | [x] | P5-14b:默认对话流、Work log 折叠和输入快捷键 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:默认只显示用户消息与 DeepSeek 回复;tool/provider/context/run completed 等过程事件收敛到默认折叠的 Work log;Work log 摘要只暴露当前工作状态;输入框 Enter 发送且 Shift+Enter 换行。验收:已补充 timeline / chat input 回归测试。 | | [x] | P5-14c:malformed tool-call 本地诊断文件 | `docs/json-rpc-protocol.md`、`docs/run-log.md`、`docs/turn-loop.md` | 已完成:当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在当前 run 的 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments,并在 `run.failed.diagnosticFile` / Sidebar failure card 指向该文件。验收:`cargo test -p prole-coder-agent-core diagnostic`、`pnpm --filter prole-coder-vscode test`。 | -| [ ] | P5-14d:Sidebar UX extension-host E2E 回归 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:用 extension-host 覆盖真实 webview 流程,包括 resume 后继续发送新 turn、默认事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、内联审批卡片、输入快捷键和 Work log 折叠展示。完成后再更新 Phase 5 验收记录。 | -| [ ] | P5-14e:大 patch / 大文件分块式工具参数协议 | `docs/tool-system.md`、`docs/turn-loop.md` | 未完成:为大 patch / 大文件写入设计 chunked tool payload 协议,允许 provider 分块流式输出到 run-scoped 临时文件,最后通过 commit/apply 调用统一进入 schema 校验、路径安全、审批和 hunk 边界;chunk 过程中不得边接收边执行写入。 | +| [x] | P5-14d:Sidebar UX extension-host E2E 回归 | `docs/testing.md`、`docs/vscode-extension.md` | 已完成:extension-host 测试新增 test-mode webview probe,覆盖真实 webview 中 resume 后继续发送新 turn、默认事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 默认折叠展示和 Enter / Shift+Enter 输入行为。验收:`pnpm run vscode:test-electron`。 | +| [x] | P5-14e:大 patch / 大文件分块式工具参数协议 | `docs/tool-system.md`、`docs/turn-loop.md`、`docs/run-log.md` | 已完成:Run Log 支持 run-scoped `payloads/` 文件写入、chunk append 和读取;`apply_patch` schema 新增 `payloadRef`,Turn Loop 可在 `tool.requested` 保留轻量 preview,并在执行前从当前 run 的 payload 文件 materialize `unifiedDiff`,校验 `sha256` / `sizeBytes` 后复用现有 schema、路径安全、审批和 hunk 边界。chunk 追加阶段不写 workspace。验收:`cargo test -p prole-coder-agent-core payload`、`cargo test -p prole-coder-agent-core apply_patch_schema_accepts_run_scoped_payload_refs`。 | | [ ] | P5-14f:真实试用 UX backlog 收敛 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,并把可执行项登记为 P5-14 子项或后续 Phase 任务。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/run-log.md b/docs/run-log.md index 7fc821b..9ab7711 100644 --- a/docs/run-log.md +++ b/docs/run-log.md @@ -20,6 +20,7 @@ Run Log 是 Agent Core 的本地审计记录。它记录一次 run 中发生的 /.prole-coder/runs//events.jsonl /.prole-coder/runs//summary.json /.prole-coder/runs//diagnostics/*.json +/.prole-coder/runs//payloads/** ``` `.prole-coder/` 已在 `.gitignore` 中排除,run log 不应进入 Git 仓库。 @@ -52,6 +53,8 @@ Run Log 本身负责 append/load 串行化,并允许 Turn Loop 在同一 run 当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments、JSON 解析错误和 run/turn/tool metadata;对应 `run.failed` payload 会带 `diagnosticFile` 路径,便于 VS Code Output 或 Sidebar failure card 直接定位。文件名中的短哈希来自原始 tool call id,用于避免 sanitize 或截断后的名称碰撞。诊断文件继续留在 `.prole-coder/`,不应上传或同步。 +大工具参数使用 `payloads/`:本地聚合器可以把 provider streaming chunk 追加到当前 run 的 payload 文件,最终 tool call 只通过 `payloadRef` 引用该文件。Run Log 只允许 workspace-relative run-scoped 路径,拒绝绝对路径和 `..`;payload 文件不会自动追加换行,避免改变 patch 内容。 + ## Summary Metadata 每个 run 创建时会同步创建 `summary.json`。之后每次成功追加事件,Run Log 会根据事件更新 summary: @@ -114,6 +117,7 @@ Run Log 本身负责 append/load 串行化,并允许 Turn Loop 在同一 run - 读取时发现序列缺口会失败。 - 写入前脱敏敏感字段和明显密钥片段。 - malformed tool-call arguments 会写入 run-scoped 诊断文件,并在 `run.failed.diagnosticFile` 暴露本地路径。 +- run-scoped payload 文件支持 chunk append 和读取,用于 `apply_patch.payloadRef` 这类大参数引用;chunk 追加不触碰 workspace 文件。 - 超长字符串和数组会被截断,并记录 `runLogTruncation` 元数据。 - `SerializedRunLog` 多线程 clone 并发追加时,仍生成连续 `seq`,并可被重新打开为正确的下一条序号。 - summary metadata 随事件追加更新,并可按最近更新时间列出。 diff --git a/docs/testing.md b/docs/testing.md index 521ec45..16a5170 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -90,7 +90,7 @@ Phase 5 P5-6 到 P5-13 的 API key/model、Git 工作流、Sidebar 连续会话 - `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、用户消息/DeepSeek 回复默认可见、过程事件收敛到 Work log,以及无 assistant 时 `run.completed` 摘要仍可见;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 -P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。当前已登记的 E2E 增强项是:在 extension-host 中覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 折叠展示和 Enter / Shift+Enter 输入行为。 +P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。P5-14d 已在 extension-host 中通过 test-mode webview probe 覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 折叠展示和 Enter / Shift+Enter 输入行为。 ## 新增测试的协作要求 diff --git a/docs/tool-system.md b/docs/tool-system.md index ef2cf47..74ded2e 100644 --- a/docs/tool-system.md +++ b/docs/tool-system.md @@ -1,6 +1,6 @@ # 工具系统 -状态:`0.1.0` 设计已确定,Phase 1 基础执行层、审批前 shell 动态风险升级、Phase 3 RPC/VS Code 审批接入、VS Code Native diff patch 预览、`apply_patch` hunk 级审批和命令子进程树清理已实现。 +状态:`0.1.0` 设计已确定,Phase 1 基础执行层、审批前 shell 动态风险升级、Phase 3 RPC/VS Code 审批接入、VS Code Native diff patch 预览、`apply_patch` hunk 级审批、run-scoped 大 payload 引用和命令子进程树清理已实现。 工具系统通过显式 schema 和类型化结果向 Agent Core 暴露工作区操作。模型不得直接执行文件写入、shell 命令或网络访问;它只能请求工具,工具请求必须经过 schema 校验和审批策略。 @@ -170,7 +170,8 @@ pub struct ToolDefinition { 参数: -- `unifiedDiff`:统一 diff。 +- `unifiedDiff`:统一 diff。小 patch 可直接内联该字段。 +- `payloadRef`:可选 run-scoped 大 payload 引用;与 `unifiedDiff` 互斥。当前支持 `{ kind: "run_file", path, sha256?, sizeBytes? }`,`path` 必须位于当前 run 的 `payloads/` 目录下。 - `expectedFiles`:预期修改文件列表。 结果: @@ -303,7 +304,7 @@ fixture 中的 `tools` 被当作无序集合校验;测试会按工具名规整 - `workspace_manifest`:生成 workspace manifest v0,默认遵守 `.gitignore` 和 `.prole-coderignore`,硬排除 `.git/`、`.secrets/`、`.secret/`、`.agents/`、`.codex/` 和 `.prole-coder/`,并返回稳定排序条目、manifest hash、git 状态和截断原因。 - `read_file`:只读取 workspace 内 UTF-8 文本文件,支持 1-based 行范围,并返回完整文件的 `sha256` 和 `sizeBytes`。 - `search`:通过 `rg --json --fixed-strings` 搜索,默认排除 `.git/`、`.secrets/`、`.secret/`、`.env*`、`node_modules/` 和 `target/`。 -- `apply_patch`:应用受限 unified diff,要求 patch 实际文件集合与 `expectedFiles` 完全一致;执行时会先在内存中完成全部文件的 hunk 校验和 staging,再统一写盘,因此解析或 hunk mismatch 不会留下部分文件已修改的状态;成功后返回 reverse patch。Core 会从 unified diff 生成稳定 hunk id,RPC/VS Code 首版支持 selected hunk 审批;文件创建和删除如果只批准部分 hunk 会被拒绝,避免生成不可审计的半文件操作。 +- `apply_patch`:应用受限 unified diff,要求 patch 实际文件集合与 `expectedFiles` 完全一致;执行时会先在内存中完成全部文件的 hunk 校验和 staging,再统一写盘,因此解析或 hunk mismatch 不会留下部分文件已修改的状态;成功后返回 reverse patch。Core 会从 unified diff 生成稳定 hunk id,RPC/VS Code 首版支持 selected hunk 审批;文件创建和删除如果只批准部分 hunk 会被拒绝,避免生成不可审计的半文件操作。大 patch 可以先分块追加到当前 run 的 `payloads/` 文件,再通过 `payloadRef` 引用;Turn Loop 会校验 `sha256` / `sizeBytes`,然后 materialize 为 `unifiedDiff` 进入同一条 schema、路径安全、审批和 hunk 边界链路。 - `shell`:在 workspace 内执行非交互式命令,支持超时,执行前进行命令风险分类,返回 exit code、stdout、stderr 和耗时。 - `git_status`:读取 `git status --short --branch` 或普通 `git status`。 - `git_diff`:读取 unstaged 或 staged diff,支持限定 workspace-relative 路径。 @@ -358,7 +359,7 @@ Schema 校验不能只作为 typed deserialization 失败后的补救,因为 R - 当前实现只支持受限 unified diff;后续需要支持更完整的 git patch 语法,包括 rename、copy、mode change 和更严格的 no-newline 语义。 - 已增加 VS Code patch 预览和 `apply_patch` hunk 级审批;后续继续增强冲突诊断、失败时的精确 hunk mismatch 信息,以及 rename/copy/mode change 等更完整 patch 语法下的审批边界。 -- 为大 patch / 大文件写入设计分块式工具参数协议:provider 可以流式产生 chunk,本地写入 run-scoped 临时文件,最后通过 commit/apply 调用统一进入 schema 校验、路径安全、审批和 hunk 边界;不要在 chunk 过程中边接收边执行写入。 +- 已完成 `apply_patch.payloadRef` 第一版:provider 或本地聚合器可以把大 patch 分块追加到 run-scoped `payloads/` 文件,最终调用 `apply_patch` 时只传轻量引用;后续可继续扩展为显式 RPC chunk 方法、payload GC 和更完整的大文件写入协议。 - 用修改前快照生成 reverse patch,并在 run log 中保存 patch id、审批 id 和可审计回滚信息。 - 如果需要抵抗磁盘写入中途失败,应进一步引入临时文件、原子替换或备份恢复机制;当前 staging 主要保证解析和 hunk 校验失败不会产生半应用 patch。 - 明确二进制文件和生成文件策略,避免文本 patch 意外改写不可审计内容。 diff --git a/docs/turn-loop.md b/docs/turn-loop.md index 574cec6..e563297 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -74,6 +74,8 @@ provider stream 中的 content delta 会立即写入 `assistant.delta`,payload DeepSeek streaming tool call delta 在 CLI provider wrapper 内通过 `ChatToolCallAccumulator` 拼装为完整 `ChatToolCall` 后才进入 `Completed.tool_calls`。Turn Loop 不直接处理 provider 私有 delta 形态,只要求 provider 在 `Completed` 中提供完整、可校验、可执行的工具调用列表。如果累计后的 `function.arguments` 不是合法 JSON,Turn Loop 会以 `E_INVALID_TOOL_ARGUMENTS` 失败,并把脱敏后的累计 arguments 写入当前 run 的 `diagnostics/invalid-tool-arguments--.json`,同时在 `run.failed.diagnosticFile` 暴露该本地路径。 +`apply_patch` 支持大 payload 引用:`tool.requested.argumentsPreview` 可以只包含 `payloadRef`,Turn Loop 在执行前从当前 run 的 `payloads/` 文件读取完整 diff,校验 `sha256` / `sizeBytes` 后 materialize 为 `unifiedDiff`。materialize 之后仍走同一套 schema 校验、路径安全、审批、hunk metadata、selected hunk 过滤和 patch staging;chunk 追加阶段不会写 workspace。 + `run.started` 记录规范化后的 workspace root,用于本地审计和前端展示当前 run 绑定的工作区。该路径只应进入本地 run log 和本机前端事件流,不应被上传到公开仓库或远程日志。 ## 审批边界 @@ -105,7 +107,7 @@ RPC active run 的 Run Log 使用 `SerializedRunLog`:后台 Turn Loop worker - Turn Loop 每次成功追加 Run Log 事件后,会把同一条事件交给 `TurnEventSink`,sink 看到的事件序列与本地 `events.jsonl` 一致。 - `SerializedRunLog` 并发 append 测试验证多个 clone 同时写同一 run 时仍生成连续 `seq`。 - DeepSeek wrapper 能把 streaming tool call delta 拼成完整工具调用,并在缺少必要 metadata 时失败。 -- tool call arguments 会先解析为 JSON,再按工具注册表 JSON Schema 校验,最后进入 typed deserialization;malformed JSON、未知字段和错误类型会返回 `E_INVALID_TOOL_ARGUMENTS`,malformed JSON 还会写入脱敏诊断文件。 +- tool call arguments 会先解析为 JSON,再按工具注册表 JSON Schema 校验,最后进入 typed deserialization;malformed JSON、未知字段、错误类型和非法 `payloadRef` 会返回 `E_INVALID_TOOL_ARGUMENTS`,malformed JSON 还会写入脱敏诊断文件。 这些测试验证的是模块集成骨架,不需要真实 DeepSeek API Key,也不会联网。真实 tool call delta 形态由 `live_streaming_tool_call_accumulator_smoke_test` 作为手动 opt-in live test 验收。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 7ea6312..2efa015 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX 和结构化 provider 配置错误恢复均已实现;P5-14 持续 UX 测试与体验改进已拆分为子项,仍有未完成项。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX、结构化 provider 配置错误恢复和 P5-14d Sidebar UX extension-host 回归均已实现;P5-14 持续 UX 测试与体验改进已拆分为子项,仍有未完成项。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 From 22d0b2dfdc387f4bd72a346e9ee6171409a2435c Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:15:24 +0800 Subject: [PATCH 68/84] Adds test probe infrastructure for extension-host E2E tests --- .../scripts/runVscodeIntegrationTests.mjs | 2 +- vscode/extension/src/chatView.ts | 224 +++++++++++++++++- vscode/extension/src/extension.ts | 3 + 3 files changed, 227 insertions(+), 2 deletions(-) diff --git a/vscode/extension/scripts/runVscodeIntegrationTests.mjs b/vscode/extension/scripts/runVscodeIntegrationTests.mjs index edac8c5..8e31c27 100644 --- a/vscode/extension/scripts/runVscodeIntegrationTests.mjs +++ b/vscode/extension/scripts/runVscodeIntegrationTests.mjs @@ -18,7 +18,7 @@ const extensionsPath = resolve(repoRoot, "target", `vscode-test-extensions-${pro delete process.env.ELECTRON_RUN_AS_NODE; process.env.PROLE_CODER_VSCODE_TEST = "1"; -process.env.PROLE_CODER_VSCODE_TEST_AUTO_APPROVE = "1"; +process.env.PROLE_CODER_VSCODE_TEST_AUTO_APPROVE = "0"; process.env.PROLE_CODER_VSCODE_TEST_RPC_LOG = rpcFixtureLogPath; mkdirSync(dirname(settingsPath), { recursive: true }); diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 28c83e4..86ad429 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -113,7 +113,22 @@ type ExtensionToWebviewMessage = | SubmissionWebviewMessage | RunsWebviewMessage | ContextWebviewMessage - | ApprovalWebviewMessage; + | ApprovalWebviewMessage + | TestProbeWebviewMessage; + +interface TestProbeWebviewMessage { + readonly type: "testProbe"; + readonly id: string; + readonly action: unknown; +} + +interface TestProbeResultWebviewMessage { + readonly type: "testProbeResult"; + readonly id: string; + readonly ok: boolean; + readonly result?: unknown; + readonly error?: string; +} type ChatSubmissionStatus = "idle" | "sending" | "running" | "completed" | "failed" | "canceled"; type TerminalSubmissionStatus = Extract; @@ -154,6 +169,12 @@ interface PendingApprovalState { resolve(decision: ApprovalPromptDecision): void; } +interface PendingTestProbe { + readonly timeout: ReturnType; + resolve(result: unknown): void; + reject(error: Error): void; +} + export class ProleChatViewProvider implements vscode.WebviewViewProvider, DisposableLike { private readonly timeline = new ChatEventTimeline(); private readonly terminalRuns = new Map(); @@ -163,6 +184,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private contextViz: ContextVizSnapshot = emptyContextViz(); private activeConversationRunId: string | undefined; private pendingApproval: PendingApprovalState | undefined; + private readonly pendingTestProbes = new Map(); private rpcSubscription: DisposableLike | undefined; private viewMessageSubscription: DisposableLike | undefined; private view: vscode.WebviewView | undefined; @@ -242,12 +264,39 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos }; } + async testProbeWebview(action: unknown): Promise { + if (this.view === undefined) { + throw new Error("chat webview is not available"); + } + + const id = randomUUID(); + const message: TestProbeWebviewMessage = { + type: "testProbe", + id, + action, + }; + + return await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.pendingTestProbes.delete(id); + reject(new Error(`timed out waiting for chat webview test probe ${id}`)); + }, 2000); + this.pendingTestProbes.set(id, { timeout, resolve, reject }); + void this.view?.webview.postMessage(message).then((delivered) => { + if (!delivered) { + this.rejectTestProbe(id, new Error("chat webview did not accept the test probe message")); + } + }); + }); + } + isIdle(): boolean { return !this.submission.busy; } dispose(): void { this.rejectPendingApproval("approval view disposed"); + this.rejectPendingTestProbes("chat view disposed"); this.rpcSubscription?.dispose(); this.viewMessageSubscription?.dispose(); this.rpcSubscription = undefined; @@ -268,6 +317,12 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } private async handleWebviewMessage(message: unknown): Promise { + const testProbeResult = testProbeResultFromMessage(message); + if (testProbeResult !== undefined) { + this.resolveTestProbe(testProbeResult); + return; + } + if (isRefreshRunsMessage(message)) { await this.refreshRuns(); return; @@ -687,6 +742,40 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos }); } + private resolveTestProbe(message: TestProbeResultWebviewMessage): void { + const pending = this.pendingTestProbes.get(message.id); + if (pending === undefined) { + return; + } + + clearTimeout(pending.timeout); + this.pendingTestProbes.delete(message.id); + if (message.ok) { + pending.resolve(message.result); + } else { + pending.reject(new Error(message.error ?? "chat webview test probe failed")); + } + } + + private rejectTestProbe(id: string, error: Error): void { + const pending = this.pendingTestProbes.get(id); + if (pending === undefined) { + return; + } + + clearTimeout(pending.timeout); + this.pendingTestProbes.delete(id); + pending.reject(error); + } + + private rejectPendingTestProbes(reason: string): void { + for (const [id, pending] of this.pendingTestProbes) { + clearTimeout(pending.timeout); + pending.reject(new Error(reason)); + this.pendingTestProbes.delete(id); + } + } + private redact(message: string): string { return this.redactor?.redact(message) ?? message; } @@ -1616,6 +1705,7 @@ function renderChatViewHtml( const initialApproval = ${initialApproval}; const runModes = ${safeScriptJson(CHAT_RUN_MODES)}; const defaultMode = ${safeScriptJson(DEFAULT_CHAT_MODE)}; + const testProbeEnabled = ${safeScriptJson(process.env["PROLE_CODER_VSCODE_TEST"] === "1")}; const WORK_LOG_RENDER_LIMIT = 80; const WORK_LOG_STATUS_IGNORED_TYPES = new Set(["run.completed"]); const vscodeApi = acquireVsCodeApi(); @@ -1658,6 +1748,10 @@ function renderChatViewHtml( window.addEventListener("message", (event) => { const message = event.data; + if (message && message.type === "testProbe") { + handleTestProbe(message); + return; + } if (message && message.type === "snapshot") { render(message.snapshot); } @@ -1809,6 +1903,7 @@ function renderChatViewHtml( button.className = "run-entry " + status + (run.runId === selectedRunId ? " selected" : ""); button.disabled = disabled || confirmingDelete; button.title = runId; + row.dataset.runId = runId; button.addEventListener("click", () => { if (runId.length > 0) { vscodeApi.postMessage({ type: "resumeRun", runId }); @@ -1833,6 +1928,7 @@ function renderChatViewHtml( deleteButton.disabled = disabled; deleteButton.title = "Delete run"; deleteButton.setAttribute("aria-label", "Delete run"); + deleteButton.dataset.runId = runId; deleteButton.textContent = confirmingDelete ? "-" : "x"; deleteButton.addEventListener("click", (event) => { event.stopPropagation(); @@ -2586,6 +2682,112 @@ function renderChatViewHtml( return article; } + + function handleTestProbe(message) { + if (testProbeEnabled !== true) { + return; + } + + const id = typeof message.id === "string" ? message.id : ""; + if (!id) { + return; + } + + try { + vscodeApi.postMessage({ + type: "testProbeResult", + id, + ok: true, + result: runTestProbe(message.action), + }); + } catch (error) { + vscodeApi.postMessage({ + type: "testProbeResult", + id, + ok: false, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + function runTestProbe(action) { + const type = action && typeof action === "object" && typeof action.type === "string" ? action.type : "snapshot"; + if (type === "click") { + const selector = typeof action.selector === "string" ? action.selector : ""; + const element = selector ? document.querySelector(selector) : undefined; + if (element && typeof element.click === "function") { + element.click(); + return { clicked: true, snapshot: collectTestSnapshot() }; + } + return { clicked: false, snapshot: collectTestSnapshot() }; + } + + if (type === "setPrompt") { + promptInput.value = typeof action.value === "string" ? action.value : ""; + promptInput.dispatchEvent(new Event("input", { bubbles: true })); + return { value: promptInput.value, snapshot: collectTestSnapshot() }; + } + + if (type === "keydown") { + const event = new KeyboardEvent("keydown", { + key: typeof action.key === "string" ? action.key : "Enter", + shiftKey: action.shiftKey === true, + bubbles: true, + cancelable: true, + }); + promptInput.dispatchEvent(event); + return { + defaultPrevented: event.defaultPrevented, + snapshot: collectTestSnapshot(), + }; + } + + return { snapshot: collectTestSnapshot() }; + } + + function collectTestSnapshot() { + const workLog = document.querySelector(".item.work-log"); + const workLogSummary = workLog ? workLog.querySelector("summary .title") : undefined; + return { + conversationActive: shellRoot.classList.contains("conversation-active"), + statusHidden: isDisplayNone(document.querySelector(".status")), + runsHidden: isDisplayNone(document.getElementById("runs-section")), + contextHidden: isDisplayNone(document.querySelector(".context-viz")), + approvalVisible: approvalRoot.classList.contains("active"), + approvalTitle: textContent(".approval-title"), + approvalActionLabels: textContents(".approval-action"), + runDeleteConfirmVisible: document.querySelector(".run-delete-confirm") !== null, + runIds: textAttributes(".run-entry-row", "data-run-id"), + visibleItemTitles: textContents("#events > .item:not(.work-log) .title"), + visibleItemTypes: textContents("#events > .item:not(.work-log) .type"), + workLogVisible: workLog !== null, + workLogOpen: workLog && "open" in workLog ? workLog.open === true : false, + workLogTitle: workLogSummary ? workLogSummary.textContent || "" : "", + workLogTypes: textContents(".work-log-row-meta span:last-child"), + promptValue: promptInput.value, + sendDisabled: sendButton.disabled, + cancelDisabled: cancelButton.disabled, + }; + } + + function isDisplayNone(element) { + return !element || getComputedStyle(element).display === "none"; + } + + function textContent(selector) { + const element = document.querySelector(selector); + return element ? element.textContent || "" : ""; + } + + function textContents(selector) { + return Array.from(document.querySelectorAll(selector)).map((element) => element.textContent || ""); + } + + function textAttributes(selector, attribute) { + return Array.from(document.querySelectorAll(selector)) + .map((element) => element.getAttribute(attribute) || "") + .filter((value) => value.length > 0); + } `; @@ -2708,6 +2910,26 @@ function isShowRunsMessage(message: unknown): boolean { return isRecord(message) && message["type"] === "showRuns"; } +function testProbeResultFromMessage(message: unknown): TestProbeResultWebviewMessage | undefined { + if (!isRecord(message) || message["type"] !== "testProbeResult") { + return undefined; + } + + const id = message["id"]; + const ok = message["ok"]; + if (typeof id !== "string" || typeof ok !== "boolean") { + return undefined; + } + + return { + type: "testProbeResult", + id, + ok, + ...(message["result"] === undefined ? {} : { result: message["result"] }), + ...(typeof message["error"] === "string" ? { error: message["error"] } : {}), + }; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } diff --git a/vscode/extension/src/extension.ts b/vscode/extension/src/extension.ts index 3c5d541..56cfa3e 100644 --- a/vscode/extension/src/extension.ts +++ b/vscode/extension/src/extension.ts @@ -225,6 +225,9 @@ function registerTestCommands(context: vscode.ExtensionContext, chatView: ProleC chatView.testHandleWebviewMessage(message), ), vscode.commands.registerCommand("prole-coder.test.chatState", () => chatView.testState()), + vscode.commands.registerCommand("prole-coder.test.chatProbe", (action: unknown) => + chatView.testProbeWebview(action), + ), ); } From 4321f13f9cef7274b3a9268864a1e344c0c0cb9d Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:15:25 +0800 Subject: [PATCH 69/84] Extends extension-host E2E tests with webview probe coverage --- vscode/extension/test/electron/index.ts | 209 +++++++++++++++++- .../test/fixtures/rpcFixtureServer.mjs | 8 +- 2 files changed, 211 insertions(+), 6 deletions(-) diff --git a/vscode/extension/test/electron/index.ts b/vscode/extension/test/electron/index.ts index 787d482..63a7f75 100644 --- a/vscode/extension/test/electron/index.ts +++ b/vscode/extension/test/electron/index.ts @@ -6,6 +6,7 @@ import * as vscode from "vscode"; const extensionId = "prole-coder.prole-coder-vscode"; const TEST_CHAT_MESSAGE_COMMAND = "prole-coder.test.chatMessage"; const TEST_CHAT_STATE_COMMAND = "prole-coder.test.chatState"; +const TEST_CHAT_PROBE_COMMAND = "prole-coder.test.chatProbe"; const CONTRIBUTED_COMMANDS = [ "prole-coder.openChat", "prole-coder.openSettings", @@ -36,11 +37,13 @@ export async function run(): Promise { } assert.equal(commands.includes(TEST_CHAT_MESSAGE_COMMAND), true); assert.equal(commands.includes(TEST_CHAT_STATE_COMMAND), true); + assert.equal(commands.includes(TEST_CHAT_PROBE_COMMAND), true); await vscode.commands.executeCommand("prole-coder.openChat"); await exerciseChatSendTurnDiagnosticsAndApproval(); await exerciseChatCancel(); await exerciseRunListAndResume(); + await exerciseChatKeyboardSubmit(); } async function exerciseChatSendTurnDiagnosticsAndApproval(): Promise { @@ -64,17 +67,45 @@ async function exerciseChatSendTurnDiagnosticsAndApproval(): Promise { mode: "edit", }); + const approvalReady = await waitFor("inline approval card in chat webview", async () => { + const current = await chatState(); + return current.approval?.approvalId === "approval-run-approval-1-turn-1" && + current.submission.runId === "run-approval-1" + ? current + : undefined; + }); + assert.equal(approvalReady.context.status, "ready"); + assert.equal(approvalReady.timeline.latestRunId, "run-approval-1"); + assert.ok(approvalReady.timeline.items.some((item) => item.type === "tool.approvalRequired")); + + const approvalProbe = await chatProbeSnapshot(); + assert.equal(approvalProbe.conversationActive, true); + assert.equal(approvalProbe.runsHidden, true); + assert.equal(approvalProbe.contextHidden, true); + assert.equal(approvalProbe.approvalVisible, true); + assert.equal(approvalProbe.workLogVisible, true); + assert.equal(approvalProbe.workLogOpen, false); + assert.ok(approvalProbe.visibleItemTitles.includes("You")); + assert.ok(!approvalProbe.visibleItemTitles.includes("Approval required: shell")); + + await chatProbe({ type: "click", selector: ".approval-action.approve" }); + const state = await waitFor("completed approval-backed chat turn", async () => { const current = await chatState(); return current.submission.status === "completed" && - current.submission.runId === "run-approval-1" + current.submission.runId === "run-approval-1" && + current.timeline.items.some((item) => item.type === "tool.approvalResolved") ? current : undefined; }); assert.equal(state.context.status, "ready"); - assert.equal(state.timeline.latestRunId, "run-approval-1"); - assert.ok(state.timeline.items.some((item) => item.type === "tool.approvalRequired")); assert.ok(state.timeline.items.some((item) => item.type === "tool.approvalResolved")); + const completedProbe = await chatProbeSnapshot(); + assert.equal(completedProbe.approvalVisible, false); + assert.equal(completedProbe.workLogVisible, true); + assert.equal(completedProbe.workLogOpen, false); + assert.ok(completedProbe.visibleItemTitles.includes("You")); + assert.ok(completedProbe.visibleItemTitles.includes("DeepSeek")); const sendTurn = await waitFor("logged sendTurn with diagnostics", async () => logEntry((entry) => entry.method === "agent.sendTurn" && entry.params?.message === "integration approval flow"), @@ -91,7 +122,7 @@ async function exerciseChatSendTurnDiagnosticsAndApproval(): Promise { ); const approve = await waitFor("logged approval response", async () => - logEntry((entry) => entry.method === "agent.approve" && entry.params?.approvalId === "approval-approval-1"), + logEntry((entry) => entry.method === "agent.approve" && entry.params?.approvalId === "approval-run-approval-1-turn-1"), ); assert.equal(approve.params?.persist, "never"); } finally { @@ -172,6 +203,125 @@ async function exerciseRunListAndResume(): Promise { await waitFor("logged resume request", async () => logEntry((entry) => entry.method === "agent.resume" && entry.params?.runId === "run-history-1"), ); + + await postChatMessage({ + type: "submitTurn", + message: "integration resume follow up", + mode: "edit", + }); + + await waitFor("inline approval for resumed run follow-up", async () => { + const current = await chatState(); + return current.approval?.approvalId === "approval-run-history-1-turn-3" ? current : undefined; + }); + await chatProbe({ type: "click", selector: ".approval-action.approve" }); + + const continued = await waitFor("completed resumed run follow-up turn", async () => { + const current = await chatState(); + return current.submission.status === "completed" && + current.submission.runId === "run-history-1" && + current.timeline.items.some((item) => item.turnId === "turn-3" && item.type === "run.completed") + ? current + : undefined; + }); + assert.equal(continued.runs.selectedRunId, "run-history-1"); + + const resumedSendTurn = await waitFor("logged resumed sendTurn with runId", async () => + logEntry( + (entry) => + entry.method === "agent.sendTurn" && + entry.params?.message === "integration resume follow up" && + entry.params?.runId === "run-history-1", + ), + ); + assert.equal(resumedSendTurn.params?.mode, "edit"); + + await chatProbe({ type: "click", selector: "#show-runs" }); + const runsProbe = await waitFor("runs visible after conversation back", async () => { + const snapshot = await chatProbeSnapshot(); + return snapshot.conversationActive === false && snapshot.runIds.includes("run-history-1") + ? snapshot + : undefined; + }); + assert.equal(runsProbe.runsHidden, false); + + await chatProbe({ type: "click", selector: '.run-entry-row[data-run-id="run-history-1"] .run-delete' }); + const deleteProbe = await chatProbeSnapshot(); + assert.equal(deleteProbe.runDeleteConfirmVisible, true); + + await chatProbe({ type: "click", selector: '.run-entry-row[data-run-id="run-history-1"] .confirm-delete' }); + + const deleted = await waitFor("run deleted through inline webview confirmation", async () => { + const current = await chatState(); + return current.runs.status === "ready" && current.runs.runs.every((run) => run.runId !== "run-history-1") + ? current + : undefined; + }); + assert.equal(deleted.runs.selectedRunId, undefined); + + await waitFor("logged delete run request", async () => + logEntry((entry) => entry.method === "agent.deleteRun" && entry.params?.runId === "run-history-1"), + ); +} + +async function exerciseChatKeyboardSubmit(): Promise { + await postChatMessage({ + type: "showRuns", + }); + await chatProbe({ type: "setPrompt", value: "keyboard integration turn" }); + + const shifted = await chatProbe({ type: "keydown", key: "Enter", shiftKey: true }); + assert.equal(shifted.defaultPrevented, false); + + const submitted = await chatProbe({ type: "keydown", key: "Enter" }); + assert.equal(submitted.defaultPrevented, true); + + const sentTurn = await waitFor("logged keyboard sendTurn", async () => + logEntry((entry) => entry.method === "agent.sendTurn" && entry.params?.message === "keyboard integration turn"), + ); + assert.equal(sentTurn.params?.message, "keyboard integration turn"); + const turnStarted = await waitFor("logged keyboard turn start", async () => + logEntry( + (entry) => + entry.kind === "event" && + entry.event?.type === "turn.started" && + entry.event.payload?.userTask === "keyboard integration turn", + ), + ); + const sentRunId = turnStarted.event?.runId; + const sentTurnId = turnStarted.event?.turnId; + assert.ok(typeof sentRunId === "string"); + assert.ok(typeof sentTurnId === "string"); + const approvalRequired = await waitFor("logged keyboard approval request", async () => + logEntry( + (entry) => + entry.kind === "event" && + entry.event?.type === "tool.approvalRequired" && + entry.event.runId === sentRunId && + entry.event.turnId === sentTurnId && + typeof entry.event.payload?.approvalId === "string", + ), + ); + const expectedApprovalId = approvalRequired.event?.payload?.approvalId; + assert.ok(typeof expectedApprovalId === "string"); + + await waitFor("keyboard-submitted turn waiting for approval", async () => { + const current = await chatState(); + return current.approval?.approvalId === expectedApprovalId ? current : undefined; + }); + await chatProbe({ type: "click", selector: ".approval-action.approve" }); + + await waitFor("keyboard-submitted turn completed", async () => { + const current = await chatState(); + return current.submission.status === "completed" && + current.timeline.items.some((item) => item.type === "run.completed") + ? current + : undefined; + }); + + await waitFor("logged keyboard turn approval", async () => + logEntry((entry) => entry.method === "agent.approve" && entry.params?.approvalId === expectedApprovalId), + ); } async function postChatMessage(message: unknown): Promise { @@ -182,6 +332,15 @@ async function chatState(): Promise { return await vscode.commands.executeCommand(TEST_CHAT_STATE_COMMAND); } +async function chatProbe(action: Record): Promise { + return await vscode.commands.executeCommand(TEST_CHAT_PROBE_COMMAND, action); +} + +async function chatProbeSnapshot(): Promise { + const result = await chatProbe({ type: "snapshot" }); + return result.snapshot; +} + async function waitFor( label: string, predicate: () => Promise, @@ -266,6 +425,7 @@ interface ChatState { readonly latestRunId?: string; readonly items: ReadonlyArray<{ readonly type: string; + readonly turnId?: string; }>; }; readonly submission: { @@ -283,9 +443,13 @@ interface ChatState { readonly context: { readonly status: string; }; + readonly approval?: { + readonly approvalId: string; + }; } interface RpcFixtureLogEntry { + readonly kind?: string; readonly method?: string; readonly params?: { readonly message?: string; @@ -296,4 +460,41 @@ interface RpcFixtureLogEntry { readonly reason?: string; readonly attachments?: ReadonlyArray>; }; + readonly event?: { + readonly type?: string; + readonly runId?: string; + readonly turnId?: string; + readonly payload?: { + readonly approvalId?: string; + readonly userTask?: string; + }; + }; +} + +interface WebviewProbeSnapshotResult { + readonly clicked?: boolean; + readonly defaultPrevented?: boolean; + readonly value?: string; + readonly snapshot: WebviewProbeSnapshot; +} + +interface WebviewProbeSnapshot { + readonly conversationActive: boolean; + readonly statusHidden: boolean; + readonly runsHidden: boolean; + readonly contextHidden: boolean; + readonly approvalVisible: boolean; + readonly approvalTitle: string; + readonly approvalActionLabels: readonly string[]; + readonly runDeleteConfirmVisible: boolean; + readonly runIds: readonly string[]; + readonly visibleItemTitles: readonly string[]; + readonly visibleItemTypes: readonly string[]; + readonly workLogVisible: boolean; + readonly workLogOpen: boolean; + readonly workLogTitle: string; + readonly workLogTypes: readonly string[]; + readonly promptValue: string; + readonly sendDisabled: boolean; + readonly cancelDisabled: boolean; } diff --git a/vscode/extension/test/fixtures/rpcFixtureServer.mjs b/vscode/extension/test/fixtures/rpcFixtureServer.mjs index 04e22a1..d907b1b 100644 --- a/vscode/extension/test/fixtures/rpcFixtureServer.mjs +++ b/vscode/extension/test/fixtures/rpcFixtureServer.mjs @@ -135,8 +135,8 @@ function handleSendTurn(request) { return; } - const approvalId = "approval-approval-1"; - const toolCallId = "tool-approval-1"; + const approvalId = `approval-${encodeIdSegment(runId)}-${encodeIdSegment(turnId)}`; + const toolCallId = `tool-${encodeIdSegment(runId)}-${encodeIdSegment(turnId)}`; pendingApprovals.set(approvalId, { runId, turnId, toolCallId }); emitEvent(runId, turnId, "tool.approvalRequired", { approvalId, @@ -415,3 +415,7 @@ function log(entry) { function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined; } + +function encodeIdSegment(value) { + return encodeURIComponent(String(value)); +} From 6cd83fc56287e4449a4b36b226553f19505ae7ed Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:24:14 +0800 Subject: [PATCH 70/84] Fixes run log file opening to use append-only mode --- crates/agent-core/src/run_log.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/agent-core/src/run_log.rs b/crates/agent-core/src/run_log.rs index 5d79cc8..08e18e6 100644 --- a/crates/agent-core/src/run_log.rs +++ b/crates/agent-core/src/run_log.rs @@ -792,7 +792,6 @@ fn append_run_text_file( })?; } let mut file = OpenOptions::new() - .write(true) .create(true) .append(true) .open(&path) From a31419755a2e04abd2e9ead63ce85a022e837ad7 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Wed, 3 Jun 2026 22:24:14 +0800 Subject: [PATCH 71/84] Maps InvalidToolPayloadReference to invalid tool arguments RPC code --- crates/cli/src/lib.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index 956f80d..340bbf5 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -845,7 +845,8 @@ fn turn_loop_error_json_rpc_code(error: &AgentTurnLoopError) -> i64 { | AgentTurnLoopError::UnsupportedTool { .. } | AgentTurnLoopError::Serialization(_) => RPC_TOOL_EXECUTION_FAILED, AgentTurnLoopError::InvalidToolArguments { .. } - | AgentTurnLoopError::InvalidToolArgumentSchema { .. } => RPC_INVALID_TOOL_ARGUMENTS, + | AgentTurnLoopError::InvalidToolArgumentSchema { .. } + | AgentTurnLoopError::InvalidToolPayloadReference { .. } => RPC_INVALID_TOOL_ARGUMENTS, AgentTurnLoopError::Canceled { .. } | AgentTurnLoopError::ApprovalCanceled { .. } | AgentTurnLoopError::ApprovalExpired { .. } => RPC_RUN_CANCELED, @@ -1569,11 +1570,11 @@ mod tests { }, run_log::{REDACTED_VALUE, RunLogStore}, test_helpers::TestWorkspace, - turn_loop::TurnProviderEvent, + turn_loop::{AgentTurnLoopError, TurnProviderEvent}, }; use prole_coder_agent_rpc::{ - FimPreviewParams, PROTOCOL_VERSION, RPC_APPROVAL_DENIED, RPC_PROVIDER_ERROR, - RPC_TOOL_EXECUTION_FAILED, RpcTurnProviderFactory, + FimPreviewParams, PROTOCOL_VERSION, RPC_APPROVAL_DENIED, RPC_INVALID_TOOL_ARGUMENTS, + RPC_PROVIDER_ERROR, RPC_TOOL_EXECUTION_FAILED, RpcTurnProviderFactory, }; use serde_json::{Value, json}; use std::{ @@ -1586,7 +1587,7 @@ mod tests { use super::{ CliCommand, CliRpcProviderFactory, FixtureKind, ProviderKind, RunCommand, ThinkingKind, deepseek_chat_stream_to_turn_provider_stream, deepseek_configuration_rpc_error, run_cli, - run_cli_with_input, + run_cli_with_input, turn_loop_error_json_rpc_code, }; #[test] @@ -2109,6 +2110,20 @@ mod tests { assert_eq!(data["recoverableAction"]["label"], "Configure API Key"); } + #[test] + fn invalid_tool_payload_ref_maps_to_invalid_tool_arguments_rpc_code() { + let error = AgentTurnLoopError::InvalidToolPayloadReference { + tool_call_id: "call_1".to_owned(), + name: "apply_patch".to_owned(), + detail: "payloadRef.sha256 does not match payload file".to_owned(), + }; + + assert_eq!( + turn_loop_error_json_rpc_code(&error), + RPC_INVALID_TOOL_ARGUMENTS + ); + } + type InteractiveCliRpc = ( mpsc::Sender, SharedOutput, From 51bc4a975788c8e49b5158a9a76b1011b458ce73 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Thu, 4 Jun 2026 20:14:25 +0800 Subject: [PATCH 72/84] Improve VS Code UX and provider resilience - Add Sidebar Settings access and RPC args docs - Retry transient DeepSeek send failures - Improve Windows shell encoding and length-limit summary retry --- Cargo.lock | 1 + README.md | 2 +- crates/agent-core/Cargo.toml | 1 + .../agent-core/src/provider/deepseek_api.rs | 210 +++++++++++++++--- crates/agent-core/src/tool.rs | 2 +- crates/agent-core/src/tool_execution.rs | 108 ++++++++- crates/agent-core/src/turn_loop.rs | 126 +++++++++++ docs/deepseek-api-adapter.md | 6 +- docs/phase-tasks.md | 2 +- docs/roadmap.md | 2 +- docs/tool-system.md | 4 +- docs/turn-loop.md | 4 + docs/vscode-extension.md | 8 +- vscode/extension/src/chatView.ts | 22 +- vscode/extension/test/electron/index.ts | 2 + 15 files changed, 454 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 170db15..81e9515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -644,6 +644,7 @@ name = "prole-coder-agent-core" version = "0.1.0" dependencies = [ "async-stream", + "base64", "futures-util", "ignore", "reqwest", diff --git a/README.md b/README.md index 1247e07..026c9fd 100644 --- a/README.md +++ b/README.md @@ -714,7 +714,7 @@ extension.ts - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar composer 提供 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 +- 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar composer 提供 Key/Model/Settings 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 - `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 子项未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 diff --git a/crates/agent-core/Cargo.toml b/crates/agent-core/Cargo.toml index b807870..801b927 100644 --- a/crates/agent-core/Cargo.toml +++ b/crates/agent-core/Cargo.toml @@ -10,6 +10,7 @@ path = "src/lib.rs" [dependencies] async-stream = "0.3" +base64 = "0.22" futures-util = "0.3" ignore = "0.4" reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls", "stream", "system-proxy"] } diff --git a/crates/agent-core/src/provider/deepseek_api.rs b/crates/agent-core/src/provider/deepseek_api.rs index c86595e..8e14e28 100644 --- a/crates/agent-core/src/provider/deepseek_api.rs +++ b/crates/agent-core/src/provider/deepseek_api.rs @@ -15,6 +15,7 @@ pub const DEFAULT_MODEL: &str = DeepSeekModelId::V4_PRO; const CHAT_COMPLETIONS_PATH: &str = "chat/completions"; const FIM_COMPLETIONS_PATH: &str = "completions"; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(600); +const DEFAULT_HTTP_SEND_ATTEMPTS: usize = 3; const FIM_MAX_TOKENS_LIMIT: u32 = 4096; #[derive(Debug, Error)] @@ -68,10 +69,16 @@ pub enum DeepSeekApiError { impl DeepSeekApiError { fn http(source: reqwest::Error) -> Self { - Self::Http { - details: format_reqwest_error(&source), - source, + Self::http_after_send_attempts(source, 1) + } + + fn http_after_send_attempts(source: reqwest::Error, attempts: usize) -> Self { + let mut details = format_reqwest_error(&source); + if attempts > 1 { + details.push_str(&format!("; retry attempts exhausted: {attempts}")); } + + Self::Http { details, source } } } @@ -122,6 +129,8 @@ pub struct DeepSeekApiConfig { base_url: Url, model: DeepSeekModelId, timeout: Duration, + http_send_attempts: usize, + use_system_proxy: bool, } impl DeepSeekApiConfig { @@ -153,6 +162,8 @@ impl DeepSeekApiConfig { base_url, model: DeepSeekModelId::new(model)?, timeout: DEFAULT_TIMEOUT, + http_send_attempts: DEFAULT_HTTP_SEND_ATTEMPTS, + use_system_proxy: true, }) } @@ -178,6 +189,16 @@ impl DeepSeekApiConfig { self } + pub fn with_http_send_attempts(mut self, attempts: usize) -> Self { + self.http_send_attempts = attempts.max(1); + self + } + + pub fn without_system_proxy(mut self) -> Self { + self.use_system_proxy = false; + self + } + pub fn base_url(&self) -> &Url { &self.base_url } @@ -190,6 +211,14 @@ impl DeepSeekApiConfig { self.timeout } + pub fn http_send_attempts(&self) -> usize { + self.http_send_attempts + } + + pub fn use_system_proxy(&self) -> bool { + self.use_system_proxy + } + fn endpoint(&self, path: &str) -> Result { let mut base_url = self.base_url.clone(); if !base_url.path().ends_with('/') { @@ -215,6 +244,8 @@ impl fmt::Debug for DeepSeekApiConfig { .field("base_url", &self.base_url) .field("model", &self.model) .field("timeout", &self.timeout) + .field("http_send_attempts", &self.http_send_attempts) + .field("use_system_proxy", &self.use_system_proxy) .finish() } } @@ -227,10 +258,11 @@ pub struct DeepSeekApiAdapter { impl DeepSeekApiAdapter { pub fn new(config: DeepSeekApiConfig) -> Result { - let client = reqwest::Client::builder() - .timeout(config.timeout()) - .build() - .map_err(DeepSeekApiError::http)?; + let mut client = reqwest::Client::builder().timeout(config.timeout()); + if !config.use_system_proxy() { + client = client.no_proxy(); + } + let client = client.build().map_err(DeepSeekApiError::http)?; Ok(Self { client, config }) } @@ -256,13 +288,8 @@ impl DeepSeekApiAdapter { } let response = self - .client - .post(self.config.endpoint(CHAT_COMPLETIONS_PATH)?) - .bearer_auth(&self.config.api_key) - .json(&request) - .send() - .await - .map_err(DeepSeekApiError::http)?; + .send_json_with_retries(CHAT_COMPLETIONS_PATH, &request) + .await?; decode_chat_completion_response(response).await } @@ -275,13 +302,8 @@ impl DeepSeekApiAdapter { request.validate_for_deepseek()?; let response = self - .client - .post(self.config.endpoint(CHAT_COMPLETIONS_PATH)?) - .bearer_auth(&self.config.api_key) - .json(&request) - .send() - .await - .map_err(DeepSeekApiError::http)?; + .send_json_with_retries(CHAT_COMPLETIONS_PATH, &request) + .await?; decode_chat_completion_stream(response).await } @@ -292,16 +314,47 @@ impl DeepSeekApiAdapter { ) -> Result { request.validate_for_deepseek()?; let response = self - .client - .post(self.config.endpoint(FIM_COMPLETIONS_PATH)?) - .bearer_auth(&self.config.api_key) - .json(&request) - .send() - .await - .map_err(DeepSeekApiError::http)?; + .send_json_with_retries(FIM_COMPLETIONS_PATH, &request) + .await?; decode_fim_completion_response(response).await } + + async fn send_json_with_retries( + &self, + path: &str, + request: &T, + ) -> Result + where + T: Serialize + ?Sized, + { + let endpoint = self.config.endpoint(path)?; + let attempts = self.config.http_send_attempts(); + + for attempt in 1..=attempts { + let result = self + .client + .post(endpoint.clone()) + .bearer_auth(&self.config.api_key) + .json(request) + .send() + .await; + + match result { + Ok(response) => return Ok(response), + Err(source) => { + let retryable = retryable_send_error(&source); + if retryable && attempt < attempts { + continue; + } + + return Err(DeepSeekApiError::http_after_send_attempts(source, attempt)); + } + } + } + + unreachable!("send attempts loop always returns") + } } async fn decode_chat_completion_response( @@ -400,6 +453,10 @@ fn format_reqwest_error(error: &reqwest::Error) -> String { details.join("; ") } +fn retryable_send_error(error: &reqwest::Error) -> bool { + error.is_timeout() || error.is_connect() || error.is_request() +} + fn redact_url_credentials(message: &str) -> String { let mut result = String::with_capacity(message.len()); let mut rest = message; @@ -1204,12 +1261,23 @@ pub fn parse_stream_event_block(block: &str) -> Result, Deep #[cfg(test)] mod tests { + use std::{ + io::{Read, Write}, + net::{TcpListener, TcpStream}, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + thread, + time::Duration, + }; + use super::{ ChatCompletionRequest, ChatFunctionCallDelta, ChatMessage, ChatTool, ChatToolCall, ChatToolCallAccumulator, ChatToolCallAccumulatorError, ChatToolCallDelta, ChatToolType, - DeepSeekApiConfig, DeepSeekApiError, DeepSeekModelId, FimCompletionRequest, - ReasoningEffort, SseEventParser, StreamEvent, StreamOptions, ThinkingConfig, ToolChoice, - parse_stream_event_block, redact_url_credentials, + DeepSeekApiAdapter, DeepSeekApiConfig, DeepSeekApiError, DeepSeekModelId, + FimCompletionRequest, ReasoningEffort, SseEventParser, StreamEvent, StreamOptions, + ThinkingConfig, ToolChoice, parse_stream_event_block, redact_url_credentials, }; #[test] @@ -1270,6 +1338,33 @@ mod tests { ); } + #[tokio::test] + async fn chat_completion_retries_transient_send_errors() { + let (base_url, attempts, server) = spawn_retry_fixture_server(); + let config = DeepSeekApiConfig::new("test-api-key", base_url, DeepSeekModelId::V4_PRO) + .expect("config should be valid") + .with_http_send_attempts(2) + .without_system_proxy(); + let adapter = DeepSeekApiAdapter::new(config).expect("adapter should be created"); + let request = ChatCompletionRequest::new( + DeepSeekModelId::new(DeepSeekModelId::V4_PRO).expect("model should be valid"), + vec![ChatMessage::user("hello")], + ) + .expect("request should be valid"); + + let response = adapter + .create_chat_completion(request) + .await + .expect("second send attempt should succeed"); + + server.join().expect("fixture server should finish"); + assert_eq!(attempts.load(Ordering::SeqCst), 2); + assert_eq!( + response.choices[0].message.content.as_deref(), + Some("retry-ok") + ); + } + #[test] fn fim_request_serializes_prefix_suffix_and_non_streaming_default() { let request = FimCompletionRequest::new( @@ -1289,6 +1384,59 @@ mod tests { assert_eq!(json["stream"], false); } + fn spawn_retry_fixture_server() -> (String, Arc, thread::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("fixture listener should bind"); + let base_url = format!( + "http://{}", + listener + .local_addr() + .expect("fixture listener should have an address") + ); + let attempts = Arc::new(AtomicUsize::new(0)); + let attempt_counter = Arc::clone(&attempts); + + let handle = thread::spawn(move || { + for attempt in 1..=2 { + let (mut stream, _) = listener.accept().expect("fixture connection should arrive"); + attempt_counter.fetch_add(1, Ordering::SeqCst); + + if attempt == 1 { + drop(stream); + continue; + } + + read_http_request(&mut stream); + let body = r#"{"id":"chatcmpl-test","choices":[{"index":0,"message":{"role":"assistant","content":"retry-ok"},"finish_reason":"stop"}],"created":1,"model":"deepseek-v4-pro","object":"chat.completion","usage":null,"system_fingerprint":null}"#; + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}", + body.len(), + body + ); + stream + .write_all(response.as_bytes()) + .expect("fixture response should write"); + } + }); + + (base_url, attempts, handle) + } + + fn read_http_request(stream: &mut TcpStream) { + let mut buffer = [0_u8; 4096]; + let mut received = Vec::new(); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("fixture read timeout should be set"); + + while !received.windows(4).any(|window| window == b"\r\n\r\n") { + match stream.read(&mut buffer) { + Ok(0) => break, + Ok(count) => received.extend_from_slice(&buffer[..count]), + Err(_) => break, + } + } + } + #[test] fn fim_request_rejects_invalid_max_tokens() { let request = FimCompletionRequest::new( diff --git a/crates/agent-core/src/tool.rs b/crates/agent-core/src/tool.rs index 25fc32c..c9a2528 100644 --- a/crates/agent-core/src/tool.rs +++ b/crates/agent-core/src/tool.rs @@ -294,7 +294,7 @@ pub const BUILTIN_TOOLS: &[ToolDefinition] = &[ ), ToolDefinition::new( ToolName::Shell, - "Execute a non-interactive shell command.", + "Execute a non-interactive shell command from the selected workspace-relative cwd; use cwd instead of embedding cd into command.", RiskLevel::Exec, ApprovalRequirement::Required, ToolImplementationStatus::ExecutorImplemented, diff --git a/crates/agent-core/src/tool_execution.rs b/crates/agent-core/src/tool_execution.rs index cad054e..d7f00e5 100644 --- a/crates/agent-core/src/tool_execution.rs +++ b/crates/agent-core/src/tool_execution.rs @@ -1,5 +1,6 @@ use std::{ collections::BTreeSet, + ffi::OsStr, fs, io, path::{Component, Path, PathBuf}, process::{Child, Command, Stdio}, @@ -843,9 +844,32 @@ fn run_shell_command( ) -> Result { #[cfg(windows)] { + let user_command = powershell_single_quoted_literal(command); + let script = format!( + concat!( + "$utf8 = New-Object System.Text.UTF8Encoding $false; ", + "[Console]::InputEncoding = $utf8; ", + "[Console]::OutputEncoding = $utf8; ", + "$OutputEncoding = $utf8; ", + "$ErrorActionPreference = 'Stop'; ", + "$ProgressPreference = 'SilentlyContinue'; ", + "try {{ Invoke-Expression -Command {user_command}; $proleSuccess = $? }} ", + "catch {{ [Console]::Error.WriteLine($_.ToString()); exit 1 }}; ", + "$proleSuccess = $?; ", + "if ($global:LASTEXITCODE -ne $null) {{ exit $global:LASTEXITCODE }}; ", + "if (-not $proleSuccess) {{ exit 1 }}" + ), + user_command = user_command + ); + let encoded_script = encode_powershell_command(&script); run_command( "powershell", - ["-NoProfile", "-NonInteractive", "-Command", command], + vec![ + "-NoProfile".to_owned(), + "-NonInteractive".to_owned(), + "-EncodedCommand".to_owned(), + encoded_script, + ], cwd, timeout, cancellation_token, @@ -858,13 +882,43 @@ fn run_shell_command( } } -fn run_command<'a>( +#[cfg(windows)] +fn powershell_single_quoted_literal(value: &str) -> String { + let mut escaped = String::with_capacity(value.len() + 2); + escaped.push('\''); + for character in value.chars() { + if character == '\'' { + escaped.push_str("''"); + } else { + escaped.push(character); + } + } + escaped.push('\''); + escaped +} + +#[cfg(windows)] +fn encode_powershell_command(script: &str) -> String { + use base64::{Engine as _, engine::general_purpose::STANDARD}; + + let mut bytes = Vec::with_capacity(script.len() * 2); + for code_unit in script.encode_utf16() { + bytes.extend_from_slice(&code_unit.to_le_bytes()); + } + STANDARD.encode(bytes) +} + +fn run_command( program: &str, - args: impl IntoIterator, + args: I, cwd: &Path, timeout: Duration, cancellation_token: &CancellationToken, -) -> Result { +) -> Result +where + I: IntoIterator, + S: AsRef, +{ check_canceled(cancellation_token, program)?; let start = Instant::now(); let mut command = Command::new(program); @@ -1990,6 +2044,52 @@ mod tests { assert!(result.stdout.contains("hello")); } + #[cfg(windows)] + #[test] + fn shell_decodes_windows_powershell_stderr_as_utf8() { + let workspace = TestWorkspace::new("tool-execution"); + let tools = WorkspaceToolExecutor::new(workspace.path()).expect("workspace should open"); + + let result = tools + .shell(ShellArgs { + command: "[Console]::Error.WriteLine('错误信息'); exit 1".to_owned(), + cwd: None, + timeout_ms: Some(10_000), + }) + .expect("shell should return failed command output"); + + assert_eq!(result.status, ToolStatus::Failed); + assert!( + result.stderr.contains("错误信息"), + "stderr was {:?}", + result.stderr + ); + assert!(!result.stderr.contains('\u{fffd}')); + } + + #[cfg(windows)] + #[test] + fn shell_decodes_windows_powershell_parser_errors_as_utf8() { + let workspace = TestWorkspace::new("tool-execution"); + let tools = WorkspaceToolExecutor::new(workspace.path()).expect("workspace should open"); + + let result = tools + .shell(ShellArgs { + command: "cd /home/user/ledger && npm test".to_owned(), + cwd: Some(".".to_owned()), + timeout_ms: Some(10_000), + }) + .expect("shell should return parser error output"); + + assert_eq!(result.status, ToolStatus::Failed); + assert!( + result.stderr.contains("&&"), + "stderr was {:?}", + result.stderr + ); + assert!(!result.stderr.contains('\u{fffd}')); + } + #[test] fn shell_cancels_running_command() { let workspace = TestWorkspace::new("tool-execution"); diff --git a/crates/agent-core/src/turn_loop.rs b/crates/agent-core/src/turn_loop.rs index 28cdf71..b5a8f82 100644 --- a/crates/agent-core/src/turn_loop.rs +++ b/crates/agent-core/src/turn_loop.rs @@ -46,6 +46,24 @@ const FINAL_RESPONSE_SUMMARY_INSTRUCTION: &str = concat!( "Mention what changed, important files, verification or tests, and any blockers. ", "Do not dump raw tool logs, JSON-RPC events, or intermediate provider/tool chatter; those details are recorded in ProleCoder Output." ); +const FINAL_RESPONSE_LENGTH_RETRY_INSTRUCTION: &str = concat!( + "Your previous response ended because the model reached its output length limit before a complete final user-facing summary. ", + "Do not call tools unless absolutely necessary. Return only a concise final work summary now, covering changes, important files, verification or tests, and blockers." +); +#[cfg(windows)] +const TOOL_USAGE_INSTRUCTION: &str = concat!( + "Tool usage rules: all workspace paths must be workspace-relative unless a tool explicitly says otherwise. ", + "Do not invent absolute paths such as /home/user/project, and do not use cd to move into guessed directories. ", + "For the shell tool, put the target directory in the cwd argument, usually \".\" or a workspace-relative subdirectory, and keep command to the command itself. ", + "Shell commands run under Windows PowerShell 5.1, so do not use POSIX-only paths or PowerShell 7-only operators such as && and ||. Use separate tool calls or Windows PowerShell-compatible syntax." +); +#[cfg(not(windows))] +const TOOL_USAGE_INSTRUCTION: &str = concat!( + "Tool usage rules: all workspace paths must be workspace-relative unless a tool explicitly says otherwise. ", + "Do not invent absolute paths such as /home/user/project, and do not use cd to move into guessed directories. ", + "For the shell tool, put the target directory in the cwd argument, usually \".\" or a workspace-relative subdirectory, and keep command to the command itself. ", + "Shell commands run under POSIX sh from the selected cwd." +); #[derive(Debug)] pub struct AgentTurnLoop { @@ -236,6 +254,17 @@ where return Err(AgentTurnLoopError::MissingAssistantReasoningContent); } + if response.completion.finish_reason == TurnProviderFinishReason::Length { + if response.tool_calls.is_empty() { + push_final_response_retry_messages(&mut messages, &response); + continue; + } + + return Err(AgentTurnLoopError::Provider(TurnProviderError::new( + "provider response reached the output length limit before tool calls completed", + ))); + } + if let Some(content) = response .content .as_deref() @@ -255,6 +284,14 @@ where } if response.tool_calls.is_empty() { + if response.completion.finish_reason != TurnProviderFinishReason::Stop { + return Err(AgentTurnLoopError::Provider(TurnProviderError::new( + format!( + "provider finished with `{}` before a final assistant summary", + turn_provider_finish_reason_label(response.completion.finish_reason) + ), + ))); + } let final_message = response.content.unwrap_or_default(); append_turn_event( run_log, @@ -430,6 +467,10 @@ where "stable workspace manifest summary", )); } + builder.add_item(ContextItem::project_rules( + TOOL_USAGE_INSTRUCTION, + "default tool usage contract", + )); builder.add_item(ContextItem::project_rules( FINAL_RESPONSE_SUMMARY_INSTRUCTION, "default final response summary contract", @@ -1042,6 +1083,30 @@ where ) } +fn push_final_response_retry_messages( + messages: &mut Vec, + response: &TurnProviderResponse, +) { + if let Some(content) = response + .content + .as_deref() + .filter(|content| !content.trim().is_empty()) + { + messages.push(ChatMessage::assistant(content.to_owned())); + } + messages.push(ChatMessage::user(FINAL_RESPONSE_LENGTH_RETRY_INSTRUCTION)); +} + +fn turn_provider_finish_reason_label(reason: TurnProviderFinishReason) -> &'static str { + match reason { + TurnProviderFinishReason::Stop => "stop", + TurnProviderFinishReason::Length => "length", + TurnProviderFinishReason::ToolCalls => "tool_calls", + TurnProviderFinishReason::ContentFilter => "content_filter", + TurnProviderFinishReason::Error => "error", + } +} + fn attachment_path( index: usize, attachment: &TurnAttachment, @@ -2653,6 +2718,67 @@ mod tests { .expect("provider prompt should include context"); assert!(prompt.contains("final assistant message")); assert!(prompt.contains("Do not dump raw tool logs")); + assert!(prompt.contains("Tool usage rules")); + assert!(prompt.contains("cwd argument")); + assert!(prompt.contains("do not use cd")); + #[cfg(windows)] + assert!(prompt.contains("Windows PowerShell 5.1")); + #[cfg(not(windows))] + assert!(prompt.contains("POSIX sh")); + } + + #[tokio::test] + async fn turn_loop_retries_length_finished_final_response_until_summary() { + let workspace = TestWorkspace::new("turn-loop"); + let store = RunLogStore::new(workspace.path()).expect("run log store should open"); + let mut run = store + .create_run("run_turn_length_final_retry") + .expect("run should be created"); + let length_response = TurnProviderResponse { + content: None, + reasoning_content: None, + tool_calls: Vec::new(), + completion: TurnProviderCompletion::fixture(TurnProviderFinishReason::Length), + }; + let provider = ScriptedProvider::new(vec![ + length_response, + TurnProviderResponse::final_text("Final work summary."), + ]); + let mut loop_runner = + AgentTurnLoop::new(workspace.path(), provider).expect("turn loop should initialize"); + + let outcome = loop_runner + .run_turn(AgentTurnInput::new("turn_1", "Finish the task"), &mut run) + .await + .expect("length-truncated final response should be retried"); + + assert_eq!(outcome.final_message, "Final work summary."); + assert_eq!(outcome.iterations, 2); + assert_eq!(loop_runner.provider.requests.len(), 2); + assert_eq!(loop_runner.provider.requests[1].messages.len(), 2); + let retry_prompt = loop_runner.provider.requests[1] + .messages + .last() + .and_then(|message| message.content.as_deref()) + .expect("retry request should include a user instruction"); + assert!(retry_prompt.contains("output length limit")); + assert!(retry_prompt.contains("Return only a concise final work summary")); + + let events = store + .load_run("run_turn_length_final_retry") + .expect("events should load"); + let finish_reasons = events + .iter() + .filter(|event| event.event_type == "provider.completed") + .map(|event| event.payload["finishReason"].clone()) + .collect::>(); + assert_eq!(finish_reasons, vec![json!("length"), json!("stop")]); + let completed = events + .iter() + .filter(|event| event.event_type == "run.completed") + .collect::>(); + assert_eq!(completed.len(), 1); + assert_eq!(completed[0].payload["summary"], "Final work summary."); } #[tokio::test] diff --git a/docs/deepseek-api-adapter.md b/docs/deepseek-api-adapter.md index d8e3b66..8446555 100644 --- a/docs/deepseek-api-adapter.md +++ b/docs/deepseek-api-adapter.md @@ -90,7 +90,7 @@ crates/agent-core/src/provider/deepseek_api.rs 核心类型: -- `DeepSeekApiConfig`:base URL、模型、超时和 API Key。 +- `DeepSeekApiConfig`:base URL、模型、超时、HTTP send retry 次数和 API Key。 - `DeepSeekApiAdapter`:持有 `reqwest::Client`,负责发送 HTTP 请求。 - `ChatCompletionRequest`:DeepSeek chat completion 请求体。 - `ChatCompletionResponse`:非流式响应。 @@ -153,7 +153,9 @@ adapter 使用显式错误枚举: - DeepSeek 返回非 2xx 状态。 - JSON 响应或 SSE data 解析失败。 -HTTP client 启用 reqwest `system-proxy`,让 CLI、RPC server 和 VS Code 插件启动的子进程都尽量复用系统代理配置。发送失败时错误消息会保留 timeout/connect/request/body/decode 等分类和底层 source chain;如果 source 中包含带用户名密码的 URL,会先脱敏。非 2xx 响应保留 HTTP status 和响应 body,便于上层生成用户可读错误。API Key 不进入错误消息。 +HTTP client 默认启用 reqwest `system-proxy`,让 CLI、RPC server 和 VS Code 插件启动的子进程都尽量复用系统代理配置。发送失败时错误消息会保留 timeout/connect/request/body/decode 等分类和底层 source chain;如果 source 中包含带用户名密码的 URL,会先脱敏。非 2xx 响应保留 HTTP status 和响应 body,便于上层生成用户可读错误。API Key 不进入错误消息。 + +DeepSeek adapter 会对 `send().await` 阶段的 transient timeout/connect/request 错误做最多 3 次发送尝试,用于缓解 Windows 本机软件或代理偶发中止已建立连接时的 `os error 10053`。重试只发生在还没有拿到 HTTP response 的发送阶段;HTTP 非 2xx、JSON 解析错误、SSE 已建立后的中途断流不会自动重试,避免重复消费已开始的模型响应或掩盖真实 API 错误。测试 fixture 可通过 `DeepSeekApiConfig::without_system_proxy()` 禁用系统代理,真实 CLI/RPC 默认仍使用 system-proxy。 ## reasoning_content 规则 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 229d5c3..227bd99 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -134,7 +134,7 @@ | [x] | P5-14c:malformed tool-call 本地诊断文件 | `docs/json-rpc-protocol.md`、`docs/run-log.md`、`docs/turn-loop.md` | 已完成:当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在当前 run 的 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments,并在 `run.failed.diagnosticFile` / Sidebar failure card 指向该文件。验收:`cargo test -p prole-coder-agent-core diagnostic`、`pnpm --filter prole-coder-vscode test`。 | | [x] | P5-14d:Sidebar UX extension-host E2E 回归 | `docs/testing.md`、`docs/vscode-extension.md` | 已完成:extension-host 测试新增 test-mode webview probe,覆盖真实 webview 中 resume 后继续发送新 turn、默认事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 默认折叠展示和 Enter / Shift+Enter 输入行为。验收:`pnpm run vscode:test-electron`。 | | [x] | P5-14e:大 patch / 大文件分块式工具参数协议 | `docs/tool-system.md`、`docs/turn-loop.md`、`docs/run-log.md` | 已完成:Run Log 支持 run-scoped `payloads/` 文件写入、chunk append 和读取;`apply_patch` schema 新增 `payloadRef`,Turn Loop 可在 `tool.requested` 保留轻量 preview,并在执行前从当前 run 的 payload 文件 materialize `unifiedDiff`,校验 `sha256` / `sizeBytes` 后复用现有 schema、路径安全、审批和 hunk 边界。chunk 追加阶段不写 workspace。验收:`cargo test -p prole-coder-agent-core payload`、`cargo test -p prole-coder-agent-core apply_patch_schema_accepts_run_scoped_payload_refs`。 | -| [ ] | P5-14f:真实试用 UX backlog 收敛 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model、审批、Chat、Output 日志和上下文压缩等体验问题,并把可执行项登记为 P5-14 子项或后续 Phase 任务。 | +| [ ] | P5-14f:真实试用 UX backlog 收敛 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model/Settings、审批、Chat、Output 日志和上下文压缩等体验问题,并把可执行项登记为 P5-14 子项或后续 Phase 任务。本轮已修复 `finishReason=length` 时误写空/截断 `run.completed.summary` 的问题,Core 会自动追问最终工作总结;也补强了 shell 工具的 `cwd`/平台提示与 Windows PowerShell UTF-8 stderr/stdout 输出,避免真实试用中出现 `/home/... &&` 误用后错误信息乱码;Sidebar composer 补齐 Settings 按钮,直达扩展设置页,便于发现和编辑 `prole-coder.rpc.args` 等 RPC 参数;DeepSeek adapter 对发送阶段 transient timeout/connect/request 错误增加有界重试,缓解 Windows `os error 10053` 直接导致 run 失败的问题。持续试用项仍未完成。 | ## Phase 6:TUI 与生态扩展 diff --git a/docs/roadmap.md b/docs/roadmap.md index a3cebe3..e5652bd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -121,7 +121,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 - P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 -- P5-14:持续 UX 测试与体验改进总项,用于真实插件试用后的 Runs、Key/Model、审批、Chat、Output 日志、上下文压缩和大 patch / 大文件工具参数问题回归;具体 P5-14a 到 P5-14f 子项见 `docs/phase-tasks.md`。 +- P5-14:持续 UX 测试与体验改进总项,用于真实插件试用后的 Runs、Key/Model/Settings、审批、Chat、Output 日志、上下文压缩和大 patch / 大文件工具参数问题回归;具体 P5-14a 到 P5-14f 子项见 `docs/phase-tasks.md`。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 diff --git a/docs/tool-system.md b/docs/tool-system.md index 74ded2e..f3e5f03 100644 --- a/docs/tool-system.md +++ b/docs/tool-system.md @@ -193,6 +193,8 @@ pub struct ToolDefinition { - `cwd`:workspace-relative 工作目录,省略时使用 workspace root。 - `timeoutMs`:超时时间。 +模型不应在 `command` 中 `cd` 到猜测的绝对路径;需要切换目录时使用 `cwd`,且 `cwd` 必须是 workspace-relative。Windows 上执行器使用 Windows PowerShell 5.1,并在启动脚本中把 PowerShell 层 stdout/stderr 设置为 UTF-8;模型不应使用 POSIX-only 路径或 PowerShell 7-only 的 `&&` / `||` 操作符。 + 结果: - `exitCode` @@ -305,7 +307,7 @@ fixture 中的 `tools` 被当作无序集合校验;测试会按工具名规整 - `read_file`:只读取 workspace 内 UTF-8 文本文件,支持 1-based 行范围,并返回完整文件的 `sha256` 和 `sizeBytes`。 - `search`:通过 `rg --json --fixed-strings` 搜索,默认排除 `.git/`、`.secrets/`、`.secret/`、`.env*`、`node_modules/` 和 `target/`。 - `apply_patch`:应用受限 unified diff,要求 patch 实际文件集合与 `expectedFiles` 完全一致;执行时会先在内存中完成全部文件的 hunk 校验和 staging,再统一写盘,因此解析或 hunk mismatch 不会留下部分文件已修改的状态;成功后返回 reverse patch。Core 会从 unified diff 生成稳定 hunk id,RPC/VS Code 首版支持 selected hunk 审批;文件创建和删除如果只批准部分 hunk 会被拒绝,避免生成不可审计的半文件操作。大 patch 可以先分块追加到当前 run 的 `payloads/` 文件,再通过 `payloadRef` 引用;Turn Loop 会校验 `sha256` / `sizeBytes`,然后 materialize 为 `unifiedDiff` 进入同一条 schema、路径安全、审批和 hunk 边界链路。 -- `shell`:在 workspace 内执行非交互式命令,支持超时,执行前进行命令风险分类,返回 exit code、stdout、stderr 和耗时。 +- `shell`:在 workspace 内执行非交互式命令,支持 workspace-relative `cwd`、超时和命令风险分类,返回 exit code、stdout、stderr 和耗时;Windows PowerShell wrapper 会先设置 UTF-8 输出,避免 PowerShell 本地化错误信息在 run log / Output Channel 中乱码。 - `git_status`:读取 `git status --short --branch` 或普通 `git status`。 - `git_diff`:读取 unstaged 或 staged diff,支持限定 workspace-relative 路径。 diff --git a/docs/turn-loop.md b/docs/turn-loop.md index e563297..b1604c5 100644 --- a/docs/turn-loop.md +++ b/docs/turn-loop.md @@ -48,6 +48,7 @@ AgentTurnInput ``` 工具结果写入 run log 或进入下一轮 prompt 前会通过 `redacted_tool_result_value` 转成已脱敏 JSON。原始工具结果仍由工具执行层返回,便于即时诊断和后续 UI 展示,但 Turn Loop 的持久化与模型回传路径使用脱敏结果。 +Turn Loop 默认会向 provider 注入工具使用契约,要求所有工具路径保持 workspace-relative,并要求 `shell` 使用 `cwd` 参数切换工作目录,避免模型生成猜测的绝对路径或当前平台不支持的 shell 语法。Windows 构建中该契约会明确 shell 运行在 Windows PowerShell 5.1 下,不应使用 POSIX-only 路径或 PowerShell 7-only 的 `&&` / `||`。 ## Run Log 事件 @@ -72,6 +73,8 @@ provider stream 中的 content delta 会立即写入 `assistant.delta`,payload `Completed.content` 是最终 assistant 消息文本的权威来源,用于 `run.completed.summary` 或 assistant tool-call replay。`assistant.delta` 是展示和 run log 增量事件,不反向推断最终文本;当 provider 已经发送过可见 content delta 时,Turn Loop 不会在 `Completed` 时重复写一份完整 `assistant.delta`。因此 reasoning delta 不进入用户可见 summary,tool call 前的可见文本如果存在,应由 provider 同时保留在最终 `Completed.content` 中。 +如果 `provider.completed.finishReason` 为 `length` 且没有 tool calls,Turn Loop 不会把空文本或截断文本当作 `run.completed.summary`;它会把截断 assistant 回合保留在内部消息历史中,并追加一次只要求最终工作总结的用户追问。只有后续 provider 以 `stop` 返回无工具调用的 final content 时才写入 `run.completed`。其他非 `stop` 的无工具最终响应会按 provider error 失败,避免 UI 展示“完成但没有总结”的假终态。 + DeepSeek streaming tool call delta 在 CLI provider wrapper 内通过 `ChatToolCallAccumulator` 拼装为完整 `ChatToolCall` 后才进入 `Completed.tool_calls`。Turn Loop 不直接处理 provider 私有 delta 形态,只要求 provider 在 `Completed` 中提供完整、可校验、可执行的工具调用列表。如果累计后的 `function.arguments` 不是合法 JSON,Turn Loop 会以 `E_INVALID_TOOL_ARGUMENTS` 失败,并把脱敏后的累计 arguments 写入当前 run 的 `diagnostics/invalid-tool-arguments--.json`,同时在 `run.failed.diagnosticFile` 暴露该本地路径。 `apply_patch` 支持大 payload 引用:`tool.requested.argumentsPreview` 可以只包含 `payloadRef`,Turn Loop 在执行前从当前 run 的 `payloads/` 文件读取完整 diff,校验 `sha256` / `sizeBytes` 后 materialize 为 `unifiedDiff`。materialize 之后仍走同一套 schema 校验、路径安全、审批、hunk metadata、selected hunk 过滤和 patch staging;chunk 追加阶段不会写 workspace。 @@ -103,6 +106,7 @@ RPC active run 的 Run Log 使用 `SerializedRunLog`:后台 Turn Loop worker - provider 请求 `apply_patch`,测试审批策略批准后修改文件、记录 `changedFiles`,并完成 run。 - thinking disabled 配置下,provider 返回无 `reasoning_content` 的工具调用时,Turn Loop 会按非 reasoning replay 路径继续执行工具并完成 run。 - provider 发送多个 streaming content delta,Turn Loop 写入多条 `assistant.delta`,并避免在 `Completed` 时重复写入完整文本。 +- provider 最终响应以 `finishReason=length` 且没有工具调用结束时,Turn Loop 会自动追问一次最终工作总结,并且只有后续 `stop` 响应才写入 `run.completed.summary`。 - provider stream 或 shell 工具收到 `CancellationToken` 后,Turn Loop 写入 `run.canceled`,并返回 `E_RUN_CANCELED`。 - Turn Loop 每次成功追加 Run Log 事件后,会把同一条事件交给 `TurnEventSink`,sink 看到的事件序列与本地 `events.jsonl` 一致。 - `SerializedRunLog` 并发 append 测试验证多个 clone 同时写同一 run 时仍生成连续 `seq`。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index 2efa015..a1472b5 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -70,7 +70,7 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server { "prole-coder.rpc.autoStart": true, "prole-coder.rpc.command": "prole", - "prole-coder.rpc.args": ["rpc"], + "prole-coder.rpc.args": ["rpc", "--max-output-tokens", "65536"], "prole-coder.provider.model": "deepseek-v4-pro" } ``` @@ -80,10 +80,12 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server ```json { "prole-coder.rpc.command": "cargo", - "prole-coder.rpc.args": ["run", "-p", "prole-coder-cli", "--", "rpc"] + "prole-coder.rpc.args": ["run", "-p", "prole-coder-cli", "--", "rpc", "--max-output-tokens", "65536"] } ``` +这些设置可通过 Sidebar composer 的 Settings 按钮或命令面板 `ProleCoder: Open Settings` 打开;需要手动编辑 JSON 时使用 VS Code `Preferences: Open User Settings (JSON)`,也可以在测试 workspace 的 `.vscode/settings.json` 中写入 workspace 级配置。`prole-coder.rpc.args` 改动后需要重启 RPC server 或 reload Extension Development Host,已启动的子进程不会自动继承新参数。 + 配置不保存 API Key。DeepSeek API Key 由插件命令写入 VS Code SecretStorage 的多 key 管理器,或由 CLI/RPC server 继续按既有规则从环境变量读取;Key 管理器展示 alias 与 masked key,支持添加 key+alias、选择 active key、改 alias 和删除指定 key。DeepSeek model ID 是非敏感配置,可通过 `prole-coder.provider.model` 或 Sidebar 的 Model 按钮选择。 ## MVP 分层 @@ -116,7 +118,7 @@ Phase 3 P0 验收标准: - `tool.approvalRequired` 触发 Sidebar 内联审批卡片,approve/reject 能回传到 `agent.approve` / `agent.reject`。已完成真实 RPC pending queue 接入;`apply_patch` 支持 selected hunk 并通过 `agent.approve.hunks` 回传。 - Sidebar Chat 能通过 `agent.listRuns` 展示最近 run,并用 `agent.resume` 回放历史事件。已完成首版 Run List / resume 接入。 - Sidebar Chat 能把 `context.built` 渲染为 Context Capsule 面板,展示 token 分段、来源和 manifest/cache/estimator metadata。已完成首版 Context Capsule 可视化。 -- `ProleCoder: Open Settings` 能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key,DeepSeek model ID 作为非敏感配置保存。 +- `ProleCoder: Open Settings` 和 Sidebar Settings 按钮能打开 VS Code 设置,并显示 server capability、模型预算、审批策略、RPC command/state;扩展配置不保存 API Key,DeepSeek model ID 作为非敏感配置保存。 - Inline completion 首版通过 `agent.previewFim` 请求 RPC server 的 FIM preview,只有 server capability 明确标记 `supportsFim` 的模型会被使用。 Phase 4 深度集成清单与 `docs/phase-tasks.md` 对齐;实现细节和验收命令以任务索引为准: diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 86ad429..83d435a 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -27,7 +27,7 @@ import { } from "./chatApprovals"; import { CHAT_RUN_MODES, DEFAULT_CHAT_MODE, parseChatTurnSubmission, sendTurnParams } from "./chatInput"; import { ChatEventTimeline, type ChatTimelineSnapshot } from "./chatEvents"; -import type { ApprovalPromptDecision, ApprovalPromptRequest } from "./commands"; +import { OPEN_SETTINGS_COMMAND, type ApprovalPromptDecision, type ApprovalPromptRequest } from "./commands"; import { contextVizFromEvent, emptyContextViz, @@ -370,6 +370,11 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } + if (isOpenSettingsMessage(message)) { + await this.openSettings(); + return; + } + if (!isRecord(message) || message["type"] !== "submitTurn") { return; } @@ -787,6 +792,10 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private async selectDeepSeekModel(): Promise { await vscode.commands.executeCommand(SELECT_DEEPSEEK_MODEL_COMMAND); } + + private async openSettings(): Promise { + await vscode.commands.executeCommand(OPEN_SETTINGS_COMMAND); + } } function renderChatViewHtml( @@ -1691,6 +1700,7 @@ function renderChatViewHtml( +
@@ -1725,6 +1735,7 @@ function renderChatViewHtml( const modeInput = document.getElementById("mode"); const apiKeyButton = document.getElementById("api-key"); const modelButton = document.getElementById("model"); + const settingsButton = document.getElementById("settings"); const sendButton = document.getElementById("send"); const cancelButton = document.getElementById("cancel"); const submissionRoot = document.getElementById("submission"); @@ -1785,6 +1796,10 @@ function renderChatViewHtml( vscodeApi.postMessage({ type: "selectDeepSeekModel" }); }); + settingsButton.addEventListener("click", () => { + vscodeApi.postMessage({ type: "openSettings" }); + }); + cancelButton.addEventListener("click", () => { const runId = cancelButton.dataset.runId; if (typeof runId === "string" && runId.length > 0) { @@ -2767,6 +2782,7 @@ function renderChatViewHtml( promptValue: promptInput.value, sendDisabled: sendButton.disabled, cancelDisabled: cancelButton.disabled, + providerActions: textContents(".provider-action"), }; } @@ -2906,6 +2922,10 @@ function isSelectDeepSeekModelMessage(message: unknown): boolean { return isRecord(message) && message["type"] === "selectDeepSeekModel"; } +function isOpenSettingsMessage(message: unknown): boolean { + return isRecord(message) && message["type"] === "openSettings"; +} + function isShowRunsMessage(message: unknown): boolean { return isRecord(message) && message["type"] === "showRuns"; } diff --git a/vscode/extension/test/electron/index.ts b/vscode/extension/test/electron/index.ts index 63a7f75..8e1aa44 100644 --- a/vscode/extension/test/electron/index.ts +++ b/vscode/extension/test/electron/index.ts @@ -85,6 +85,7 @@ async function exerciseChatSendTurnDiagnosticsAndApproval(): Promise { assert.equal(approvalProbe.approvalVisible, true); assert.equal(approvalProbe.workLogVisible, true); assert.equal(approvalProbe.workLogOpen, false); + assert.deepEqual(approvalProbe.providerActions, ["API Key", "Model", "Settings"]); assert.ok(approvalProbe.visibleItemTitles.includes("You")); assert.ok(!approvalProbe.visibleItemTitles.includes("Approval required: shell")); @@ -497,4 +498,5 @@ interface WebviewProbeSnapshot { readonly promptValue: string; readonly sendDisabled: boolean; readonly cancelDisabled: boolean; + readonly providerActions: readonly string[]; } From ec33d700a2646ae800e9c92bf9e84f4a8cd91512 Mon Sep 17 00:00:00 2001 From: LonelyQuantum Date: Fri, 5 Jun 2026 22:46:22 +0800 Subject: [PATCH 73/84] Fix sidebar Markdown replay rendering - Render horizontal rules in safe DOM Markdown - Clarify table escape handling for backslashes - Preserve non-string timeline body values defensively - Extend webview/E2E probes for historical Markdown replay - Document follow-up renderer unit test work --- README.md | 99 +- docs/phase-tasks.md | 191 ++-- docs/roadmap.md | 25 +- docs/testing.md | 12 +- docs/vscode-extension.md | 8 +- vscode/extension/package.json | 12 +- vscode/extension/src/chatView.ts | 967 +++++++++++++++++- vscode/extension/test/electron/index.ts | 27 +- .../test/fixtures/rpcFixtureServer.mjs | 63 +- vscode/extension/test/webviewHtml.test.ts | 76 ++ 10 files changed, 1234 insertions(+), 246 deletions(-) create mode 100644 vscode/extension/test/webviewHtml.test.ts diff --git a/README.md b/README.md index 026c9fd..b43a884 100644 --- a/README.md +++ b/README.md @@ -568,45 +568,27 @@ extension.ts ## 开发计划 -当前进度:Phase 1 到 Phase 4 已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 5 已完成原生 Chat、简化审批、自动上下文压缩、Output Channel、API key/model 配置、Git 工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX 和结构化 provider 配置错误恢复;P5-14 作为持续 UX 测试与体验改进总项,详细子项和完成口径见 `docs/phase-tasks.md`。完成 P5-14 全部子项后再进入 Phase 6:TUI 与生态扩展。 +当前进度:Phase 1 到 Phase 4 已完成;Phase 5:VS Code Codex-like UX 与开发工作流仍在进行中。Phase 5 已完成原生 Chat、简化审批、自动上下文压缩、Output Channel、API key/model 配置、Git 工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX、结构化 provider 配置错误恢复和真实试用回归修复;P5-15 真实试用 UX backlog 持续收敛仍未完成。完成 P5-15 后再进入 Phase 6:TUI 与生态扩展。 阶段完成口径:README 中某个 Phase 只有在 `docs/phase-tasks.md` 对应 Phase 下的所有任务都标记为 `[x]` 后,才能在高层开发计划中表述为“全部完成”。如果某阶段核心功能已完成但仍有 P1/P2 增强或发布/文档验收项未完成,README 必须继续把该阶段表述为进行中,并列出剩余任务。 ### Phase 0:项目章程 -- [x] 确定 `ProleCoder` 名称和 AGPL-3.0-or-later 许可证。 -- [x] 编写 README 技术方案、架构、开发计划和注意事项。 -- [x] 建立 Rust workspace:`agent-core`、`agent-rpc`、`cli`、`tui`。 -- [x] 建立 TypeScript/pnpm workspace:`packages/protocol` 和 `vscode/extension`。 -- [x] 建立基础环境配置:`rust-toolchain.toml`、`rustfmt.toml`、`tsconfig.base.json`、`.editorconfig`、`.gitattributes`、`.env.example`。 -- [x] 更新 `.gitignore`,排除本地状态、依赖目录、构建产物和密钥文件。 -- [x] 生成并保留 `Cargo.lock` 与 `pnpm-lock.yaml`。 -- [x] 在 Windows 本机跑通 `pnpm run check`。 -- [x] 建立 CI 骨架。 -- [x] 建立 `CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md`。 -- [x] 建立 `docs/` 设计文档目录和 `docs/adr/` 架构决策记录。 -- [x] 设计正式 JSON-RPC 事件协议。 -- [x] 定义工具 schema、风险等级和审批模型。 +- [x] P0-1:项目身份、许可证与治理。 +- [x] P0-2:Rust / TypeScript workspace 与基础工程配置。 +- [x] P0-3:基础协议、工具 schema、风险等级与审批模型设计。 ### Phase 1:Agent Core MVP -- [x] DeepSeek provider 与 streaming 基础。 -- [x] Reasoning 与 Context Builder。 -- [x] Workspace 工具执行层。 -- [x] Run Log 与 summary。 -- [x] Agent Turn Loop。 -- [x] CLI `run` / `rpc` 最小闭环。 -- [x] Agent RPC Server。 -- [x] 审批前端基础。 -- [x] Phase 1 合并前第一轮测试增强。 -- [x] 合并前测试基础设施收敛。 -- [x] 合并前 live 测试配置收敛。 -- [x] 合并前 RPC/CLI/protocol 验收补齐。 -- [x] 合并前最终验收。 +- [x] P1-1:DeepSeek provider、streaming 与 reasoning 基础。 +- [x] P1-2:基础 Context Builder 与 workspace 工具执行层。 +- [x] P1-3:Run Log、Agent Turn Loop 与 CLI/RPC 最小闭环。 +- [x] P1-4:Agent RPC Server 与审批前端基础。 +- [x] P1-5:合并主线前测试、live 配置、RPC/CLI/protocol 和最终验收收敛。 说明:VS Code RPC server 启动监管与 JSON-RPC request client 已提前完成,归入 Phase 3 前置项;Agent Core MVP 验收不依赖完整 VS Code UI。 -细任务维护规则:README 开发计划只保留阶段级和任务名级摘要;每个任务的实现细节、验收命令、审查来源和子任务拆分统一维护在 `docs/phase-tasks.md`。高层阶段条目标记完成前,必须确认该阶段在 `docs/phase-tasks.md` 的所有父项和子项都已完成。 +细任务维护规则:README 开发计划只保留阶段级和大任务摘要,使用 `P阶段-数字` 编号;每个任务的实现细节、验收命令、审查来源和 `P阶段-数字字母` 子任务拆分统一维护在 `docs/phase-tasks.md`。高层阶段条目标记完成前,必须确认该阶段在 `docs/phase-tasks.md` 的所有子项都已完成。 验收标准: @@ -617,14 +599,11 @@ extension.ts ### Phase 2:1M Context Capsule -- [x] Phase 2a-1:`read_file` 文件摘要元数据。 -- [x] Phase 2a-2:Context Capsule 数据模型与稳定 renderer。 -- [x] Phase 2a-3:Workspace Manifest v0。 -- [x] Phase 2a-4:Context Builder manifest 接入。 -- [x] Phase 2b:TokenEstimator 与稳定前缀。 -- [x] Phase 2c:Attachments、provider summary 和 cache 实验。 -- [x] Phase 2d:大仓库验收、超预算解释、Run Log 体积控制和 JSON Schema 校验层。 -- [x] Phase 2e:合并主线前展示型 demo 扩展。 +- [x] P2-1:Context Capsule 数据模型与 Workspace Manifest。 +- [x] P2-2:TokenEstimator 与稳定前缀。 +- [x] P2-3:Attachments、provider summary 和 cache 实验。 +- [x] P2-4:大仓库验收、超预算解释、Run Log 体积控制和 JSON Schema 校验层。 +- [x] P2-5:合并主线前展示型 demo 扩展。 验收标准: @@ -638,21 +617,11 @@ extension.ts ### Phase 3:VS Code 插件核心与共享 RPC 交互管线 -- [x] RPC 全双工 reader/writer 与事件发送队列。 -- [x] 长 provider request 期间的 client 断连取消。 -- [x] TypeScript extension scaffold。 -- [x] RPC server 管理。 -- [x] JSON-RPC request client。 -- [x] VS Code/protocol TypeScript 类型共享收敛。 -- [x] VS Code RPC/commands 边界测试补齐。 -- [x] Sidebar Chat 与 `agent.event` 渲染。 -- [x] 文本输入发送 turn,并通过 `agent.sendTurn` 驱动真实 Agent 回合。 -- [x] VS Code 审批 UI 接入真实 RPC pending queue。 -- [x] 命令风险分类器和动态风险升级。 -- [x] 更强进程树清理策略。 -- [x] Native diff editor 展示 patch,并为 hunk 级审批预留交互边界。 -- [x] Run List / resume。 -- [x] Context Capsule 可视化。 +- [x] P3-1:RPC 全双工事件管线与断连取消。 +- [x] P3-2:VS Code extension scaffold、RPC server 管理和 JSON-RPC request client。 +- [x] P3-3:VS Code/protocol 类型共享与 RPC/commands 边界测试。 +- [x] P3-4:Sidebar Chat、真实 sendTurn 和 RPC pending queue 审批。 +- [x] P3-5:命令风险、进程树清理、Native diff、Run List/resume 和 Context Capsule 可视化。 验收标准: @@ -707,33 +676,31 @@ extension.ts - [x] P5-11:Phase 5 UX 工作流验收与文档收敛。 - [x] P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX。 - [x] P5-13:结构化 provider 配置错误码与恢复动作。 -- [ ] P5-14:持续 UX 测试与体验改进:持续收敛真实 VS Code 插件试用中的对话、审批、Runs、Key/Model、Output 日志、上下文压缩和大工具参数稳定性问题;具体子项与完成口径见 `docs/phase-tasks.md`。 +- [x] P5-14:真实试用回归修复包:收敛 Sidebar 对话视图、内联确认、Work log 折叠、Markdown 渲染、webview 渲染诊断、provider/shell 稳定性和 composer / Settings 入口回归;具体子项与完成口径见 `docs/phase-tasks.md`。 +- [ ] P5-15:真实试用 UX backlog 持续收敛:继续收集并拆分真实 VS Code 插件试用中的对话、审批、Runs、Key/Model、Output 日志、上下文压缩、Markdown 兼容性和大工具参数稳定性问题。 验收标准: - `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 并填入 `@prole`,用户无需手动拖动 Activity Bar view 到右侧。 - 原生 Chat 和 Sidebar Chat 都通过真实 `agent.sendTurn` 驱动回合,并继续复用 Problems diagnostics、审批回传、Cancel、Run Log 和 Context Capsule。 - 连续对话会自动生成可审计、受限长度的上下文压缩 attachment;不会在 UI 文案里要求用户手动重开对话来延续上下文。 -- 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar composer 提供 Key/Model/Settings 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 -- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-14 子项未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 +- 主审批保持简单并默认在 Sidebar 内联卡片中完成;Sidebar 提供右上角 Settings 齿轮以及 Key/Model 直接入口,过程事件默认折叠,完整事件和错误诊断可在 `Output > ProleCoder` 查看。 +- `docs/phase-tasks.md` 的 Phase 5 仍有 P5-15 未完成,README 不能把 Phase 5 表述为整阶段完成;G4 自动 commit / push / create PR 仍是后续增强。 ### Phase 6:TUI 与生态扩展 -- [ ] TUI RPC 入口和事件流消费。 -- [ ] Chat/Plan/Diff/Tools/Context/Settings 页面。 -- [ ] TUI hunk 级审批、run resume 和配置文件。 -- [ ] TUI release binary。 -- [ ] 多 active run 与事件订阅模型。 -- [ ] 更细的 replay 语义。 -- [ ] MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具和审计包导出。 +- [ ] P6-1:TUI RPC 入口、事件流消费和核心页面。 +- [ ] P6-2:TUI hunk 级审批、run resume、配置文件和 release binary。 +- [ ] P6-3:多 active run、replay 语义和事件订阅模型。 +- [ ] P6-4:生态扩展:MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具和审计包导出。 ### Phase 7:自由软件发布 -- [x] 确定许可证:AGPL-3.0-or-later 已作为项目许可证策略写入 README,后续发布阶段补齐正式 `LICENSE` 文件和源码提供说明。 -- [ ] 发布 `LICENSE`、源码获取说明和网络服务源码提供说明。 -- [ ] 发布源码包、Cargo crate、npm wrapper、VSIX、GitHub Release 校验和。 -- [ ] 建立公开 roadmap 和 issue 模板。 -- [ ] 增加 reproducible build 说明。 +- [x] P7-1:许可证策略确定。 +- [ ] P7-2:发布法律/源码提供文件。 +- [ ] P7-3:发布产物、校验和与源码包。 +- [ ] P7-4:公开 roadmap、issue 模板和贡献流程增强。 +- [ ] P7-5:reproducible build 说明。 ## 安全与注意事项 diff --git a/docs/phase-tasks.md b/docs/phase-tasks.md index 227bd99..a5fb034 100644 --- a/docs/phase-tasks.md +++ b/docs/phase-tasks.md @@ -1,6 +1,6 @@ # 详细任务索引 -状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-13 已完成;P5-14 拆分为子项管理,其中 P5-14a 到 P5-14e 已完成,P5-14f 真实试用 UX backlog 仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 +状态:Phase 1、Phase 2、Phase 3、Phase 4 已完成。Phase 5 进行中,P5-1 到 P5-14 已完成;P5-15 真实试用 UX backlog 仍未完成;Phase 5 全部任务完成后再进入 Phase 6:TUI 与生态扩展。 本文档是详细设计文档里的任务账本。README 保留高层开发计划;这里把各模块文档中出现的“已实现、尚未实现、后续增强、下一步”收敛为可勾选任务,避免后续工作只散落在说明文字里。 @@ -9,8 +9,8 @@ - 新增任何预期实现项时,必须在本文件登记阶段和状态。 - README 开发计划中的阶段条目标记完成前,应检查本文件中对应细任务是否已经完成。 - 如果一个 README 条目完成了它蕴含的细任务,应同步把本文件对应行标记为 `[x]`,并在说明中写清验收方式。 -- README 开发计划只写阶段和任务摘要;实现细节、验收命令、审查来源、后续拆分统一写在本文件。 -- 复杂任务使用父项加子项编号维护,例如 `P5-14` 作为总项,`P5-14a` / `P5-14b` 记录可独立开发和验收的子任务;父项只有在所有子项完成后才能标记完成。 +- README 开发计划只写阶段和大任务摘要,并使用 `P阶段-数字` 编号;实现细节、验收命令、审查来源、后续拆分统一写在本文件。 +- 本文件记录 `P阶段-数字字母` 子任务,例如 `P5-14a` / `P5-14b`;README 中的父任务不需要在本文件重复登记为父项。即使某个 README 大任务目前只有一个实现项,也登记为 `P4-1a` 这类子项,后续扩展时继续追加字母。 - 详细模块文档仍保留设计说明;本文件只记录阶段、状态和追踪入口。 ## 审计结论 @@ -26,133 +26,138 @@ | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | 项目名称、AGPL-3.0-or-later、Rust/TypeScript workspace、pnpm workspace、Windows 环境说明、CI 骨架和治理文件 | `README.md`、`docs/adr/`、`CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md` | Phase 0 已完成并进入 README 高层计划。 | -| [x] | 建立 `docs/` 与 ADR,明确 README 只做入口和高层计划 | `docs/README.md`、`docs/adr/0005-keep-readme-as-entrypoint-and-move-design-to-docs.md` | 详细设计文档已按模块拆分。 | -| [x] | JSON-RPC 基础协议、工具 schema、风险等级和审批模型设计 | `docs/json-rpc-protocol.md`、`docs/tool-system.md`、`docs/approval-model.md` | Phase 0 设计完成;Phase 1 已实现基础执行闭环。 | +| [x] | P0-1a:项目名称、AGPL-3.0-or-later、治理文件与 README 入口定位 | `README.md`、`docs/adr/`、`CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md` | Phase 0 已完成并进入 README 高层计划。 | +| [x] | P0-2a:Rust/TypeScript workspace、pnpm workspace、Windows 环境说明、CI 骨架和基础环境配置 | `README.md`、`docs/adr/`、`CONTRIBUTING.md`、`CODE_OF_CONDUCT.md`、`SECURITY.md` | Phase 0 已完成并进入 README 高层计划。 | +| [x] | P0-3a:JSON-RPC 基础协议、工具 schema、风险等级和审批模型设计 | `docs/json-rpc-protocol.md`、`docs/tool-system.md`、`docs/approval-model.md` | Phase 0 设计完成;Phase 1 已实现基础执行闭环。 | ## Phase 1:Agent Core MVP | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | DeepSeek API adapter、data-only SSE parser、streaming 基础和真实联网 smoke | `docs/deepseek-api-adapter.md`、`docs/agent-core.md` | 已有离线解析测试和 ignored live tests。 | -| [x] | streaming tool-call delta accumulator | `docs/deepseek-api-adapter.md`、`docs/roadmap.md` | 已覆盖 delta 拼装、冲突和缺失元数据测试,并有 live forced tool-call 验收。 | -| [x] | `reasoning_content` replay 状态机 | `docs/reasoning-content.md` | 已覆盖 replay required、缺失 reasoning、thinking disabled 等边界。 | -| [x] | 基础 Context Builder 与 token 预算报告 | `docs/context-capsule.md`、`docs/agent-core.md` | Phase 1 仅实现基础 builder;完整 1M Capsule 归入 Phase 2。 | -| [x] | read/search/apply_patch/shell/git 工具执行层 | `docs/tool-system.md` | 已覆盖路径约束、敏感路径拒绝、命令超时、结构化结果和工具取消。 | -| [x] | Run Log `events.jsonl`、`summary.json`、脱敏和写入串行化 | `docs/run-log.md` | 已接入 CLI 和 RPC;全双工 notification writer 不属于 Phase 1。 | -| [x] | Agent Turn Loop 基础编排、工具审批、验证命令和 run log 写入 | `docs/turn-loop.md`、`docs/agent-core.md` | 已有 fixture 端到端和 CLI smoke。 | -| [x] | `TurnProvider` async / streaming 边界和 `TurnEventSink` 实时事件出口 | `docs/turn-loop.md` | CLI `--json` 和 `StdioEventBridge` 已接入。 | -| [x] | CLI `run` / `rpc` 最小闭环和 JSON-RPC 错误输出 | `docs/cli.md` | 已有库级、进程级和 fixture smoke 测试。 | -| [x] | Agent RPC Server request loop、真实 Turn Loop handler、pending approval、取消、超时、EOF shutdown 和 `agent.listRuns` | `docs/rpc-server.md`、`docs/json-rpc-protocol.md` | 已覆盖审批批准/拒绝/取消/超时、并发拒绝、EOF shutdown、resume/listRuns。 | -| [x] | CLI/TUI/VS Code 审批前端基础原语 | `docs/approval-model.md`、`docs/tui.md`、`docs/vscode-extension.md` | CLI prompt、TUI prompt 状态机、VS Code legacy modal adapter 已实现;后续默认 UX 已改为 Sidebar 内联审批。 | -| [x] | Rust/TypeScript 工具注册表和错误码协议交叉校验 | `docs/tool-system.md`、`docs/json-rpc-protocol.md`、`packages/protocol` | 工具 registry fixture 与错误码表已进入默认测试。 | -| [x] | 合并前测试基础设施、live 配置收敛和离线最终验收 | `docs/testing.md`、`docs/demos.md` | `pnpm run check`、测试清单、离线 demo、diff/sensitive scan 已完成。 | -| [x] | VS Code RPC server 管理和 JSON-RPC request client 前置实现 | `docs/vscode-extension.md`、`docs/roadmap.md` | 属于 Phase 3 前置项,已提前完成;不作为 Phase 1 阻塞验收条件。 | +| [x] | P1-1a:DeepSeek API adapter、data-only SSE parser、streaming 基础和真实联网 smoke | `docs/deepseek-api-adapter.md`、`docs/agent-core.md` | 已有离线解析测试和 ignored live tests。 | +| [x] | P1-1b:streaming tool-call delta accumulator | `docs/deepseek-api-adapter.md`、`docs/roadmap.md` | 已覆盖 delta 拼装、冲突和缺失元数据测试,并有 live forced tool-call 验收。 | +| [x] | P1-1c:`reasoning_content` replay 状态机 | `docs/reasoning-content.md` | 已覆盖 replay required、缺失 reasoning、thinking disabled 等边界。 | +| [x] | P1-2a:基础 Context Builder 与 token 预算报告 | `docs/context-capsule.md`、`docs/agent-core.md` | Phase 1 仅实现基础 builder;完整 1M Capsule 归入 Phase 2。 | +| [x] | P1-2b:read/search/apply_patch/shell/git 工具执行层 | `docs/tool-system.md` | 已覆盖路径约束、敏感路径拒绝、命令超时、结构化结果和工具取消。 | +| [x] | P1-3a:Run Log `events.jsonl`、`summary.json`、脱敏和写入串行化 | `docs/run-log.md` | 已接入 CLI 和 RPC;全双工 notification writer 不属于 Phase 1。 | +| [x] | P1-3b:Agent Turn Loop 基础编排、工具审批、验证命令和 run log 写入 | `docs/turn-loop.md`、`docs/agent-core.md` | 已有 fixture 端到端和 CLI smoke。 | +| [x] | P1-3c:`TurnProvider` async / streaming 边界和 `TurnEventSink` 实时事件出口 | `docs/turn-loop.md` | CLI `--json` 和 `StdioEventBridge` 已接入。 | +| [x] | P1-3d:CLI `run` / `rpc` 最小闭环和 JSON-RPC 错误输出 | `docs/cli.md` | 已有库级、进程级和 fixture smoke 测试。 | +| [x] | P1-4a:Agent RPC Server request loop、真实 Turn Loop handler、pending approval、取消、超时、EOF shutdown 和 `agent.listRuns` | `docs/rpc-server.md`、`docs/json-rpc-protocol.md` | 已覆盖审批批准/拒绝/取消/超时、并发拒绝、EOF shutdown、resume/listRuns。 | +| [x] | P1-4b:CLI/TUI/VS Code 审批前端基础原语 | `docs/approval-model.md`、`docs/tui.md`、`docs/vscode-extension.md` | CLI prompt、TUI prompt 状态机、VS Code legacy modal adapter 已实现;后续默认 UX 已改为 Sidebar 内联审批。 | +| [x] | P1-5a:Rust/TypeScript 工具注册表和错误码协议交叉校验 | `docs/tool-system.md`、`docs/json-rpc-protocol.md`、`packages/protocol` | 工具 registry fixture 与错误码表已进入默认测试。 | +| [x] | P1-5b:合并前测试基础设施、live 配置收敛和离线最终验收 | `docs/testing.md`、`docs/demos.md` | `pnpm run check`、测试清单、离线 demo、diff/sensitive scan 已完成。 | +| [x] | P3-2a(前置实现):VS Code RPC server 管理和 JSON-RPC request client | `docs/vscode-extension.md`、`docs/roadmap.md` | 属于 Phase 3 前置项,已提前完成;不作为 Phase 1 阻塞验收条件。 | ## Phase 2:1M Context Capsule | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | Phase 2a-1:`read_file` 增加 `sha256` / `sizeBytes` | `docs/tool-system.md`、`docs/context-capsule.md` | 已完成:`read_file` 返回完整文件的 `sha256` 和 `sizeBytes`,Rust/TypeScript result schema 与单元测试已同步。 | -| [x] | Phase 2a-2:定义 `ContextCapsule` / `ContextSection` / `CachePlacement` 和稳定 renderer | `README.md`、`docs/context-capsule.md`、`docs/agent-core.md` | 已完成:三层布局与 kind priority 解耦,`context_capsule.v1` renderer 可稳定生成 provider 输入,`content` 兼容别名与 `rendered` 保持一致。 | -| [x] | Phase 2a-3:workspace manifest v0 自动构建 | `README.md`、`docs/context-capsule.md`、`docs/tool-system.md` | 已完成:结构化 JSON、canonical `manifestHash`、默认 `maxEntries=500`、硬安全排除、默认工程排除、`.gitignore` + `.prole-coderignore`,并提供可执行 `workspace_manifest` 工具。 | -| [x] | Phase 2a-4:Context Builder 接入 manifest summary 和扩展 `context.built` payload | `docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Turn Loop 自动生成 manifest summary 进入 `StablePrefix`,`context.built` 输出 stable/dynamic/suffix token、sections、manifest hash 和 `max_entries_exceeded` 截断原因。 | -| [x] | Phase 2b-1:TokenEstimator trait 与 `CalibratedEstimator` | `docs/roadmap.md`、`docs/context-capsule.md`、`docs/deepseek-api-adapter.md` | 已完成:新增 `token_estimator` 模块,默认保持 `utf8_bytes`;`CalibratedEstimator` 只保存字节数/实际 token 数和聚合误差,报告 `exact=false`,并有离线 fixture 测试。 | -| [x] | Phase 2b-2:稳定前缀和缓存友好 prompt 布局 | `README.md`、`docs/context-capsule.md`、`docs/deepseek-api-adapter.md` | 已完成:`ContextBuilderConfig` 增加 30% 默认稳定前缀预算,`context.built` 输出 `stablePrefixHash` 和预算字段;修改 `TurnSuffix` 不改变 `StablePrefix`、可选稳定前缀超预算省略均有测试。 | -| [x] | Phase 2c-1:Context Builder 接入 attachments 和 diagnostics | `docs/json-rpc-protocol.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:`agent.sendTurn.attachments` 从拒绝改为消费,支持 file、selection、explicit_content、diagnostic;Core/RPC 已覆盖路径、重复和大小限制测试。 | -| [x] | Phase 2c-2:`provider.completed` 事件和 DeepSeek cache hit/miss 实验 | `README.md`、`docs/roadmap.md`、`docs/deepseek-api-adapter.md`、`docs/testing.md` | 已完成基础闭环:Turn Loop 独立记录模型、duration、usage、cache hit/miss 和 stream 摘要,DeepSeek streaming wrapper 会从 usage chunk 填充字段;更大 cache hit/miss 手动实验留作 Phase 2d 前增强样本。 | -| [x] | Phase 2d-1:200K、500K、900K 样例仓库 token 预算与 Context Capsule 验收 | `README.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:新增 `context_capsule_large_repository_budget_benchmark` ignored/manual 测试,本地跑通 200K、500K、900K 三档样例 Context Capsule,默认 CI 只编译不自动执行。 | -| [x] | Phase 2d-2:超预算解释、Run Log 体积/截断/脱敏边界和 tool call JSON Schema 校验层 | `docs/run-log.md`、`docs/security-model.md`、`docs/agent-core.md`、`docs/tool-system.md` | 已完成:required context 超预算失败和 optional omitted reason 继续由 Context Builder 测试覆盖;Run Log 写入入口统一脱敏和字符串/数组截断并记录 `runLogTruncation`;tool call arguments 在 typed deserialization 前执行注册表 JSON Schema 校验。 | -| [x] | Phase 2e-1:展示型 demo 基础收敛 | `README.md`、`docs/demos.md`、`docs/testing.md` | 已完成:新增 demo 短命令均登记在 `.cargo/config.toml`,测试默认 ignored;`docs/demos.md` 作为统一清单记录用途、运行命令和预期输出。 | -| [x] | Phase 2e-2:`cargo demo-context` | `README.md`、`docs/demos.md`、`docs/context-capsule.md` | 已完成:展示 manifest summary、Context Capsule sections、included/omitted sources 和 `context.built` payload;已运行 `cargo demo-context`。 | -| [x] | Phase 2e-3:`cargo demo-truncation` | `README.md`、`docs/demos.md`、`docs/run-log.md` | 已完成:展示 Run Log 脱敏、截断、`runLogTruncation`,并区分截断、空输出和缺失字段;已运行 `cargo demo-truncation`。 | -| [x] | Phase 2e-4:`cargo demo-schema` | `README.md`、`docs/demos.md`、`docs/tool-system.md` | 已完成:展示 tool call arguments 在 typed deserialization 前被 JSON Schema 拒绝,并输出稳定 `E_INVALID_TOOL_ARGUMENTS`;已运行 `cargo demo-schema`。 | -| [x] | Phase 2e-5:`cargo demo-context-visual` | `README.md`、`docs/demos.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:用 ASCII 视图展示 StablePrefix、DynamicPrelude、TurnSuffix 的 token 分布,并输出原始 JSON;已运行 `cargo demo-context-visual`。 | -| [x] | Phase 2e-6:`cargo demo-attachment` | `README.md`、`docs/demos.md`、`docs/json-rpc-protocol.md`、`docs/context-capsule.md` | 已完成:展示 file、selection、explicit_content、diagnostic attachments 如何进入 Context Builder 和 provider prompt;已运行 `cargo demo-attachment`。 | -| [x] | Phase 2e-7:`cargo demo-live` provider summary 增强 | `README.md`、`docs/demos.md`、`docs/deepseek-api-adapter.md`、`docs/roadmap.md` | 已完成:现有 live demo 的人类可读事件摘要会展示 `provider.completed` 的模型、duration、usage、cache hit/miss 和 stream 字段;离线 fixture 已运行,联网入口仍按 `PROLE_CODER_LIVE_TESTS=1 cargo demo-live` 手动执行。 | +| [x] | P2-1a:`read_file` 增加 `sha256` / `sizeBytes` | `docs/tool-system.md`、`docs/context-capsule.md` | 已完成:`read_file` 返回完整文件的 `sha256` 和 `sizeBytes`,Rust/TypeScript result schema 与单元测试已同步。 | +| [x] | P2-1b:定义 `ContextCapsule` / `ContextSection` / `CachePlacement` 和稳定 renderer | `README.md`、`docs/context-capsule.md`、`docs/agent-core.md` | 已完成:三层布局与 kind priority 解耦,`context_capsule.v1` renderer 可稳定生成 provider 输入,`content` 兼容别名与 `rendered` 保持一致。 | +| [x] | P2-1c:workspace manifest v0 自动构建 | `README.md`、`docs/context-capsule.md`、`docs/tool-system.md` | 已完成:结构化 JSON、canonical `manifestHash`、默认 `maxEntries=500`、硬安全排除、默认工程排除、`.gitignore` + `.prole-coderignore`,并提供可执行 `workspace_manifest` 工具。 | +| [x] | P2-1d:Context Builder 接入 manifest summary 和扩展 `context.built` payload | `docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Turn Loop 自动生成 manifest summary 进入 `StablePrefix`,`context.built` 输出 stable/dynamic/suffix token、sections、manifest hash 和 `max_entries_exceeded` 截断原因。 | +| [x] | P2-2a:TokenEstimator trait 与 `CalibratedEstimator` | `docs/roadmap.md`、`docs/context-capsule.md`、`docs/deepseek-api-adapter.md` | 已完成:新增 `token_estimator` 模块,默认保持 `utf8_bytes`;`CalibratedEstimator` 只保存字节数/实际 token 数和聚合误差,报告 `exact=false`,并有离线 fixture 测试。 | +| [x] | P2-2b:稳定前缀和缓存友好 prompt 布局 | `README.md`、`docs/context-capsule.md`、`docs/deepseek-api-adapter.md` | 已完成:`ContextBuilderConfig` 增加 30% 默认稳定前缀预算,`context.built` 输出 `stablePrefixHash` 和预算字段;修改 `TurnSuffix` 不改变 `StablePrefix`、可选稳定前缀超预算省略均有测试。 | +| [x] | P2-3a:Context Builder 接入 attachments 和 diagnostics | `docs/json-rpc-protocol.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:`agent.sendTurn.attachments` 从拒绝改为消费,支持 file、selection、explicit_content、diagnostic;Core/RPC 已覆盖路径、重复和大小限制测试。 | +| [x] | P2-3b:`provider.completed` 事件和 DeepSeek cache hit/miss 实验 | `README.md`、`docs/roadmap.md`、`docs/deepseek-api-adapter.md`、`docs/testing.md` | 已完成基础闭环:Turn Loop 独立记录模型、duration、usage、cache hit/miss 和 stream 摘要,DeepSeek streaming wrapper 会从 usage chunk 填充字段;更大 cache hit/miss 手动实验留作 P2-4 前增强样本。 | +| [x] | P2-4a:200K、500K、900K 样例仓库 token 预算与 Context Capsule 验收 | `README.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:新增 `context_capsule_large_repository_budget_benchmark` ignored/manual 测试,本地跑通 200K、500K、900K 三档样例 Context Capsule,默认 CI 只编译不自动执行。 | +| [x] | P2-4b:超预算解释、Run Log 体积/截断/脱敏边界和 tool call JSON Schema 校验层 | `docs/run-log.md`、`docs/security-model.md`、`docs/agent-core.md`、`docs/tool-system.md` | 已完成:required context 超预算失败和 optional omitted reason 继续由 Context Builder 测试覆盖;Run Log 写入入口统一脱敏和字符串/数组截断并记录 `runLogTruncation`;tool call arguments 在 typed deserialization 前执行注册表 JSON Schema 校验。 | +| [x] | P2-5a:展示型 demo 基础收敛 | `README.md`、`docs/demos.md`、`docs/testing.md` | 已完成:新增 demo 短命令均登记在 `.cargo/config.toml`,测试默认 ignored;`docs/demos.md` 作为统一清单记录用途、运行命令和预期输出。 | +| [x] | P2-5b:`cargo demo-context` | `README.md`、`docs/demos.md`、`docs/context-capsule.md` | 已完成:展示 manifest summary、Context Capsule sections、included/omitted sources 和 `context.built` payload;已运行 `cargo demo-context`。 | +| [x] | P2-5c:`cargo demo-truncation` | `README.md`、`docs/demos.md`、`docs/run-log.md` | 已完成:展示 Run Log 脱敏、截断、`runLogTruncation`,并区分截断、空输出和缺失字段;已运行 `cargo demo-truncation`。 | +| [x] | P2-5d:`cargo demo-schema` | `README.md`、`docs/demos.md`、`docs/tool-system.md` | 已完成:展示 tool call arguments 在 typed deserialization 前被 JSON Schema 拒绝,并输出稳定 `E_INVALID_TOOL_ARGUMENTS`;已运行 `cargo demo-schema`。 | +| [x] | P2-5e:`cargo demo-context-visual` | `README.md`、`docs/demos.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:用 ASCII 视图展示 StablePrefix、DynamicPrelude、TurnSuffix 的 token 分布,并输出原始 JSON;已运行 `cargo demo-context-visual`。 | +| [x] | P2-5f:`cargo demo-attachment` | `README.md`、`docs/demos.md`、`docs/json-rpc-protocol.md`、`docs/context-capsule.md` | 已完成:展示 file、selection、explicit_content、diagnostic attachments 如何进入 Context Builder 和 provider prompt;已运行 `cargo demo-attachment`。 | +| [x] | P2-5g:`cargo demo-live` provider summary 增强 | `README.md`、`docs/demos.md`、`docs/deepseek-api-adapter.md`、`docs/roadmap.md` | 已完成:现有 live demo 的人类可读事件摘要会展示 `provider.completed` 的模型、duration、usage、cache hit/miss 和 stream 字段;离线 fixture 已运行,联网入口仍按 `PROLE_CODER_LIVE_TESTS=1 cargo demo-live` 手动执行。 | ## Phase 3:VS Code 插件核心与共享 RPC 交互管线 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | RPC 全双工 reader/writer 与独立事件 writer 队列 | `docs/rpc-server.md`、`docs/turn-loop.md`、`docs/run-log.md`、`docs/roadmap.md` | 已完成:`agent.sendTurn` 创建 run 后立即返回 accepted,后台 live `agent.event` 由有界队列和单 writer 持续推送;交互式 RPC 测试覆盖 response-before-event、provider 未完成前早返回、审批批准/拒绝/取消和 resume/listRuns。验收:`cargo test`、`cargo clippy --all-targets -- -D warnings`。 | -| [x] | 长 provider request 期间的 client 断连取消 | `docs/rpc-server.md`、`docs/json-rpc-protocol.md`、`docs/approval-model.md` | 已完成:stdio EOF / shutdown 会取消 active run,writer 失败会触发断连取消句柄并取消 active run 与 pending approvals。 | -| [x] | TypeScript extension scaffold | `README.md`、`docs/vscode-extension.md` | 已完成基础命令和测试骨架。 | -| [x] | RPC server 启动监管 | `README.md`、`docs/vscode-extension.md` | 已能启动 `prole rpc`、发送 initialize、转发事件并处理退出。 | -| [x] | JSON-RPC request client | `README.md`、`docs/vscode-extension.md` | 已管理 request id、pending response、error response 和进程退出清理。 | -| [x] | VS Code/protocol TypeScript 类型共享收敛 | `packages/protocol`、`docs/json-rpc-protocol.md`、`docs/vscode-extension.md` | 已完成:extension 通过 workspace devDependency 消费 `@prole-coder/protocol`,`rpcServer.ts` re-export protocol `AgentEventEnvelope` 类型 alias,删除本地重复 envelope 定义;extension build/typecheck/test/lint 会先构建 protocol 声明。 | -| [x] | VS Code RPC/commands 边界测试补齐 | `docs/vscode-extension.md`、`.agents/communication/daily/2026-05-28/code_review.md` | 已完成:`rpcServer.test.ts` 覆盖 spawn throw、stdio 缺失、invalid JSON、process error、stop pending startup、onEvent dispose、stderrPreview、sendRequest 写入失败和非 agent.event 通知;`commands.test.ts` 覆盖 openChat 启动失败提示、非 Error fallback、`persistable: false` approve 和 paths 拼接。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | Sidebar Chat 和 `agent.event` 渲染 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 贡献 ProleCoder Activity Bar view 和 Webview Sidebar Chat;`ProleChatViewProvider` 订阅 `RpcServerManager.onEvent()`,通过 `ChatEventTimeline` 渲染 assistant delta、tool lifecycle、审批、context/provider 和 terminal event,并合并同一 run/turn 的 assistant delta。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | 文本输入发送 turn 并接收真实 Agent 响应 | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 提供 prompt 输入和 mode 选择,Webview submit 经过 `chatInput` 校验后调用 typed `RpcServerManager.sendTurn()`,accepted 后通过同一 run 的 `agent.event` terminal event 收口输入状态。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | VS Code 审批 UI 接入真实 RPC pending queue | `docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:新增 `ApprovalEventController` 订阅 `tool.approvalRequired`,校验 protocol payload 后调用 VS Code approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 发送 `agent.approve` / `agent.reject`;重复 approvalId 不会重复提示。后续 P5-14 已把默认 UX 从系统 modal 切换为 Sidebar 内联审批卡片。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | 命令风险分类器和动态风险升级 | `README.md`、`docs/approval-model.md`、`docs/tool-system.md`、`docs/turn-loop.md` | 已完成:Agent Core 对 shell 命令做词法分段和显式命令族分类,递归检查 shell 包装器、`$(...)` 和传统反引号子命令,识别依赖安装、网络访问、远程 git、删除和发布命令,升级 `tool.requested` / `tool.approvalRequired` 风险并输出 `riskReasons`;VS Code/CLI/TUI 展示升级原因。验收:`cargo fmt --check`、`cargo test -p prole-coder-agent-core command_risk`、`cargo test -p prole-coder-agent-core turn_loop_upgrades_shell_approval_risk`、`cargo test`、`cargo clippy --all-targets -- -D warnings`、`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`git diff --check`。 | -| [x] | 更强进程树清理策略 | `docs/tool-system.md`、`docs/security-model.md`、`docs/roadmap.md` | 已完成:命令类工具启动时建立可收束的进程树边界,Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底;取消和超时会清理 shell/search/git 等工具的子进程树。验收:`cargo test -p prole-coder-agent-core shell_cancels_descendant_processes`、`cargo test -p prole-coder-agent-core shell_timeout_cleans_descendant_processes`、`cargo test -p prole-coder-agent-core shell_cancels_running_command`。 | -| [x] | Native diff editor 与 hunk 级审批边界 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 侧新增 patch preview controller,缓存 `tool.requested.argumentsPreview.unifiedDiff`,在 `apply_patch` 审批提示前打开 VS Code 原生 diff editor;纯 TS parser 会生成稳定 hunk approval boundary,当前仍以 whole-patch approve/reject 回传,为后续 hunk 级决策预留结构。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | Run List / resume | `README.md`、`docs/vscode-extension.md`、`docs/rpc-server.md` | 已完成:VS Code Sidebar Chat 通过 typed `RpcServerManager.listRuns()` 拉取最近 run summary,Run List 保留 loading/failed/selected 状态;点击历史 run 会调用 `agent.resume` 并清空当前事件视图,随后消费 replay 的 `agent.event`。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | Context Capsule 可视化 | `README.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:VS Code Sidebar Chat 新增 Context Capsule 面板,消费 `context.built` metadata,展示 StablePrefix / DynamicPrelude / TurnSuffix token 分布、input/stable budget、cache/estimator 摘要、included/omitted source 预览和 manifest 摘要。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-1a:RPC 全双工 reader/writer 与独立事件 writer 队列 | `docs/rpc-server.md`、`docs/turn-loop.md`、`docs/run-log.md`、`docs/roadmap.md` | 已完成:`agent.sendTurn` 创建 run 后立即返回 accepted,后台 live `agent.event` 由有界队列和单 writer 持续推送;交互式 RPC 测试覆盖 response-before-event、provider 未完成前早返回、审批批准/拒绝/取消和 resume/listRuns。验收:`cargo test`、`cargo clippy --all-targets -- -D warnings`。 | +| [x] | P3-1b:长 provider request 期间的 client 断连取消 | `docs/rpc-server.md`、`docs/json-rpc-protocol.md`、`docs/approval-model.md` | 已完成:stdio EOF / shutdown 会取消 active run,writer 失败会触发断连取消句柄并取消 active run 与 pending approvals。 | +| [x] | P3-2b:TypeScript extension scaffold | `README.md`、`docs/vscode-extension.md` | 已完成基础命令和测试骨架。 | +| [x] | P3-2c:RPC server 启动监管 | `README.md`、`docs/vscode-extension.md` | 已能启动 `prole rpc`、发送 initialize、转发事件并处理退出。 | +| [x] | P3-2d:JSON-RPC request client | `README.md`、`docs/vscode-extension.md` | 已管理 request id、pending response、error response 和进程退出清理。 | +| [x] | P3-3a:VS Code/protocol TypeScript 类型共享收敛 | `packages/protocol`、`docs/json-rpc-protocol.md`、`docs/vscode-extension.md` | 已完成:extension 通过 workspace devDependency 消费 `@prole-coder/protocol`,`rpcServer.ts` re-export protocol `AgentEventEnvelope` 类型 alias,删除本地重复 envelope 定义;extension build/typecheck/test/lint 会先构建 protocol 声明。 | +| [x] | P3-3b:VS Code RPC/commands 边界测试补齐 | `docs/vscode-extension.md`、`.agents/communication/daily/2026-05-28/code_review.md` | 已完成:`rpcServer.test.ts` 覆盖 spawn throw、stdio 缺失、invalid JSON、process error、stop pending startup、onEvent dispose、stderrPreview、sendRequest 写入失败和非 agent.event 通知;`commands.test.ts` 覆盖 openChat 启动失败提示、非 Error fallback、`persistable: false` approve 和 paths 拼接。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-4a:Sidebar Chat 和 `agent.event` 渲染 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 贡献 ProleCoder Activity Bar view 和 Webview Sidebar Chat;`ProleChatViewProvider` 订阅 `RpcServerManager.onEvent()`,通过 `ChatEventTimeline` 渲染 assistant delta、tool lifecycle、审批、context/provider 和 terminal event,并合并同一 run/turn 的 assistant delta。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-4b:文本输入发送 turn 并接收真实 Agent 响应 | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 提供 prompt 输入和 mode 选择,Webview submit 经过 `chatInput` 校验后调用 typed `RpcServerManager.sendTurn()`,accepted 后通过同一 run 的 `agent.event` terminal event 收口输入状态。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-4c:VS Code 审批 UI 接入真实 RPC pending queue | `docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:新增 `ApprovalEventController` 订阅 `tool.approvalRequired`,校验 protocol payload 后调用 VS Code approval adapter,并通过 typed `RpcServerManager.approve()` / `reject()` 发送 `agent.approve` / `agent.reject`;重复 approvalId 不会重复提示。后续 P5-14 已把默认 UX 从系统 modal 切换为 Sidebar 内联审批卡片。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-5a:命令风险分类器和动态风险升级 | `README.md`、`docs/approval-model.md`、`docs/tool-system.md`、`docs/turn-loop.md` | 已完成:Agent Core 对 shell 命令做词法分段和显式命令族分类,递归检查 shell 包装器、`$(...)` 和传统反引号子命令,识别依赖安装、网络访问、远程 git、删除和发布命令,升级 `tool.requested` / `tool.approvalRequired` 风险并输出 `riskReasons`;VS Code/CLI/TUI 展示升级原因。验收:`cargo fmt --check`、`cargo test -p prole-coder-agent-core command_risk`、`cargo test -p prole-coder-agent-core turn_loop_upgrades_shell_approval_risk`、`cargo test`、`cargo clippy --all-targets -- -D warnings`、`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`git diff --check`。 | +| [x] | P3-5b:更强进程树清理策略 | `docs/tool-system.md`、`docs/security-model.md`、`docs/roadmap.md` | 已完成:命令类工具启动时建立可收束的进程树边界,Unix 使用独立 process group,Windows 使用新 process group、ParentProcessId descendant 枚举和 `taskkill /T /F` 兜底;取消和超时会清理 shell/search/git 等工具的子进程树。验收:`cargo test -p prole-coder-agent-core shell_cancels_descendant_processes`、`cargo test -p prole-coder-agent-core shell_timeout_cleans_descendant_processes`、`cargo test -p prole-coder-agent-core shell_cancels_running_command`。 | +| [x] | P3-5c:Native diff editor 与 hunk 级审批边界 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code 侧新增 patch preview controller,缓存 `tool.requested.argumentsPreview.unifiedDiff`,在 `apply_patch` 审批提示前打开 VS Code 原生 diff editor;纯 TS parser 会生成稳定 hunk approval boundary,当前仍以 whole-patch approve/reject 回传,为后续 hunk 级决策预留结构。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-5d:Run List / resume | `README.md`、`docs/vscode-extension.md`、`docs/rpc-server.md` | 已完成:VS Code Sidebar Chat 通过 typed `RpcServerManager.listRuns()` 拉取最近 run summary,Run List 保留 loading/failed/selected 状态;点击历史 run 会调用 `agent.resume` 并清空当前事件视图,随后消费 replay 的 `agent.event`。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P3-5e:Context Capsule 可视化 | `README.md`、`docs/context-capsule.md`、`docs/vscode-extension.md` | 已完成:VS Code Sidebar Chat 新增 Context Capsule 面板,消费 `context.built` metadata,展示 StablePrefix / DynamicPrelude / TurnSuffix token 分布、input/stable budget、cache/estimator 摘要、included/omitted source 预览和 manifest 摘要。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | ## Phase 4:VS Code 深度集成 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | P4-1:VSIX dry-run packaging smoke | `README.md`、`docs/vscode-extension.md`、`docs/release.md` | 已完成:新增 `pnpm run vsix:smoke` / `vscode/extension/scripts/vsixDryRunSmoke.mjs`,构建 protocol 与 extension 后在 `target/` 下临时生成 VSIX,检查 `.vscodeignore`、`workspace:*` 运行时边界、media asset、compiled `out/`、activationEvents 和包内排除规则,并清理临时产物;不代表 P4-13 完成。验收:`pnpm -r --if-present vsix:smoke`。 | -| [x] | P4-2:`@vscode/test-electron` 最小 harness | `README.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vscode:test-electron` / `vscode/extension/scripts/runVscodeIntegrationTests.mjs`,测试 extension activation、trusted workspace、Chat view focus 和命令注册;测试工作区禁用 RPC autoStart,并已扩展为 P4-14 E2E 入口。 | -| [x] | P4-3:Provider capability model data contract | `README.md`、`docs/roadmap.md`、`docs/deepseek-api-adapter.md`、`docs/json-rpc-protocol.md` | 已完成:新增 ADR 0006;`agent.initialize.capabilities.provider` 暴露 DeepSeek V4 model capability data contract,包含 thinking、tool calls/tool choice、FIM、stream/cache usage、上下文和输出限制,首版不引入 heavy trait。 | -| [x] | P4-4:事件 payload schema 与协议 fixture 对齐 | `docs/json-rpc-protocol.md`、`docs/turn-loop.md`、`packages/protocol` | 已完成:新增 `docs/protocol/event-payloads.v1.json`,将 `provider.requested`、`tool.completed`、`run.completed` 纳入 Rust/TypeScript 兼容性测试;VS Code 初始化协议版本不匹配会给出明确提示。 | -| [x] | P4-5:RPC 高频事件输出节流与批量发送策略 | `docs/rpc-server.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:实时 live event wire 层支持 `agent.eventBatch` 批量发送,VS Code manager 按序分发;Run Log 仍逐事件写入并保持 `seq` 为事实来源,`agent.resume` replay 仍按单事件结构输出。 | -| [x] | P4-6:`agent.cancel` 类型化 helper 与 Chat Cancel UI | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:新增 `RpcServerManager.cancel()` typed helper、Cancel 按钮和运行中 composer 状态;覆盖 typed cancel RPC 边界测试。 | -| [x] | P4-7:Problems 面板诊断进入 Context Builder | `README.md`、`docs/vscode-extension.md`、`docs/context-capsule.md` | 已完成:VS Code 发送 turn 时采集 Problems 快照,并通过 `agent.sendTurn.attachments` 的 diagnostic attachment 注入;前端按协议 attachment 上限裁剪、优先保留 error,Core/Context Builder 继续负责 attachment 校验和 token 预算。 | -| [x] | P4-8:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批 UI 不再暴露复杂持久化选项。 | -| [x] | P4-9:审批持久化存储 | `docs/approval-model.md`、`docs/tool-system.md`、`docs/vscode-extension.md` | 已完成:RPC pending queue 支持 session 内存复用和 workspace `.prole-coder/approvals.v1.json` 存储;继续在 Core 和 RPC 层禁止 network/destructive 风险持久化。 | -| [x] | P4-10:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 不进入 VS Code settings。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | -| [x] | P4-11:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code 审批 UI 提供 selected hunk 选择;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | -| [x] | P4-12:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | -| [x] | P4-13:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | -| [x] | P4-14:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | +| [x] | P4-1a:VSIX dry-run packaging smoke | `README.md`、`docs/vscode-extension.md`、`docs/release.md` | 已完成:新增 `pnpm run vsix:smoke` / `vscode/extension/scripts/vsixDryRunSmoke.mjs`,构建 protocol 与 extension 后在 `target/` 下临时生成 VSIX,检查 `.vscodeignore`、`workspace:*` 运行时边界、media asset、compiled `out/`、activationEvents 和包内排除规则,并清理临时产物;不代表 P4-13 完成。验收:`pnpm -r --if-present vsix:smoke`。 | +| [x] | P4-2a:`@vscode/test-electron` 最小 harness | `README.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vscode:test-electron` / `vscode/extension/scripts/runVscodeIntegrationTests.mjs`,测试 extension activation、trusted workspace、Chat view focus 和命令注册;测试工作区禁用 RPC autoStart,并已扩展为 P4-14 E2E 入口。 | +| [x] | P4-3a:Provider capability model data contract | `README.md`、`docs/roadmap.md`、`docs/deepseek-api-adapter.md`、`docs/json-rpc-protocol.md` | 已完成:新增 ADR 0006;`agent.initialize.capabilities.provider` 暴露 DeepSeek V4 model capability data contract,包含 thinking、tool calls/tool choice、FIM、stream/cache usage、上下文和输出限制,首版不引入 heavy trait。 | +| [x] | P4-4a:事件 payload schema 与协议 fixture 对齐 | `docs/json-rpc-protocol.md`、`docs/turn-loop.md`、`packages/protocol` | 已完成:新增 `docs/protocol/event-payloads.v1.json`,将 `provider.requested`、`tool.completed`、`run.completed` 纳入 Rust/TypeScript 兼容性测试;VS Code 初始化协议版本不匹配会给出明确提示。 | +| [x] | P4-5a:RPC 高频事件输出节流与批量发送策略 | `docs/rpc-server.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:实时 live event wire 层支持 `agent.eventBatch` 批量发送,VS Code manager 按序分发;Run Log 仍逐事件写入并保持 `seq` 为事实来源,`agent.resume` replay 仍按单事件结构输出。 | +| [x] | P4-6a:`agent.cancel` 类型化 helper 与 Chat Cancel UI | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:新增 `RpcServerManager.cancel()` typed helper、Cancel 按钮和运行中 composer 状态;覆盖 typed cancel RPC 边界测试。 | +| [x] | P4-7a:Problems 面板诊断进入 Context Builder | `README.md`、`docs/vscode-extension.md`、`docs/context-capsule.md` | 已完成:VS Code 发送 turn 时采集 Problems 快照,并通过 `agent.sendTurn.attachments` 的 diagnostic attachment 注入;前端按协议 attachment 上限裁剪、优先保留 error,Core/Context Builder 继续负责 attachment 校验和 token 预算。 | +| [x] | P4-8a:Terminal command approval | `README.md`、`docs/vscode-extension.md`、`docs/approval-model.md` | 已完成:审批 payload 支持命令、cwd、风险等级、风险原因、上一条 shell 输出摘要字段和 never/session/workspace 持久化语义;P5-2 后 VS Code 主审批 UI 不再暴露复杂持久化选项。 | +| [x] | P4-9a:审批持久化存储 | `docs/approval-model.md`、`docs/tool-system.md`、`docs/vscode-extension.md` | 已完成:RPC pending queue 支持 session 内存复用和 workspace `.prole-coder/approvals.v1.json` 存储;继续在 Core 和 RPC 层禁止 network/destructive 风险持久化。 | +| [x] | P4-10a:provider、model、预算、审批策略和 RPC 命令配置界面 | `README.md`、`docs/vscode-extension.md` | 已完成:VS Code `ProleCoder: Open Settings` 打开扩展设置,并从 `agent.initialize` ready state 展示 provider、默认模型、context/output budget、模型 capability、审批策略、RPC command/args/autostart 和 state dir;配置贡献只包含非敏感 RPC/FIM 选项,API Key 不进入 VS Code settings。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`。 | +| [x] | P4-11a:真实 hunk 级 patch 审批 | `docs/tool-system.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md` | 已完成:首版限定 `apply_patch`,Core 解析 unified diff 生成稳定 hunk id,`ApprovalDecision::ApprovedHunks` 会过滤 patch 后只应用已批准 hunks;RPC pending queue 校验未知、重复、空 hunk 和持久化误用;VS Code 审批 UI 提供 selected hunk 选择;`tool.approvalRequired` / `tool.approvalResolved` 已扩展并纳入协议 fixture。验收:`cargo test -p prole-coder-agent-core filter_apply_patch_hunks_keeps_only_selected_hunks`、`cargo test -p prole-coder-agent-core turn_loop_applies_only_approved_patch_hunks`、`cargo test -p prole-coder-agent-rpc approval_queue_resolves_hunk_level_patch_decisions`、`pnpm -r test`。 | +| [x] | P4-12a:FIM completion preview | `README.md`、`docs/deepseek-api-adapter.md`、`docs/vscode-extension.md` | 已完成:新增 `agent.previewFim` RPC 类型、Rust request loop 分发、CLI provider factory FIM preview、DeepSeek beta `/completions` FIM adapter、fixture provider 预览和 VS Code 原生 inline completion provider;前端模型选择只使用 P4-3 capability data 的 `supportsFim`,不靠模型名称推断。验收:`cargo test -p prole-coder-agent-rpc request_loop_handles_fim_preview_requests`、`cargo test -p prole-coder-cli fixture_rpc_provider_factory_returns_fim_preview`、`pnpm -r typecheck`、`pnpm -r test`。 | +| [x] | P4-13a:VSIX alpha / pre-release 打包与安装说明 | `docs/release.md`、`docs/vscode-extension.md` | 已完成:新增 `pnpm run vsix:alpha` / `vscode/extension/scripts/packageAlphaVsix.mjs`,构建 protocol 与 extension 后在 `target/vsix/` 保留可安装 pre-release VSIX,并生成 SHA-256 校验和;脚本校验 VSIX manifest 的 pre-release 标记与 publisher/name/version 一致性,`docs/release.md` 记录 clean user-data/extensions 目录安装验收步骤。验收:`pnpm run vsix:alpha`。 | +| [x] | P4-14a:补齐 end-to-end 集成测试覆盖 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:`pnpm run vscode:test-electron` 在隔离 user-data/extensions profile 中启动 VS Code test host,并通过 `vscode/extension/test/fixtures/rpcFixtureServer.mjs` 本地 JSON-RPC fixture 覆盖 extension activation、Chat sendTurn、Problems diagnostic attachments、自动审批回传、Cancel、Run List / resume 和 Chat timeline/submission/context 状态;VSIX 安装后的 clean 环境基础交互继续按 `docs/release.md` 的可重复手动路径验收。 | + ## Phase 5:VS Code Codex-like UX 与开发工作流 -状态:进行中。P5-1 到 P5-13 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX,以及结构化 provider 配置错误码与恢复动作;P5-14 作为持续 UX 测试与体验改进总项,已拆分为 P5-14a 到 P5-14f。G4 自动 commit / push / create PR 留作后续增强。 +状态:进行中。P5-1 到 P5-14 已完成,覆盖原生 Chat、审批简化、自动上下文、测试验收、Output Channel、API key/model 配置、错误恢复、只读 Git context、GitLens-like commit / PR 文案生成工作流、Sidebar 连续会话 / Run 删除 / 折叠事件 UX、结构化 provider 配置错误码与恢复动作,以及真实试用回归修复包;P5-15 真实试用 UX backlog 持续收敛仍未完成。G4 自动 commit / push / create PR 留作后续增强。 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | P5-1:原生 VS Code Chat Participant `@prole` | `README.md`、`docs/vscode-extension.md` | 已完成:贡献 `contributes.chatParticipants` 和 `vscode.chat.createChatParticipant`,让 `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 侧栏并填入 `@prole`;Activity Bar Webview 保留为高级状态面板。验收:`pnpm -r typecheck`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`。 | -| [x] | P5-2:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批动作收敛为 Approve / Reject;`apply_patch` 多 hunk 继续保留 hunk 选择边界;持久化批准能力保留在后端策略与 RPC 队列,不在主审批 UI 里暴露复杂选项。验收:`commands.test.ts` 覆盖 legacy modal adapter 的简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | -| [x] | P5-3:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | -| [x] | P5-4:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | -| [x] | P5-5:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | -| [x] | P5-6:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;API key 配置入口升级为 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 按钮修改 alias 和 trash 按钮删除指定 key;删除非 active key 只刷新 SecretStorage/redaction,删除 active key 才更新 child env 并按 idle 状态重启 RPC;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | -| [x] | P5-7:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并在 Sidebar 错误状态保留修复按钮;API key 配置/清除或 model 切换后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | -| [x] | P5-8:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | -| [x] | P5-9:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | -| [x] | P5-10:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | -| [x] | P5-11:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | -| [x] | P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | -| [x] | P5-13:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | -| [ ] | P5-14:持续 UX 测试与体验改进总项 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 父项:用于承接真实 VS Code 插件试用后的体验修复、稳定性增强和回归验收。只有 P5-14a 到 P5-14f 全部完成后,P5-14 才能标记完成,Phase 5 才能整体标记完成。 | +| [x] | P5-1a:原生 VS Code Chat Participant `@prole` | `README.md`、`docs/vscode-extension.md` | 已完成:贡献 `contributes.chatParticipants` 和 `vscode.chat.createChatParticipant`,让 `ProleCoder: Open Chat` 优先打开 VS Code 原生 Chat 侧栏并填入 `@prole`;Activity Bar Webview 保留为高级状态面板。验收:`pnpm -r typecheck`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`。 | +| [x] | P5-2a:审批 UX 简化 | `README.md`、`docs/approval-model.md`、`docs/vscode-extension.md` | 已完成:主审批动作收敛为 Approve / Reject;`apply_patch` 多 hunk 继续保留 hunk 选择边界;持久化批准能力保留在后端策略与 RPC 队列,不在主审批 UI 里暴露复杂选项。验收:`commands.test.ts` 覆盖 legacy modal adapter 的简化 choices、一次性 approve、reject/dismiss 和 hunk 选择。 | +| [x] | P5-3a:自动上下文压缩 | `README.md`、`docs/context-capsule.md`、`docs/json-rpc-protocol.md` | 已完成:Sidebar Chat 和原生 Chat Participant 从历史对话/事件流生成受限长度的 `explicit_content` attachment,交给已有 Context Capsule 处理,让连续对话自然承接上下文;Sidebar timeline 单条消息会先限长,避免极端长流式输出造成过大的中间文本。验收:`automaticContext.test.ts` 和 `chatParticipantCore.test.ts` 覆盖压缩、预算裁剪、单条 timeline 消息限长、attachment 合并和 turn runner 注入。 | +| [x] | P5-4a:UX 收敛测试与打包验收 | `README.md`、`docs/testing.md`、`docs/release.md` | 已完成:覆盖自动上下文压缩、原生 Chat Participant turn runner、简化审批 choices、extension-host E2E、VSIX smoke/alpha 打包和文档一致性,并补充 Chat Participant 早到 terminal event 缓冲回归测试。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | +| [x] | P5-5a:VS Code Output Channel 错误诊断 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:插件创建 `ProleCoder` Output Channel,Sidebar Chat 的 sendTurn、Run List refresh/resume/cancel 失败、原生 `@prole` Chat Participant turn 失败以及 RPC 启动/运行 warning 会写入完整日志;activation 层使用统一 notifier 分发 Output Channel 日志与 VS Code toast;侧边栏保留短状态并通过 title 暴露完整文本。验收:`logging.test.ts` 覆盖日志格式与输出分发,`chatParticipantCore.test.ts` 覆盖 RPC 失败写入 logger。 | +| [x] | P5-6a:DeepSeek API key SecretStorage、model selector 与 provider status | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增 `ProleCoder: Configure DeepSeek API Key` / `Clear DeepSeek API Key` / `Select DeepSeek Model` / `Show Provider Status` 命令;API key 配置入口升级为 SecretStorage 多 key 管理器,列表展示 alias 与 masked key,支持 `+ Add` 添加 key+alias、选择 active key、行内 edit 按钮修改 alias 和 trash 按钮删除指定 key;删除非 active key 只刷新 SecretStorage/redaction,删除 active key 才更新 child env 并按 idle 状态重启 RPC;provider status 同时显示 key 来源/alias 与当前 model;Sidebar composer 常驻 Key/Model 按钮;SecretStorage 优先、process env fallback、missing 明确展示;RPC child env 继承 `process.env` 后覆盖 active `DEEPSEEK_API_KEY` 与选中的 `DEEPSEEK_MODEL`,不改变 CLI env 路径。 | +| [x] | P5-7a:统一 redaction 与 API key 错误恢复 UX | `docs/vscode-extension.md`、`docs/testing.md`、`docs/security-model.md` | 已完成:在 notifier/logger 边界统一脱敏 SecretStorage/env 中的 key,覆盖 Output Channel、toast 和 RPC startup failure;缺少 `DEEPSEEK_API_KEY` 时 Sidebar/原生 Chat 自动打开配置入口并在 Sidebar 错误状态保留修复按钮;API key 配置/清除或 model 切换后 idle 状态自动重启 RPC,active run 场景提示当前回合结束后生效。 | +| [x] | P5-8a:Git context 只读采集与大 diff attachment 管线 | `docs/vscode-extension.md`、`docs/testing.md`、`docs/context-capsule.md` | 已完成:优先使用 VS Code Git API 采集 repository、branch/upstream 和 staged diff;git CLI 仅作受控 fallback,cwd 来自 repository root;commit/PR 命令把 diff context 作为 `explicit_content` attachment 交给现有 Context Capsule 预算管线。 | +| [x] | P5-9a:Generate Commit Message 写入 Source Control inputBox | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:新增命令从 staged diff 生成 Conventional Commit 风格候选 message,并写入 `repository.inputBox.value`;staged 为空时才询问是否使用 unstaged context;不自动 commit。 | +| [x] | P5-10a:Generate PR Description markdown 生成 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:基于 upstream tracking branch、`main`、`master` 或用户选择确定 base,采集 branch diff/stat/commit summary,生成 PR title/body markdown;首版提供有标题的 untitled markdown 预览/复制/打开入口,不自动创建 PR。 | +| [x] | P5-11a:Phase 5 UX 工作流验收与文档收敛 | `README.md`、`docs/roadmap.md`、`docs/testing.md` | 已完成:补齐 P5-6 到 P5-10 的单元测试、extension-host 验收、VSIX smoke/alpha 验证和文档一致性检查;Git workflow agent 终态事件已补幂等保护,明确 G4 自动 commit / push / create PR 为后续增强,需要接入审批模型后再做。验收:`pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 | +| [x] | P5-12a:Sidebar 连续会话、Run 删除与折叠事件 UX | `README.md`、`docs/vscode-extension.md`、`docs/json-rpc-protocol.md`、`docs/testing.md` | 已完成:RPC/protocol 新增 `agent.deleteRun`;Run Log 支持删除 inactive run;`agent.sendTurn.runId` 可复用已有 run 并自动递增 `turn_N`,Sidebar Chat 在 resume 后继续同一会话发送多轮 turn;tool/provider/request 等过程事件默认折叠,assistant 文本和最终 `run.completed.summary` 保持可见;完整事件 payload 写入 `Output > ProleCoder` 便于 debug;Turn Loop 默认注入最终回复摘要契约。验收:新增 Rust/TS 单元测试覆盖 deleteRun、多 turn run log、折叠 timeline、typed RPC delete 和 runHistory delete message。 | +| [x] | P5-13a:结构化 provider 配置错误码与恢复动作 | `docs/json-rpc-protocol.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:DeepSeek 缺少 API key 的 RPC `E_PROVIDER_ERROR` 返回 `data.provider` / `data.configurationError` / `data.recoverableAction`;VS Code Sidebar 和原生 Chat Participant 依据结构化 recoverable action 展示/触发 API key 配置入口,不再依赖后端英文错误消息;Run failed payload 同样可携带恢复动作。验收:新增 protocol、providerConfigurationUx、Chat Participant 和 CLI 单元测试覆盖结构化错误数据。 | | [x] | P5-14a:Sidebar 对话专属视图与就地确认 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:进入 run 后切换 Sidebar 对话专属视图并隐藏 Runs/Context 管理面板;approval 和 run delete 改为 webview 就地确认 UI,避免系统 modal 打断对话流。验收:已补充相关 Sidebar 状态和消息处理测试。 | | [x] | P5-14b:默认对话流、Work log 折叠和输入快捷键 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:默认只显示用户消息与 DeepSeek 回复;tool/provider/context/run completed 等过程事件收敛到默认折叠的 Work log;Work log 摘要只暴露当前工作状态;输入框 Enter 发送且 Shift+Enter 换行。验收:已补充 timeline / chat input 回归测试。 | | [x] | P5-14c:malformed tool-call 本地诊断文件 | `docs/json-rpc-protocol.md`、`docs/run-log.md`、`docs/turn-loop.md` | 已完成:当 provider 返回的 tool-call `function.arguments` 无法解析为 JSON 时,Turn Loop 会在当前 run 的 `diagnostics/invalid-tool-arguments--.json` 写入脱敏后的累计 arguments,并在 `run.failed.diagnosticFile` / Sidebar failure card 指向该文件。验收:`cargo test -p prole-coder-agent-core diagnostic`、`pnpm --filter prole-coder-vscode test`。 | | [x] | P5-14d:Sidebar UX extension-host E2E 回归 | `docs/testing.md`、`docs/vscode-extension.md` | 已完成:extension-host 测试新增 test-mode webview probe,覆盖真实 webview 中 resume 后继续发送新 turn、默认事件渲染、webview 内联确认后的 run 删除与 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 默认折叠展示和 Enter / Shift+Enter 输入行为。验收:`pnpm run vscode:test-electron`。 | | [x] | P5-14e:大 patch / 大文件分块式工具参数协议 | `docs/tool-system.md`、`docs/turn-loop.md`、`docs/run-log.md` | 已完成:Run Log 支持 run-scoped `payloads/` 文件写入、chunk append 和读取;`apply_patch` schema 新增 `payloadRef`,Turn Loop 可在 `tool.requested` 保留轻量 preview,并在执行前从当前 run 的 payload 文件 materialize `unifiedDiff`,校验 `sha256` / `sizeBytes` 后复用现有 schema、路径安全、审批和 hunk 边界。chunk 追加阶段不写 workspace。验收:`cargo test -p prole-coder-agent-core payload`、`cargo test -p prole-coder-agent-core apply_patch_schema_accepts_run_scoped_payload_refs`。 | -| [ ] | P5-14f:真实试用 UX backlog 收敛 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model/Settings、审批、Chat、Output 日志和上下文压缩等体验问题,并把可执行项登记为 P5-14 子项或后续 Phase 任务。本轮已修复 `finishReason=length` 时误写空/截断 `run.completed.summary` 的问题,Core 会自动追问最终工作总结;也补强了 shell 工具的 `cwd`/平台提示与 Windows PowerShell UTF-8 stderr/stdout 输出,避免真实试用中出现 `/home/... &&` 误用后错误信息乱码;Sidebar composer 补齐 Settings 按钮,直达扩展设置页,便于发现和编辑 `prole-coder.rpc.args` 等 RPC 参数;DeepSeek adapter 对发送阶段 transient timeout/connect/request 错误增加有界重试,缓解 Windows `os error 10053` 直接导致 run 失败的问题。持续试用项仍未完成。 | +| [x] | P5-14f:真实 provider / shell 运行稳定性收敛 | `docs/turn-loop.md`、`docs/tool-system.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:修复 `finishReason=length` 时误写空/截断 `run.completed.summary` 的问题,Core 会自动追问最终工作总结;补强 shell 工具的 `cwd`/平台提示与 Windows PowerShell UTF-8 stderr/stdout 输出,避免真实试用中出现 `/home/... &&` 误用后错误信息乱码;DeepSeek adapter 对发送阶段 transient timeout/connect/request 错误增加有界重试,缓解 Windows `os error 10053` 直接导致 run 失败的问题。 | +| [x] | P5-14g:Sidebar Markdown 渲染与渲染错误隔离 | `docs/vscode-extension.md`、`docs/testing.md` | 已完成:Sidebar 可见对话消息使用安全 DOM Markdown 渲染,覆盖标题、列表、代码块、表格、链接和行内格式;Work log / 工具输出继续保持纯文本便于排错;单条 Markdown 渲染失败会回退为纯文本,并通过 webview error -> `Output > ProleCoder` 记录诊断。验收:`pnpm --filter prole-coder-vscode typecheck`、`pnpm --filter prole-coder-vscode test`、`pnpm run vscode:test-electron`。 | +| [x] | P5-14h:Sidebar composer 交互与 Settings 入口回归修复 | `README.md`、`docs/vscode-extension.md`、`docs/testing.md` | 已完成:Enter 与 Send 按钮统一提交路径,Shift+Enter 继续换行;发送后立即显示 pending 用户消息,避免等待后端事件时界面空白;Settings 入口移到右上角齿轮按钮,composer 保留 API Key / Model 快捷入口。验收:`pnpm --filter prole-coder-vscode typecheck`、`pnpm --filter prole-coder-vscode test`、`pnpm run vscode:test-electron`。 | +| [x] | P5-15a:Sidebar webview bootstrap、历史加载与发送交互回归修复 | `docs/testing.md`、`docs/vscode-extension.md` | 已完成:webview 初始化和事件处理增加异常隔离并写入 `Output > ProleCoder`,初始化成功会发送 ready 诊断;CSP 明确允许 VS Code webview nonce script 执行,Markdown parser 在 HTML template literal 中的反斜杠转义已修正,避免 inline script parse error 后历史加载和发送事件全部失效;`agent.event` 高频 replay 的 snapshot/submission/context 推送改为短延迟合并,避免历史 `assistant.delta` 连续回放时反复完整 Markdown 重渲染卡住 Sidebar;安全 DOM Markdown renderer 已补齐 horizontal rule 分隔线渲染和表格 `\|` 转义边界;新增 `webviewHtml.test.ts` 解析生成后的内联脚本,extension-host fixture 用中文 Markdown 表格、inline code 与 horizontal rule 分块 replay 覆盖历史对话渲染;Enter 默认发送、Shift+Enter 换行,composition 状态下 Enter 先阻止 textarea 换行但不误发;Send/keyboard 交互继续由 extension-host probe 覆盖。 | +| [ ] | P5-15b:真实试用 UX backlog 持续收敛 | `README.md`、`docs/testing.md`、`docs/vscode-extension.md` | 未完成:继续根据真实 VS Code 插件试用收集 Runs、Key/Model/Settings、审批、Chat、Output 日志、上下文压缩、Markdown 兼容性和大工具参数稳定性问题;新增可执行项时优先拆成新的 P5-15 子任务或后续 Phase 任务,不能把新需求继续堆进本行。 | +| [ ] | P5-15c:Sidebar Markdown renderer 模块化与独立边界测试 | `docs/testing.md`、`docs/vscode-extension.md` | 未完成:将当前 webview inline Markdown parser 抽取为可独立测试的模块或共享 renderer,覆盖 horizontal rule、表格转义、嵌套/未闭合行内标记、链接边界和大文本性能;当前已由 `webviewHtml.test.ts` parse smoke 与 extension-host probe 覆盖主要回归。 | ## Phase 6:TUI 与生态扩展 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [ ] | TUI RPC 入口和事件流消费 | `README.md`、`docs/tui.md` | 消费 `agent.event`,展示 run、turn、工具和审批状态。 | -| [ ] | TUI Chat / Plan / Diff / Tools / Context / Settings 页面 | `README.md`、`docs/tui.md` | 完整 ratatui 界面仍未实现。 | -| [ ] | TUI hunk 级审批、run resume、配置文件和 release binary | `README.md`、`docs/tui.md` | 建议在 VS Code 核心体验和共享事件管线稳定后推进。 | -| [ ] | 多 active run 与事件订阅模型 | `docs/rpc-server.md`、`docs/turn-loop.md`、`docs/tool-system.md` | 扩展 active run、审批队列、取消句柄和事件订阅模型,支持多个 run 或多个前端并发推进。 | -| [ ] | 更细的 replay 语义 | `docs/rpc-server.md`、`docs/tool-system.md`、`docs/run-log.md` | 明确 resume 时哪些事件原样回放、哪些需要历史标记,并与 pending approval / hunk 审批状态保持一致。 | -| [ ] | MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具、审计包导出 | `docs/roadmap.md` | 生态扩展应在核心闭环、编辑器体验和 DeepSeek 差异化稳定后推进。 | +| [ ] | P6-1a:TUI RPC 入口和事件流消费 | `README.md`、`docs/tui.md` | 消费 `agent.event`,展示 run、turn、工具和审批状态。 | +| [ ] | P6-1b:TUI Chat / Plan / Diff / Tools / Context / Settings 页面 | `README.md`、`docs/tui.md` | 完整 ratatui 界面仍未实现。 | +| [ ] | P6-2a:TUI hunk 级审批、run resume、配置文件和 release binary | `README.md`、`docs/tui.md` | 建议在 VS Code 核心体验和共享事件管线稳定后推进。 | +| [ ] | P6-3a:多 active run 与事件订阅模型 | `docs/rpc-server.md`、`docs/turn-loop.md`、`docs/tool-system.md` | 扩展 active run、审批队列、取消句柄和事件订阅模型,支持多个 run 或多个前端并发推进。 | +| [ ] | P6-3b:更细的 replay 语义 | `docs/rpc-server.md`、`docs/tool-system.md`、`docs/run-log.md` | 明确 resume 时哪些事件原样回放、哪些需要历史标记,并与 pending approval / hunk 审批状态保持一致。 | +| [ ] | P6-4a:MCP client、本地模型/私有推理服务 adapter、包管理器工具、issue/PR 工具、审计包导出 | `docs/roadmap.md` | 生态扩展应在核心闭环、编辑器体验和 DeepSeek 差异化稳定后推进。 | ## Phase 7:发布与治理 | 状态 | 任务 | 来源 | 说明 | | --- | --- | --- | --- | -| [x] | 许可证策略确定为 AGPL-3.0-or-later | `README.md`、`docs/release.md`、`docs/adr/0003-use-agpl-3.0-or-later.md` | 正式发布文件仍在后续任务。 | -| [ ] | 发布 `LICENSE`、源码获取说明和网络服务源码提供说明 | `README.md`、`docs/release.md` | 发布前必需。 | -| [ ] | 发布源码包、Cargo crate、npm wrapper、VSIX、GitHub Release 校验和 | `README.md`、`docs/release.md` | 需要发布脚本和产物签名/校验策略。 | -| [ ] | 公开 roadmap、issue 模板和贡献流程增强 | `README.md`、`CONTRIBUTING.md` | 面向外部协作者。 | -| [ ] | reproducible build 说明 | `README.md`、`docs/release.md` | 发布可信度要求。 | +| [x] | P7-1a:许可证策略确定为 AGPL-3.0-or-later | `README.md`、`docs/release.md`、`docs/adr/0003-use-agpl-3.0-or-later.md` | 正式发布文件仍在后续任务。 | +| [ ] | P7-2a:发布 `LICENSE`、源码获取说明和网络服务源码提供说明 | `README.md`、`docs/release.md` | 发布前必需。 | +| [ ] | P7-3a:发布源码包、Cargo crate、npm wrapper、VSIX、GitHub Release 校验和 | `README.md`、`docs/release.md` | 需要发布脚本和产物签名/校验策略。 | +| [ ] | P7-4a:公开 roadmap、issue 模板和贡献流程增强 | `README.md`、`CONTRIBUTING.md` | 面向外部协作者。 | +| [ ] | P7-5a:reproducible build 说明 | `README.md`、`docs/release.md` | 发布可信度要求。 | diff --git a/docs/roadmap.md b/docs/roadmap.md index e5652bd..bf9d5f8 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # 路线图 -状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、Phase 2e 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列和 Phase 4 VS Code 深度集成已完成;Phase 5 VS Code Codex-like UX 与开发工作流进行中。 +状态:草案,Phase 1 Agent Core MVP、合并主线前离线最终验收、Phase 2 的 1M Context Capsule 核心收敛、P2-5 展示型 demo 扩展、Phase 3 VS Code 插件核心与共享 RPC 事件队列和 Phase 4 VS Code 深度集成已完成;Phase 5 VS Code Codex-like UX 与开发工作流进行中。 本文档把 README 中的大阶段拆成更可执行的优先级。README 保留项目入口和高层计划;这里记录跨模块的落地顺序、取舍和验收重点。具体任务的阶段、状态和来源统一登记在 `docs/phase-tasks.md`,阶段条目标记完成前应同步检查并更新该索引。 @@ -105,12 +105,12 @@ P0 不追求: 目标:让 VS Code 插件成为 Agent Core 的薄前端,而不是第二套 Agent。 -Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-13 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作;P5-14 作为持续 UX 测试与体验改进总项,子项状态以 `docs/phase-tasks.md` 为准。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。Phase 2e 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 +Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 深度集成任务;Phase 5 仍在进行中。P5-1 到 P5-14 已完成原生 Chat 入口、简化审批、自动上下文压缩、UX 验收、Output Channel 错误诊断、插件内多 API key/model 配置、统一 redaction/错误恢复、只读 Git context、commit message 写入 SCM inputBox、PR markdown 生成、Sidebar 连续会话、Run 删除和折叠事件 UX、结构化 provider 配置错误码与恢复动作,以及真实试用回归修复包;P5-15 作为持续 UX backlog,子项状态以 `docs/phase-tasks.md` 为准。Phase 5 全部完成后再进入 Phase 6,与生态扩展一起推进。Marketplace 发布不阻塞 Phase 4/5,当前已具备可安装 VSIX alpha / pre-release 产物和安装说明。P2-5 展示型 demo 已经给 VS Code Context Viz / Approval / Run Log UI 提供可观察样本。 优先事项: - VSIX dry-run packaging smoke 和 `@vscode/test-electron` 最小 harness 已完成,已提前验证打包、activation、trusted workspace 和 Chat view 基础加载。 -- Phase 5 的 P5-1 到 P5-13 Codex-like UX 与开发工作流已完成;P5-14 持续 UX 测试与体验改进已拆分为 P5-14a 等子项,仍有未完成项。 +- Phase 5 的 P5-1 到 P5-14 Codex-like UX 与开发工作流已完成;P5-15 真实试用 UX backlog 持续收敛仍有未完成项。 - G4 自动 commit / push / create PR 暂不纳入 Phase 5 完成口径,后续需要接入审批模型后再做。 - Provider capability model data contract 已完成,首版通过 `agent.initialize` 暴露给前端,不引入 heavy trait。 - 事件 payload schema、协议 fixture 与 RPC 高频事件批量发送已完成,batch 不改变 Run Log `seq` 和 replay 语义。 @@ -121,7 +121,8 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code - 配置界面依赖 Provider capability model;provider、model、预算、审批策略和 RPC 命令配置都不得保存 API Key。 - P5-12 已完成:Sidebar Chat 复用 `agent.sendTurn.runId` 继续同一 run 多轮对话,支持 `agent.deleteRun` 删除 inactive run;tool/provider/request 等过程事件默认折叠,完整 payload 写入 `Output > ProleCoder`。 - P5-13 已完成:provider 配置失败从前端字符串匹配升级为 RPC 结构化错误数据,缺少 DeepSeek API key 时返回 `E_PROVIDER_ERROR` 和 `data.recoverableAction`,供 VS Code/TUI 统一展示配置动作。 -- P5-14:持续 UX 测试与体验改进总项,用于真实插件试用后的 Runs、Key/Model/Settings、审批、Chat、Output 日志、上下文压缩和大 patch / 大文件工具参数问题回归;具体 P5-14a 到 P5-14f 子项见 `docs/phase-tasks.md`。 +- P5-14:真实试用回归修复包已完成,用于收敛 Sidebar 对话视图、内联确认、Work log 折叠、Markdown 渲染、webview 渲染诊断、provider/shell 稳定性和 composer / Settings 入口回归;具体 P5-14a 到 P5-14h 子项见 `docs/phase-tasks.md`。 +- P5-15:真实试用 UX backlog 持续收敛仍未完成,用于登记新的 Runs、Key/Model/Settings、审批、Chat、Output 日志、上下文压缩和大工具参数稳定性问题。 - 真实 hunk 级 patch 审批首版限定 `apply_patch`,再扩展 Core/RPC 审批决策和 Run Log 记录。 - FIM completion preview 依赖 Provider capability model,优先评估 VS Code 原生 inline completion 接入。 - VSIX alpha / pre-release 交付已完成,`pnpm run vsix:alpha` 会生成可安装 pre-release VSIX 和 SHA-256 校验和;end-to-end 集成测试已通过本地 JSON-RPC fixture server 覆盖 Chat sendTurn、Cancel、Problems diagnostics、自动审批、Run List / resume。 @@ -135,7 +136,7 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 当前验收重点: -- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-14 子项未完成,README 不能把 Phase 5 写成整阶段完成。 +- Phase 4 的 14 个条目已全部在 `docs/phase-tasks.md` 标记 `[x]`;Phase 5 仍有 P5-15 子项未完成,README 不能把 Phase 5 写成整阶段完成。 - VS Code 插件可通过 VSIX 安装到 clean 环境。 - fixture provider 下 Chat sendTurn、Cancel、Problems diagnostics、审批和 Run List / resume 至少有一条 extension-host 或可重复手动验收路径。 - CLI 与 VS Code 对同一 fixture task 的关键 Run Log event type 顺序一致。 @@ -150,30 +151,30 @@ Phase 3 已交付 VS Code 插件核心体验;Phase 4 已完成 14 项 VS Code 目标:把 DeepSeek V4 的长上下文和思考模式变成可见、可审计的工作流。 -Phase 2 的 1M Context Capsule 按 4 个增量轮次推进: +Phase 2 的 1M Context Capsule 按 5 个 README 大项推进: -1. **Phase 2a:Context Capsule 数据模型与 Manifest v0** +1. **P2-1:Context Capsule 数据模型与 Workspace Manifest** - [x] `read_file` 增加 `sha256` / `sizeBytes`。 - [x] 定义 `ContextCapsule`、`ContextSection`、`CachePlacement` 和稳定 renderer。 - [x] 实现 workspace manifest v0:结构化 JSON、canonical `manifestHash`、默认 `maxEntries=500`、硬安全排除、默认工程排除、`.gitignore` + `.prole-coderignore`。 - [x] Context Builder 接入 manifest summary,并扩展 `context.built` payload。 -2. **Phase 2b:TokenEstimator 与稳定前缀** +2. **P2-2:TokenEstimator 与稳定前缀** - [x] 建立 `TokenEstimator` trait,保留 `utf8_bytes` 默认估算器。 - [x] 增加基于 provider usage 样本的 `CalibratedEstimator`,但仍标注 `exact=false`,且不保存 prompt 原文。 - [x] 按 `CachePlacement::{StablePrefix, DynamicPrelude, TurnSuffix}` 构建缓存友好 prompt,并输出 `stablePrefixHash` 与稳定前缀预算。 -3. **Phase 2c:Attachments、provider summary 与 cache 实验** +3. **P2-3:Attachments、provider summary 与 cache 实验** - [x] 接入 `agent.sendTurn.attachments` 的 file、selection/explicit_content、diagnostic。 - [x] 新增 `provider.completed` 事件,记录模型、duration、usage、cache hit/miss 和 stream 摘要。 - - [x] 建立 DeepSeek cache hit/miss ignored live experiment 的基础解析路径;更大重复前缀样本归入 Phase 2d 前增强。 + - [x] 建立 DeepSeek cache hit/miss ignored live experiment 的基础解析路径;更大重复前缀样本归入 P2-4 前增强。 -4. **Phase 2d:大仓库验收与体积控制** +4. **P2-4:大仓库验收与体积控制** - [x] 200K、500K、900K 样例仓库 token 预算和 Context Capsule ignored/manual 验收。 - [x] 超预算解释、Run Log 输出截断和脱敏包边界。 - [x] tool call JSON Schema 通用校验层,且在 typed deserialization 前执行。 -5. **Phase 2e:合并主线前展示型 demo 扩展** +5. **P2-5:合并主线前展示型 demo 扩展** - [x] `demo-context`:展示 manifest summary、Context Capsule sections、included/omitted sources 和 `context.built` payload。 - [x] `demo-truncation`:展示 Run Log 脱敏、截断、`runLogTruncation`,并区分截断、空输出和缺失字段。 - [x] `demo-schema`:展示 tool call arguments 在 typed deserialization 前被 JSON Schema 拒绝。 diff --git a/docs/testing.md b/docs/testing.md index 16a5170..ce32837 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -87,10 +87,12 @@ Phase 5 P5-6 到 P5-13 的 API key/model、Git 工作流、Sidebar 连续会话 - `providerSecretCommands.test.ts` 覆盖 Key 管理器的 `+ Add` 添加 key+alias、选择已有 active key、行内 edit 按钮修改 alias、trash 按钮删除非 active key、Clear API key、Select DeepSeek Model、含 model 的 provider status、idle 状态 RPC restart、active run 场景提示稍后生效。 - `gitWorkflow.test.ts` 覆盖 staged diff、unstaged fallback、upstream/main base 选择、Generate Commit Message 写入 `repository.inputBox.value` 且不自动 commit、agent 重复 terminal event 只采纳首个终态、Generate PR Description 输出 markdown 且不自动创建 PR。 - `rpcServer.test.ts` 覆盖 RPC child env 注入、key 轮换后重启使用新 env,以及 typed `agent.deleteRun` request;`providerSecretCommands.test.ts` 覆盖 model 切换后的 env 更新与 idle restart;`test/electron/index.ts` 覆盖新增命令在 VS Code test host 中注册。 -- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、用户消息/DeepSeek 回复默认可见、过程事件收敛到 Work log,以及无 assistant 时 `run.completed` 摘要仍可见;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 +- `chatEvents.test.ts` 覆盖 tool/raw 过程事件默认折叠、用户消息/DeepSeek 回复默认可见、过程事件收敛到 Work log,以及无 assistant 时 `run.completed` 摘要仍可见;`runHistory.test.ts` 覆盖 `deleteRun` webview message 解析;`chatApprovals.test.ts` 覆盖内联审批消息解析;`webviewSerialization.test.ts` 覆盖 webview 初始 JSON 的 `undefined` 与 `<` 转义;`webviewHtml.test.ts` 覆盖生成后的 Sidebar HTML 内联脚本可被 JavaScript parser 解析,防止 template literal 反斜杠转义回归导致 webview ready 静默超时;extension-host webview probe 覆盖可见对话 Markdown 的代码块、表格、链接、inline code、horizontal rule 和中文表格 summary,并通过分块 replay 历史 `assistant.delta` 覆盖 Sidebar 高频事件合并刷新;Rust `agent-rpc`/`run_log` 测试覆盖同一 run 多 turn 续号和 inactive run 删除;`prole-coder-cli` 测试覆盖 DeepSeek missing API key RPC structured data。 - 文档/打包验收已运行 `pnpm -r typecheck`、`pnpm -r lint`、`pnpm -r test`、`pnpm run vscode:test-electron`、`pnpm run vsix:smoke`、`pnpm run vsix:alpha`、`git diff --check` 和敏感信息扫描。 -P5-14 作为持续 UX 测试与体验改进占位,保留真实插件试用后的回归入口;新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再在 `docs/phase-tasks.md` 中更新完成口径。P5-14d 已在 extension-host 中通过 test-mode webview probe 覆盖 resume 后继续发送新 turn、事件渲染、webview 内联确认完成 run 删除及 Run List 刷新、Sidebar 内联审批卡片、对话专属视图、Work log 折叠展示和 Enter / Shift+Enter 输入行为。 +P5-14 作为真实试用回归修复包,已把已发现的 Sidebar 对话、Work log、Markdown、provider/shell 稳定性和 composer / Settings 入口问题拆成 `docs/phase-tasks.md` 中的 P5-14a 到 P5-14h 子任务。新增 Runs、Key/Model、审批、Chat、Output 日志或上下文压缩体验问题时,应先补可重复测试或手动验收说明,再登记到 P5-15 或后续 Phase 任务;P5-15 保留真实试用后的持续 backlog,不得在完成前标记 Phase 5 完成。 + +P5-15 已开始登记真实 VS Code 试用回归。Sidebar webview bootstrap、历史加载、Send 按钮、Enter/Shift+Enter、composition 状态输入行为、Markdown horizontal rule / 表格转义兼容性和历史 replay 高频事件应优先通过 extension-host webview probe 覆盖;涉及 HTML template literal 内联脚本的改动还应通过 `webviewHtml.test.ts` 的 parse smoke 覆盖。Markdown renderer 抽取成独立模块和更细边界测试已登记为 P5-15c。若本机 VS Code mutex 阻塞 `pnpm run vscode:test-electron`,必须至少运行 extension 单元测试并记录需要关闭测试实例后重跑 E2E。 ## 新增测试的协作要求 @@ -171,7 +173,7 @@ Phase 2 的默认 CI 应优先覆盖离线、确定性测试: - 200K、500K、900K 样例仓库 Context Capsule 生成和 token 预算报告。 - 真实多文件任务展示 manifest、选中文件/诊断、token 预算、provider usage/cache 和最终验证结果。 -Phase 2d 的大上下文手动入口: +P2-4 的大上下文手动入口: ```powershell cargo test -p prole-coder-agent-core --test context_capsule_benchmark context_capsule_large_repository_budget_benchmark -- --ignored --exact --nocapture @@ -179,7 +181,7 @@ cargo test -p prole-coder-agent-core --test context_capsule_benchmark context_ca 该测试生成 200K、500K、900K 三档确定性样例 Context Capsule,输出 `inputTokens`、section tokens 和 omitted source 数量;默认 CI 只编译 ignored test,不自动执行。 -Phase 2c/2d 的 cache usage 手动入口: +P2-3/P2-4 的 cache usage 手动入口: ```powershell cargo test -p prole-coder-agent-core --test deepseek_api_live live_cache_usage_summary_smoke_test -- --ignored --exact --nocapture @@ -215,4 +217,4 @@ rg -n "sk-[A-Za-z0-9_-]+|C:\\User[s]\\|/Users/[^/]+/|/home/[^/]+/|DEEPSEEK_(CODE 展示型 demo 的完整清单、运行命令和预期输出见 `demos.md`。`cargo demo`、`cargo demo-live`、`cargo demo-context`、`cargo demo-context-visual`、`cargo demo-truncation`、`cargo demo-schema` 和 `cargo demo-attachment` 均来自 `.cargo/config.toml`;新增或调整展示命令时,应先更新 `demos.md`,并且只在 demo 已实现、可运行后再加入 Cargo alias。 -Phase 2e 已补齐 context、truncation、schema、context-visual、attachment,并增强 `demo-live` 的 provider summary 展示。它们仍应默认 ignored,不进入普通 CI 自动执行,作为人工观察和阶段合并前验收入口。 +P2-5 已补齐 context、truncation、schema、context-visual、attachment,并增强 `demo-live` 的 provider summary 展示。它们仍应默认 ignored,不进入普通 CI 自动执行,作为人工观察和阶段合并前验收入口。 diff --git a/docs/vscode-extension.md b/docs/vscode-extension.md index a1472b5..1994776 100644 --- a/docs/vscode-extension.md +++ b/docs/vscode-extension.md @@ -1,6 +1,6 @@ # 编辑器插件(VS Code Extension) -状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX、结构化 provider 配置错误恢复和 P5-14d Sidebar UX extension-host 回归均已实现;P5-14 持续 UX 测试与体验改进已拆分为子项,仍有未完成项。 +状态:Phase 3 VS Code 插件核心体验已完成;Phase 4 VS Code 深度集成已完成 14 项深度集成能力;Phase 5 VS Code Codex-like UX 与开发工作流进行中。基础命令、RPC server 启动监管、初始化握手、JSON-RPC request client、VS Code/protocol TypeScript 类型共享、RPC/commands 边界测试、Sidebar Chat 事件渲染、Chat 输入发送真实 turn、真实审批回传、共享 RPC 全双工事件管线、命令风险动态升级展示、Native diff editor patch 预览、Run List / resume、Context Capsule 可视化、VSIX alpha 打包、extension-host E2E、原生 `@prole` Chat Participant、简化审批 UX、自动上下文压缩、`ProleCoder` Output Channel 错误诊断、API key/model 配置、统一 redaction、Git 工作流、Sidebar 连续会话、Run 删除、折叠事件 UX、结构化 provider 配置错误恢复,以及 P5-14 真实试用回归修复包均已实现;P5-15 真实试用 UX backlog 持续收敛仍未完成。 VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server 复用 Rust Agent Core,而不是在 TypeScript 侧重新实现 agent loop、context builder、provider 调用或 tool execution。 @@ -45,9 +45,10 @@ VS Code 插件是 `ProleCoder` 的一等前端。它必须通过 JSON-RPC server - 在 Activity Bar 暴露 ProleCoder view container 和 Chat view。 - 通过 `RpcServerManager.onEvent()` 订阅 live `agent.event`。 -- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item;默认对话流只显示 `You` 用户消息、`DeepSeek` 回复和失败/取消错误,tool/provider/context/run completed 等过程事件收敛进默认折叠的 Work log,Work log 摘要在运行中只暴露当前工作状态。 +- 使用 `ChatEventTimeline` 把 `assistant.delta`、tool lifecycle、approval、context/provider 和 terminal event 转换为 timeline item;默认对话流只显示 `You` 用户消息、`DeepSeek` 回复和失败/取消错误,tool/provider/context/run completed 等过程事件收敛进默认折叠的 Work log,Work log 摘要在运行中只暴露当前工作状态;可见对话消息使用安全 DOM Markdown 渲染,覆盖标题、列表、引用、代码块、表格、链接、inline code 和 horizontal rule,Work log 和工具输出仍保留纯文本,单条消息渲染失败会回退为纯文本并写入 `Output > ProleCoder`;历史 replay 或 streaming 中的高频 `agent.event` 会合并 snapshot/submission/context 推送,避免连续 `assistant.delta` 触发重复完整 Markdown 重渲染。 - 同一 run/turn 的连续 `assistant.delta` 会合并为一条 assistant 消息,避免流式输出刷屏。 - 提供 prompt 输入、mode 选择和运行中 Cancel 按钮;通过 Webview `submitTurn` 消息调用 typed `RpcServerManager.sendTurn()`,发送时把 Problems 快照转换为 diagnostic attachments,并按协议 attachment 上限优先保留 error;如果当前已 resume/发送过 run,会复用该 `runId` 继续同一会话并由后端递增 `turn_N`;accepted 后等待同一 run 的 terminal event 收口输入状态,Cancel 会调用 typed `RpcServerManager.cancel()`。 +- Sidebar 右上角提供齿轮 Settings 入口;composer 常驻 API Key / Model 快捷入口。Enter 和 Send 按钮走同一提交路径,Shift+Enter 保留换行;发送后会先在当前对话中显示本地 pending 用户消息,等待真实 run event 覆盖。 - Run List 支持 `agent.listRuns` / `agent.resume` / `agent.deleteRun`,可回放历史 run、继续多轮会话,也可删除 inactive run。 - 失败状态会在 Sidebar Chat 中显示短消息,并把 sendTurn、Run List refresh/resume/delete/cancel、原生 `@prole` Chat Participant 和 RPC 启动/运行 warning 的完整错误写入 VS Code `Output > ProleCoder`;Sidebar Chat 还会把完整 `agent.event` payload 写入 Output 便于 debug。 @@ -153,7 +154,8 @@ Phase 5 Codex-like UX 与开发工作流清单与 `docs/phase-tasks.md` 对齐 11. P5-11:Phase 5 UX 工作流验收。 12. P5-12:Sidebar 连续会话、Run 删除与折叠事件 UX。 13. P5-13:结构化 provider 配置错误码与恢复动作。 -14. P5-14:持续 UX 测试与体验改进。 +14. P5-14:真实试用回归修复包。 +15. P5-15:真实试用 UX backlog 持续收敛。 在这些能力稳定前,不在插件侧重复实现 context builder、tool execution 或 provider 调用。 diff --git a/vscode/extension/package.json b/vscode/extension/package.json index bf10343..a85feba 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -13,17 +13,7 @@ }, "categories": ["Other"], "activationEvents": [ - "onStartupFinished", - "onCommand:prole-coder.openChat", - "onCommand:prole-coder.openSettings", - "onCommand:prole-coder.configureDeepSeekApiKey", - "onCommand:prole-coder.clearDeepSeekApiKey", - "onCommand:prole-coder.showProviderStatus", - "onCommand:prole-coder.selectDeepSeekModel", - "onCommand:prole-coder.generateCommitMessage", - "onCommand:prole-coder.generatePrDescription", - "onView:prole-coder.chat", - "onChatParticipant:prole-coder.chatParticipant" + "onStartupFinished" ], "main": "./out/extension.js", "contributes": { diff --git a/vscode/extension/src/chatView.ts b/vscode/extension/src/chatView.ts index 83d435a..d28d05c 100644 --- a/vscode/extension/src/chatView.ts +++ b/vscode/extension/src/chatView.ts @@ -62,6 +62,7 @@ import { import { safeScriptJson } from "./webviewSerialization"; export const CHAT_VIEW_ID = "prole-coder.chat"; +const WEBVIEW_EVENT_POST_DEBOUNCE_MS = 16; export interface ChatRpcEventSource { onEvent(handler: (event: AgentEventEnvelope) => void): DisposableLike; @@ -130,6 +131,16 @@ interface TestProbeResultWebviewMessage { readonly error?: string; } +interface WebviewErrorMessage { + readonly type: "webviewError"; + readonly message: string; + readonly stack?: string; +} + +interface WebviewReadyMessage { + readonly type: "webviewReady"; +} + type ChatSubmissionStatus = "idle" | "sending" | "running" | "completed" | "failed" | "canceled"; type TerminalSubmissionStatus = Extract; @@ -175,6 +186,12 @@ interface PendingTestProbe { reject(error: Error): void; } +interface PendingWebviewReady { + readonly timeout: ReturnType; + resolve(): void; + reject(error: Error): void; +} + export class ProleChatViewProvider implements vscode.WebviewViewProvider, DisposableLike { private readonly timeline = new ChatEventTimeline(); private readonly terminalRuns = new Map(); @@ -185,9 +202,16 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos private activeConversationRunId: string | undefined; private pendingApproval: PendingApprovalState | undefined; private readonly pendingTestProbes = new Map(); + private readonly webviewReadyWaiters = new Set(); private rpcSubscription: DisposableLike | undefined; private viewMessageSubscription: DisposableLike | undefined; private view: vscode.WebviewView | undefined; + private webviewReady = false; + private webviewGeneration = 0; + private webviewPostFlushTimer: ReturnType | undefined; + private snapshotPostQueued = false; + private submissionPostQueued = false; + private contextPostQueued = false; constructor( private readonly extensionUri: vscode.Uri, @@ -208,11 +232,11 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos if (terminal) { this.rejectPendingApproval("run ended before approval was resolved"); } - this.postSnapshot(); + this.queueSnapshotPost(); if (contextViz !== undefined) { - this.postContext(); + this.queueContextPost(); } - this.postSubmission(); + this.queueSubmissionPost(); if (terminal && this.view !== undefined) { void this.refreshRuns("Refreshing runs..."); } @@ -221,6 +245,8 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos resolveWebviewView(webviewView: vscode.WebviewView): void { this.view = webviewView; + this.webviewReady = false; + const webviewGeneration = (this.webviewGeneration += 1); webviewView.webview.options = { enableScripts: true, localResourceRoots: [this.extensionUri], @@ -243,6 +269,17 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos this.postContext(); this.postApproval(); void this.refreshRuns(); + setTimeout(() => { + if ( + this.view === webviewView && + this.webviewGeneration === webviewGeneration && + this.webviewReady !== true + ) { + this.logger?.error( + "Sidebar webview did not report ready within 3000ms; scripts may be blocked or the webview may need reload.", + ); + } + }, 3000); } openChatView(): Thenable { @@ -269,6 +306,9 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos throw new Error("chat webview is not available"); } + await this.waitForWebviewReady(); + this.flushQueuedWebviewPosts(); + const id = randomUUID(); const message: TestProbeWebviewMessage = { type: "testProbe", @@ -297,6 +337,8 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos dispose(): void { this.rejectPendingApproval("approval view disposed"); this.rejectPendingTestProbes("chat view disposed"); + this.rejectPendingWebviewReady("chat view disposed"); + this.clearQueuedWebviewPosts(); this.rpcSubscription?.dispose(); this.viewMessageSubscription?.dispose(); this.rpcSubscription = undefined; @@ -323,6 +365,19 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos return; } + const webviewError = webviewErrorFromMessage(message); + if (webviewError !== undefined) { + const stack = webviewError.stack === undefined ? "" : `\n${webviewError.stack}`; + this.logger?.error(this.redact(`Sidebar webview error: ${webviewError.message}${stack}`)); + return; + } + + if (isWebviewReadyMessage(message)) { + this.logger?.info("Sidebar webview ready."); + this.markWebviewReady(); + return; + } + if (isRefreshRunsMessage(message)) { await this.refreshRuns(); return; @@ -468,7 +523,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos type: "snapshot", snapshot: this.timeline.snapshot(), }; - void this.view?.webview.postMessage(message); + this.postToWebview(message); } private postSubmission(): void { @@ -476,7 +531,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos type: "submission", submission: this.submission, }; - void this.view?.webview.postMessage(message); + this.postToWebview(message); } private postRuns(): void { @@ -484,7 +539,7 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos type: "runs", runs: this.runList, }; - void this.view?.webview.postMessage(message); + this.postToWebview(message); } private postContext(): void { @@ -492,7 +547,66 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos type: "context", context: this.contextViz, }; - void this.view?.webview.postMessage(message); + this.postToWebview(message); + } + + private queueSnapshotPost(): void { + this.snapshotPostQueued = true; + this.scheduleWebviewPostFlush(); + } + + private queueSubmissionPost(): void { + this.submissionPostQueued = true; + this.scheduleWebviewPostFlush(); + } + + private queueContextPost(): void { + this.contextPostQueued = true; + this.scheduleWebviewPostFlush(); + } + + private scheduleWebviewPostFlush(): void { + if (this.webviewPostFlushTimer !== undefined) { + return; + } + + this.webviewPostFlushTimer = setTimeout(() => { + this.flushQueuedWebviewPosts(); + }, WEBVIEW_EVENT_POST_DEBOUNCE_MS); + } + + private flushQueuedWebviewPosts(): void { + if (this.webviewPostFlushTimer !== undefined) { + clearTimeout(this.webviewPostFlushTimer); + this.webviewPostFlushTimer = undefined; + } + + const postSnapshot = this.snapshotPostQueued; + const postSubmission = this.submissionPostQueued; + const postContext = this.contextPostQueued; + this.snapshotPostQueued = false; + this.submissionPostQueued = false; + this.contextPostQueued = false; + + if (postSnapshot) { + this.postSnapshot(); + } + if (postSubmission) { + this.postSubmission(); + } + if (postContext) { + this.postContext(); + } + } + + private clearQueuedWebviewPosts(): void { + if (this.webviewPostFlushTimer !== undefined) { + clearTimeout(this.webviewPostFlushTimer); + this.webviewPostFlushTimer = undefined; + } + this.snapshotPostQueued = false; + this.submissionPostQueued = false; + this.contextPostQueued = false; } private postApproval(): void { @@ -501,7 +615,33 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos type: "approval", ...(approval === undefined ? {} : { approval }), }; - void this.view?.webview.postMessage(message); + this.postToWebview(message); + } + + private postToWebview(message: ExtensionToWebviewMessage): void { + if (this.view === undefined || !this.webviewReady) { + return; + } + void this.view.webview.postMessage(message); + } + + private markWebviewReady(): void { + if (this.webviewReady) { + return; + } + + this.webviewReady = true; + for (const pending of this.webviewReadyWaiters) { + clearTimeout(pending.timeout); + pending.resolve(); + } + this.webviewReadyWaiters.clear(); + + this.postSnapshot(); + this.postSubmission(); + this.postRuns(); + this.postContext(); + this.postApproval(); } private setSubmission(submission: ChatSubmissionSnapshot): void { @@ -781,6 +921,33 @@ export class ProleChatViewProvider implements vscode.WebviewViewProvider, Dispos } } + private waitForWebviewReady(): Promise { + if (this.webviewReady) { + return Promise.resolve(); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + this.webviewReadyWaiters.delete(pending); + reject(new Error("timed out waiting for chat webview ready")); + }, 5000); + const pending: PendingWebviewReady = { + timeout, + resolve, + reject, + }; + this.webviewReadyWaiters.add(pending); + }); + } + + private rejectPendingWebviewReady(reason: string): void { + for (const pending of this.webviewReadyWaiters) { + clearTimeout(pending.timeout); + pending.reject(new Error(reason)); + this.webviewReadyWaiters.delete(pending); + } + } + private redact(message: string): string { return this.redactor?.redact(message) ?? message; } @@ -816,7 +983,7 @@ function renderChatViewHtml( - +