CodeWhale VSCode 是一个 VSCode 扩展,为 CodeWhale TUI 提供图形界面。该扩展通过 WebView 与用户交互,连接到 CodeWhale TUI 引擎进行 AI 对话。
- Node.js 18+
- npm 或 yarn
- VSCode 或兼容的 IDE(如 Trae CN)
npm installnpm run compile- 使用 webpack 编译开发版本
- 输出文件:
dist/extension.js - 包含 source map,便于调试
npm run package- 使用 webpack 编译生产版本
- 代码压缩优化
- 输出文件更小(约 168KB)
npm test- 使用 vitest 运行单元测试
- 测试文件位于
src/*.test.ts
npx @vscode/vsce package --no-dependencies- 生成
brotherwhale-vscode-0.1.0.vsix文件 - 包含所有必要的源文件和资源
- 文件大小约 180KB
code --install-extension ./brotherwhale-vscode-0.1.0.vsix --force使用 Trae CN 时如果
code不可用,先设置 alias:alias code="/Applications/Trae CN.app/Contents/Resources/app/bin/code"
npm run compile && \
npx @vscode/vsce package --no-dependencies && \
code --install-extension ./brotherwhale-vscode-0.1.0.vsix --force安装完成后,重新加载窗口:
- 按
Cmd+Shift+P打开命令面板 - 输入 "Reload Window" 并执行
DeepSeek-GUI/
├── src/
│ ├── extension.ts # 扩展入口点
│ ├── chat-provider.ts # 主聊天界面逻辑
│ ├── webview-html.ts # WebView HTML 模板
│ ├── api-client.ts # CodeWhale API 客户端
│ ├── engine.ts # TUI 引擎管理
│ ├── i18n.ts # 国际化支持
│ ├── slash-commands.ts # 斜杠命令定义
│ └── *.test.ts # 单元测试
├── dist/ # 编译输出
├── media/ # 图标等资源
├── package.json # 扩展配置
└── webpack.config.js # Webpack 配置
chat-provider.ts管理与 WebView 的双向通信- 消息类型:
sendMessage,slashCommand,loadThread,interrupt等 - 使用
postMessageAPI 发送消息
slash-commands.ts定义可用命令及其可用性handleSlashCommand()处理命令逻辑- 命令格式:
command+args(例如:/task+show task_id)
- 任务列表显示在侧边栏的 "Tasks" 标签页
- 支持创建、查看、取消任务
- 点击任务卡片触发
/task show <id>命令
- 侧边栏包含三个标签页:Threads、Work、Tasks
- 打开后保持打开状态,除非用户明确关闭
- 点击线程项不会自动关闭侧边栏
原因:消息格式错误,将整个命令字符串作为 command 参数发送
修复:
// 错误格式
vscode.postMessage({ type: 'slashCommand', command: '/task show ' + taskId });
// 正确格式
vscode.postMessage({ type: 'slashCommand', command: '/task', args: 'show ' + taskId });原因:点击线程项时自动移除 open 类
修复:移除自动关闭代码
// 移除这行
threadsPanel.classList.remove('open');- 使用 TypeScript 严格模式
- 遵循 ESLint 规则
- 函数和变量使用 camelCase
- 类和接口使用 PascalCase
- 使用
console.log输出到 VSCode 开发者工具(Help → Toggle Developer Tools) - WebView 中的
console.log输出到浏览器控制台 - 使用
postMessage记录消息流
- WebView HTML 模板使用模板字符串,避免频繁 DOM 操作
- 使用事件委托处理列表项点击
- 避免在渲染函数中创建重复的事件监听器
- 更新
package.json中的版本号 - 运行测试:
npm test - 编译生产版本:
npm run package - 打包 VSIX:
npx @vscode/vsce package --no-dependencies - 测试安装:安装到本地 IDE 验证功能
- 发布到 VSCode Marketplace(如果需要)
- CodeWhale TUI: TUI 引擎,提供 API 服务
- DeepSeek API: DeepSeek AI API 接口
- 项目仓库:https://github.com/HengQuWorld/CodeWhale-VSCode
- 问题反馈:通过 GitHub Issues
现象:在
webview-html.ts的<script>块中添加了新函数updateUndoRetryState()引用了messages变量,但原始代码中只有messagesEl(DOM 元素引用),未声明过messages数组变量。新代码在 script 块靠前位置执行 → 抛出ReferenceError: messages is not defined→ 整个 script 块停止执行 → 后续所有初始化(status 更新、消息订阅、按钮事件绑定)全部死锁 → UI 永远停在 "Initializing..."。
-
大型 inline
<script>块是一荣俱荣、一损俱损的整体- VSCode WebView 的
webview.html是单文件模板,里面常常塞一个 80~200 KB 的<script>块 - 该块中任何位置的运行时错误(
ReferenceError/TypeError/ 语法错误)都会让整个块停止执行 - 后果:用户看到的不是"某个按钮不工作",而是整个 webview 看起来死了(Initializing 永远不消失、状态栏不更新、按钮全无响应)
- VSCode WebView 的
-
不要假设前端存在某个变量
- 大型 webview 脚本里有大量闭包、模块模式、IIFE,变量要么在
window上、要么闭包私有 - 修改前必须先 grep 确认变量是否被声明:
grep -n "let X\|var X\|const X" webview-html.ts - 如果找不到 → 不要直接用。要么走 postMessage 让后端判断,要么自己维护一个 IIFE 局部变量
- 大型 webview 脚本里有大量闭包、模块模式、IIFE,变量要么在
-
修改 webview 前端的最安全姿势
- 优先后端:按钮 →
vscode.postMessage({ type: 'xxx' })→ 后端判断 + 状态机 - 不依赖前端变量:所有判断逻辑放在后端
chat-provider.ts,前端只负责"显示/发消息" - 新逻辑放 IIFE 隔离:用
(function(){ ... })()包起来,自己声明局部变量,绝不污染外层作用域 - 绑定 handler 用 try/catch 包装:单个按钮的 handler 出错不应该影响其他按钮
- 事件委托复用现有模式:新按钮的 click 处理插到原有
addEventListener('click', ...)内部,跟着target.classList.contains('xxx')走
- 优先后端:按钮 →
-
debug 大型 webview 卡死的二分定位法
- 第一步:
git stash全部本地修改 → 打包测试- 正常 → 我的修改是问题源
- 还是卡 → 是环境问题(不是代码)
- 第二步:
git checkout逐个文件回退,找出哪个文件的修改导致问题 - 第三步:定位具体行时,从最简化的修改开始(只加 1 行 HTML / 1 个 postMessage)→ 测试 → 逐步加代码 → 找到出错的那行
- 不要用
console.log散弹枪调试大型 webview 块——错误抛出后后续所有console.log也不会执行,所以看到的"没日志"不代表"没出错",而可能脚本已经死了
- 第一步:
-
绝对不要把 HTML 调试标记 + 状态文字改动直接 commit
- 调试时的 BOOT marker、CSP 临时移除、
_debugMode = true、写日志到磁盘等都是一次性探针 - 找到根因后立即 revert 这些探针代码
- 探针代码混在生产代码里 → 下次再调试时浪费更多时间判断"这些是做什么的"
- 调试时的 BOOT marker、CSP 临时移除、
-
CSP / nonce / script 块语法问题的早期信号
- JS 完全不执行 + Console 完全没日志 → 整段 script 被浏览器拒绝(CSP 阻止、语法错误导致整个块拒绝解析、nonce 不匹配)
- JS 执行了一段然后挂 →
ReferenceError/TypeError/ 无限循环 - UI 部分渲染(HTML)但交互失效 → 事件 handler 报错或没绑上
- 已用
grep确认所有引用的变量在 script 块中存在 - 所有新 UI 逻辑尽量放后端
- 新代码用 IIFE 隔离,不污染外层
- 调试探针代码标记为
// DEBUG:,调试完立即删除 - 至少在 IDE 重新加载一次 webview 验证状态从 "Initializing" 变 "Ready"
- 修改量 > 50 行时,分批 commit,便于
git bisect定位问题
GUI 是 TUI 的图形前端,用户在两种界面下的操作应该产生相同的效果。实现新功能前,先读 TUI 源码理解其设计意图,再决定 GUI 的实现方式。
- 优先使用 TUI 已有的 Runtime API — GUI 通过 HTTP 调用 TUI 的
/v1/*端点 - 如果 TUI 有功能但缺少 API 端点 — 先在 TUI 的
runtime_api.rs中添加端点,再在 GUI 实现 - 如果 TUI 也没有该功能 — 先在 TUI 设计并实现,暴露 API,再在 GUI 对接
绝不在 GUI 中用 hack/变通方式模拟一个 TUI 已有但 GUI 没有对接的功能。
| 功能 | TUI 实现 | GUI 当前实现 | 差距 |
|---|---|---|---|
/undo |
从 app.history + app.api_messages 双向删除,清空 tool_cells |
从 this.messages 删除并 loadHistory 重渲染 |
GUI 只删显示层,不删 API 侧消息;undo 后再发消息会带上旧上下文 |
/retry |
undo_conversation() + SendMessage(input) |
handleUndoLastTurn() + handleSendMessage() |
同上,undo 不彻底 |
/patch_undo |
通过 SnapshotRepo 恢复 pre-turn:* 快照,支持多次回退 |
用 parseDiffToSides 的 oldContent + WorkspaceEdit 回滚 |
GUI 方式不可靠:diff 可能不完整、不支持多次回退 |
revert_turn 工具 |
AI 主动调用的 revert_turn 工具,基于快照回滚 |
GUI 的 revert 按钮用 diff 回滚 | 应该对接 TUI 的快照机制 |
以下端点目前在 TUI Runtime API 中不存在,需要添加后 GUI 才能正确实现:
POST /v1/threads/{id}/undo— 服务端删除最后一轮的 API 消息 + 历史,返回被删除的内容摘要POST /v1/threads/{id}/retry— 服务端执行 undo + 重发最后一条用户消息GET /v1/snapshots— 列出当前 workspace 的快照(对应 TUI 的SnapshotRepo.list())POST /v1/snapshots/{id}/restore— 恢复指定快照(对应 TUI 的patch_undo/restore命令)
| 功能 | 文件 | 函数/结构 |
|---|---|---|
| undo 对话 | crates/tui/src/commands/debug.rs |
undo_conversation() |
| retry 重试 | crates/tui/src/commands/debug.rs |
retry() |
| patch_undo 文件回滚 | crates/tui/src/commands/debug.rs |
patch_undo() |
| 快照仓库 | crates/tui/src/snapshot/repo.rs |
SnapshotRepo |
| pre-turn 快照 | crates/tui/src/core/turn.rs |
pre_turn_snapshot() |
| revert_turn 工具 | crates/tui/src/tools/revert_turn.rs |
RevertTurnTool |
| Runtime API 路由 | crates/tui/src/runtime_api.rs |
build_router() |
| /restore 命令 | crates/tui/src/commands/restore.rs |
restore() |
- 在 TUI 源码中找到对应功能的实现
- 确认 TUI Runtime API 是否已暴露该功能
- 如果没有 API → 先在
runtime_api.rs添加端点 - 在 GUI 的
api-client.ts中添加调用方法 - 在
chat-provider.ts中实现业务逻辑 - 在
webview-html.ts中添加 UI(遵循 WebView 编码原则) - 对比 TUI 和 GUI 的行为是否一致
GUI 的 runtime API 端点(runtime_api.rs)必须复用 TUI 已有的核心能力,而不是自己重新实现一遍。TUI 已经有了完整的业务逻辑,API 端点只是把这些能力暴露给 GUI 调用。
错误:在 API 端点中自己重建数据
// ❌ 自己从 turns 重建消息、估算 token
let turns = runtime_threads.list_turns_for_thread_pub(&thread_id)?;
let messages = runtime_threads.reconstruct_messages_from_turns(&turns)?;
let total_tokens = messages.iter().map(|m| text.len() as u64 / 4).sum();正确:通过 Engine 获取 TUI 已有的真实数据
// ✅ 复用 Engine 的 get_session_snapshot(),和 TUI 的 build_session_snapshot 走同一路径
let engine = runtime_threads.get_engine(&thread_id).await?;
let snapshot = engine.get_session_snapshot().await?;
// snapshot.messages / snapshot.total_tokens / snapshot.model 都是 Engine 的真实状态写 API 端点时问自己:
- TUI 内部做这件事用的是什么? → API 端点应该调用同一个东西
- 我是在"暴露 TUI 的能力"还是"重新实现 TUI 的逻辑"? → 应该是前者
- 如果 TUI 的逻辑改了,我的 API 端点会不会不同步? → 如果会,说明没有复用
-
数据来源:用 Engine 的真实状态,不要自己重建
- Engine 的
session.messages是权威消息列表,不要从 turns/items 重建 - Engine 的
session.total_usage是权威 token 统计,不要从文本长度估算 - 通过
get_session_snapshot()获取完整快照
- Engine 的
-
业务逻辑:用 TUI 已有的函数,不要重写
- 保存 session:用
create_saved_session_with_id_and_mode()/update_session(),和 TUI 的build_session_snapshot一致 - 文件操作:用 Engine 的 Op 通道,不要绕过 Engine 直接操作文件
- 对话操作(undo/retry):用 Engine 的 Op 通道,确保状态一致
- 保存 session:用
-
API 端点的角色是"薄适配层"
- 接收 HTTP 请求 → 转换参数 → 调用 TUI 已有能力 → 返回结果
- 不应该在 API 层做数据转换、计算、重建等重逻辑
- 如果发现 API 端点里有超过 20 行的业务逻辑,大概率是在重复造轮子
-
Engine 是唯一的状态权威
- Engine 持有当前 session 的完整状态(messages、tokens、model 等)
- Thread store 只持久化 turn items,不是消息的权威来源
- 从 turns 重建消息是
ensure_engine_loaded的内部实现细节,API 不应该依赖它
| 方面 | 错误方式 | 正确方式 |
|---|---|---|
| 消息来源 | 从 turns 重建 | Engine get_session_snapshot() |
| token 计算 | text.len() / 4 估算 |
Engine 的 total_usage |
| model/workspace | 从 thread 记录读取 | Engine 快照自带 |
| session 构建 | 自己拼 SavedSession |
用 create_saved_session_with_id_and_mode() |
| 保存方式 | 自己写文件 | 用 SessionManager::save_session() |