Files
claude-code/docs/superpowers/plans/2026-04-07-vscode-ide-bridge.md
2026-04-09 01:26:18 +08:00

665 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VSCode IDE Bridge Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 为当前 CLI 增加一个可运行的 VSCode `ws-ide` 扩展端实现,让 `/ide`、选区上下文注入和 IDE diff 预览在本地 VSCode 中可用。
**Architecture:** 在仓库中新增独立的 VSCode 扩展包,扩展在本地启动 WebSocket IDE Bridge并通过 lockfile 让 CLI 自动发现。扩展在该连接上暴露一个 MCP Server负责发送 `selection_changed` / `ide_connected` 通知,并实现 `openDiff``close_tab``closeAllDiffTabs` 这几个 CLI 已使用的 MCP tools。
**Tech Stack:** TypeScript、VSCode Extension API、WebSocket、`@modelcontextprotocol/sdk`、Node.js 文件系统 API
> 说明:执行前已校正协议边界。这里的 `openDiff` / `close_tab` / `closeAllDiffTabs` 不是自定义裸 WebSocket RPC而是通过 MCP tool 调用完成;`selection_changed` / `ide_connected` 才是扩展主动发往 CLI 的通知。
---
### Task 1: 脚手架 VSCode 扩展包
**Files:**
- Create: `packages/vscode-ide-bridge/package.json`
- Create: `packages/vscode-ide-bridge/tsconfig.json`
- Create: `packages/vscode-ide-bridge/src/extension.ts`
- Modify: `package.json`
- [ ] **Step 1: 写出失败测试或校验入口约束**
使用最小结构校验,确保新包会被 workspace 识别并且扩展入口文件存在。
```ts
import { describe, expect, test } from "bun:test";
import pkg from "../../vscode-ide-bridge/package.json";
describe("vscode-ide-bridge package", () => {
test("declares a VSCode extension entry", () => {
expect(pkg.main).toBe("./dist/extension.js");
expect(pkg.engines.vscode).toBeDefined();
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/package.test.ts`
Expected: FAIL提示包文件不存在或字段缺失
- [ ] **Step 3: 写最小扩展包结构**
`packages/vscode-ide-bridge/package.json`
```json
{
"name": "vscode-ide-bridge",
"private": true,
"version": "0.0.1",
"type": "module",
"main": "./dist/extension.js",
"engines": {
"vscode": "^1.90.0"
},
"activationEvents": [
"onStartupFinished",
"onCommand:claudeCodeBridge.restart",
"onCommand:claudeCodeBridge.showStatus"
],
"contributes": {
"commands": [
{
"command": "claudeCodeBridge.restart",
"title": "Claude Code Bridge: Restart"
},
{
"command": "claudeCodeBridge.showStatus",
"title": "Claude Code Bridge: Show Status"
}
]
}
}
```
`packages/vscode-ide-bridge/tsconfig.json`
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node", "vscode"]
},
"include": ["src/**/*.ts"]
}
```
`packages/vscode-ide-bridge/src/extension.ts`
```ts
import * as vscode from "vscode";
export async function activate(context: vscode.ExtensionContext): Promise<void> {
context.subscriptions.push(
vscode.commands.registerCommand("claudeCodeBridge.restart", () => {}),
vscode.commands.registerCommand("claudeCodeBridge.showStatus", () => {})
);
}
export async function deactivate(): Promise<void> {}
```
根目录 `package.json` workspace 增加:
```json
{
"workspaces": [
"packages/*",
"packages/@ant/*",
"packages/vscode-ide-bridge"
]
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bun test packages/vscode-ide-bridge/test/package.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add package.json packages/vscode-ide-bridge/package.json packages/vscode-ide-bridge/tsconfig.json packages/vscode-ide-bridge/src/extension.ts packages/vscode-ide-bridge/test/package.test.ts
git commit -m "feat: scaffold vscode ide bridge extension"
```
### Task 2: 实现 lockfile 与状态模型
**Files:**
- Create: `packages/vscode-ide-bridge/src/server/lockfile.ts`
- Create: `packages/vscode-ide-bridge/src/server/workspaceInfo.ts`
- Create: `packages/vscode-ide-bridge/src/server/protocol.ts`
- Create: `packages/vscode-ide-bridge/test/lockfile.test.ts`
- [ ] **Step 1: 写失败测试**
```ts
import { describe, expect, test } from "bun:test";
import { buildLockfilePayload } from "../src/server/lockfile";
describe("buildLockfilePayload", () => {
test("includes ws transport, auth token and workspace folders", () => {
const payload = buildLockfilePayload({
port: 8123,
pid: 100,
ideName: "VS Code",
workspaceFolders: ["D:/repo"],
authToken: "token-1",
runningInWindows: true
});
expect(payload.transport).toBe("ws");
expect(payload.authToken).toBe("token-1");
expect(payload.workspaceFolders).toEqual(["D:/repo"]);
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/lockfile.test.ts`
Expected: FAIL提示模块不存在
- [ ] **Step 3: 写最小实现**
`packages/vscode-ide-bridge/src/server/protocol.ts`
```ts
export type LockfilePayload = {
workspaceFolders: string[];
pid: number;
ideName: string;
transport: "ws";
runningInWindows: boolean;
authToken: string;
};
```
`packages/vscode-ide-bridge/src/server/lockfile.ts`
```ts
import { mkdir, rm, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import type { LockfilePayload } from "./protocol";
export function buildLockfilePayload(input: {
port: number;
pid: number;
ideName: string;
workspaceFolders: string[];
authToken: string;
runningInWindows: boolean;
}): LockfilePayload {
return {
workspaceFolders: input.workspaceFolders,
pid: input.pid,
ideName: input.ideName,
transport: "ws",
runningInWindows: input.runningInWindows,
authToken: input.authToken
};
}
export function getLockfilePath(port: number): string {
return join(homedir(), ".claude", "ide", `${port}.lock`);
}
export async function writeLockfile(port: number, payload: LockfilePayload): Promise<string> {
const path = getLockfilePath(port);
await mkdir(join(homedir(), ".claude", "ide"), { recursive: true });
await writeFile(path, JSON.stringify(payload), "utf8");
return path;
}
export async function removeLockfile(path: string | null): Promise<void> {
if (!path) return;
await rm(path, { force: true });
}
```
`packages/vscode-ide-bridge/src/server/workspaceInfo.ts`
```ts
import * as vscode from "vscode";
export function getWorkspaceFolders(): string[] {
return (vscode.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath);
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bun test packages/vscode-ide-bridge/test/lockfile.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add packages/vscode-ide-bridge/src/server/protocol.ts packages/vscode-ide-bridge/src/server/lockfile.ts packages/vscode-ide-bridge/src/server/workspaceInfo.ts packages/vscode-ide-bridge/test/lockfile.test.ts
git commit -m "feat: add vscode ide bridge lockfile support"
```
### Task 3: 实现选区发布链路
**Files:**
- Create: `packages/vscode-ide-bridge/src/server/selectionPublisher.ts`
- Create: `packages/vscode-ide-bridge/test/selectionPublisher.test.ts`
- Modify: `packages/vscode-ide-bridge/src/extension.ts`
- [ ] **Step 1: 写失败测试**
```ts
import { describe, expect, test } from "bun:test";
import { buildSelectionChangedParams } from "../src/server/selectionPublisher";
describe("buildSelectionChangedParams", () => {
test("serializes editor selection and text", () => {
const params = buildSelectionChangedParams({
filePath: "D:/repo/src/app.ts",
text: "const x = 1;",
start: { line: 1, character: 0 },
end: { line: 1, character: 12 }
});
expect(params.filePath).toBe("D:/repo/src/app.ts");
expect(params.text).toBe("const x = 1;");
expect(params.selection?.start.line).toBe(1);
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/selectionPublisher.test.ts`
Expected: FAIL提示导出不存在
- [ ] **Step 3: 写最小实现**
`packages/vscode-ide-bridge/src/server/selectionPublisher.ts`
```ts
export type SelectionPoint = {
line: number;
character: number;
};
export type SelectionChangedParams = {
selection: {
start: SelectionPoint;
end: SelectionPoint;
} | null;
text?: string;
filePath?: string;
};
export function buildSelectionChangedParams(input: {
filePath?: string;
text?: string;
start?: SelectionPoint;
end?: SelectionPoint;
}): SelectionChangedParams {
if (!input.start || !input.end) {
return {
selection: null,
text: input.text,
filePath: input.filePath
};
}
return {
selection: {
start: input.start,
end: input.end
},
text: input.text,
filePath: input.filePath
};
}
```
`packages/vscode-ide-bridge/src/extension.ts` 先增加一个占位发布调用:
```ts
import * as vscode from "vscode";
import { buildSelectionChangedParams } from "./server/selectionPublisher";
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const disposable = vscode.window.onDidChangeTextEditorSelection(event => {
const editor = event.textEditor;
const selection = editor.selection;
buildSelectionChangedParams({
filePath: editor.document.uri.fsPath,
text: editor.document.getText(selection),
start: {
line: selection.start.line,
character: selection.start.character
},
end: {
line: selection.end.line,
character: selection.end.character
}
});
});
context.subscriptions.push(
disposable,
vscode.commands.registerCommand("claudeCodeBridge.restart", () => {}),
vscode.commands.registerCommand("claudeCodeBridge.showStatus", () => {})
);
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bun test packages/vscode-ide-bridge/test/selectionPublisher.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add packages/vscode-ide-bridge/src/server/selectionPublisher.ts packages/vscode-ide-bridge/test/selectionPublisher.test.ts packages/vscode-ide-bridge/src/extension.ts
git commit -m "feat: add vscode selection publisher primitives"
```
### Task 4: 实现 WebSocket bridge server 与鉴权
**Files:**
- Create: `packages/vscode-ide-bridge/src/server/bridgeServer.ts`
- Create: `packages/vscode-ide-bridge/test/bridgeServer.test.ts`
- Modify: `packages/vscode-ide-bridge/src/extension.ts`
- [ ] **Step 1: 写失败测试**
```ts
import { describe, expect, test } from "bun:test";
import { isAuthorizedUpgrade } from "../src/server/bridgeServer";
describe("isAuthorizedUpgrade", () => {
test("accepts matching token", () => {
expect(isAuthorizedUpgrade("abc", "abc")).toBe(true);
});
test("rejects mismatched token", () => {
expect(isAuthorizedUpgrade("abc", "def")).toBe(false);
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/bridgeServer.test.ts`
Expected: FAIL提示模块不存在
- [ ] **Step 3: 写最小实现**
`packages/vscode-ide-bridge/src/server/bridgeServer.ts`
```ts
import { WebSocketServer } from "ws";
export function isAuthorizedUpgrade(expected: string, actual: string | undefined): boolean {
return Boolean(actual) && expected === actual;
}
export class BridgeServer {
private server: WebSocketServer | null = null;
constructor(private readonly authToken: string) {}
async start(port: number): Promise<void> {
this.server = new WebSocketServer({
host: "127.0.0.1",
port
});
}
async stop(): Promise<void> {
await new Promise<void>(resolve => {
if (!this.server) return resolve();
this.server.close(() => resolve());
this.server = null;
});
}
}
```
`packages/vscode-ide-bridge/src/extension.ts` 中接入:
```ts
import * as vscode from "vscode";
import { randomUUID } from "node:crypto";
import { BridgeServer } from "./server/bridgeServer";
let bridgeServer: BridgeServer | null = null;
export async function activate(context: vscode.ExtensionContext): Promise<void> {
bridgeServer = new BridgeServer(randomUUID());
await bridgeServer.start(0);
context.subscriptions.push({
dispose() {
void bridgeServer?.stop();
}
});
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bun test packages/vscode-ide-bridge/test/bridgeServer.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add packages/vscode-ide-bridge/src/server/bridgeServer.ts packages/vscode-ide-bridge/test/bridgeServer.test.ts packages/vscode-ide-bridge/src/extension.ts
git commit -m "feat: add vscode ide bridge websocket server"
```
### Task 5: 实现 diff RPC 和状态命令
**Files:**
- Create: `packages/vscode-ide-bridge/src/server/diffController.ts`
- Modify: `packages/vscode-ide-bridge/src/extension.ts`
- Create: `packages/vscode-ide-bridge/test/diffController.test.ts`
- [ ] **Step 1: 写失败测试**
```ts
import { describe, expect, test } from "bun:test";
import { DiffSessionStore } from "../src/server/diffController";
describe("DiffSessionStore", () => {
test("stores and removes tab mappings by tab name", () => {
const store = new DiffSessionStore();
store.set("tab-1", "memfs:/right.ts");
expect(store.get("tab-1")).toBe("memfs:/right.ts");
store.delete("tab-1");
expect(store.get("tab-1")).toBeUndefined();
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/diffController.test.ts`
Expected: FAIL提示模块不存在
- [ ] **Step 3: 写最小实现**
`packages/vscode-ide-bridge/src/server/diffController.ts`
```ts
export class DiffSessionStore {
private readonly sessions = new Map<string, string>();
set(tabName: string, uri: string): void {
this.sessions.set(tabName, uri);
}
get(tabName: string): string | undefined {
return this.sessions.get(tabName);
}
delete(tabName: string): void {
this.sessions.delete(tabName);
}
clear(): void {
this.sessions.clear();
}
}
```
`packages/vscode-ide-bridge/src/extension.ts` 增加状态命令:
```ts
import * as vscode from "vscode";
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const output = vscode.window.createOutputChannel("Claude Code IDE Bridge");
context.subscriptions.push(
output,
vscode.commands.registerCommand("claudeCodeBridge.showStatus", async () => {
output.appendLine("Claude Code IDE Bridge is running.");
output.show(true);
})
);
}
```
- [ ] **Step 4: 运行测试确认通过**
Run: `bun test packages/vscode-ide-bridge/test/diffController.test.ts`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add packages/vscode-ide-bridge/src/server/diffController.ts packages/vscode-ide-bridge/test/diffController.test.ts packages/vscode-ide-bridge/src/extension.ts
git commit -m "feat: add vscode ide bridge diff state and status command"
```
### Task 6: 接通完整激活流程与手工验证说明
**Files:**
- Modify: `packages/vscode-ide-bridge/src/extension.ts`
- Modify: `README.md`
- Modify: `README_EN.md`
- [ ] **Step 1: 写失败校验**
用文档断言确保 README 中包含 bridge 启动与 `/ide` 使用说明。
```ts
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
describe("README bridge docs", () => {
test("documents vscode ide bridge usage", () => {
const readme = readFileSync("README.md", "utf8");
expect(readme.includes("VSCode IDE Bridge")).toBe(true);
expect(readme.includes("/ide")).toBe(true);
});
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run: `bun test packages/vscode-ide-bridge/test/readme.test.ts`
Expected: FAIL提示 README 中没有 bridge 文档
- [ ] **Step 3: 实现激活主流程与文档**
`packages/vscode-ide-bridge/src/extension.ts` 最终需要做到:
```ts
import * as vscode from "vscode";
import { randomUUID } from "node:crypto";
import { writeLockfile, removeLockfile, buildLockfilePayload } from "./server/lockfile";
import { getWorkspaceFolders } from "./server/workspaceInfo";
import { BridgeServer } from "./server/bridgeServer";
let lockfilePath: string | null = null;
let bridgeServer: BridgeServer | null = null;
export async function activate(context: vscode.ExtensionContext): Promise<void> {
const authToken = randomUUID();
const output = vscode.window.createOutputChannel("Claude Code IDE Bridge");
bridgeServer = new BridgeServer(authToken);
await bridgeServer.start(0);
const payload = buildLockfilePayload({
port: 0,
pid: process.pid,
ideName: "VS Code",
workspaceFolders: getWorkspaceFolders(),
authToken,
runningInWindows: process.platform === "win32"
});
lockfilePath = await writeLockfile(0, payload);
output.appendLine(`Bridge started. Lockfile: ${lockfilePath}`);
context.subscriptions.push(output, {
dispose() {
void bridgeServer?.stop();
void removeLockfile(lockfilePath);
}
});
}
export async function deactivate(): Promise<void> {
await bridgeServer?.stop();
await removeLockfile(lockfilePath);
}
```
README 中文和英文各补一个简短章节,说明:
- 扩展启动后会暴露本地 bridge
- 启动 CLI 后执行 `/ide`
- 在 VSCode 里选中代码,再向 CLI 提问
- diff 预览由 CLI 主动触发
- [ ] **Step 4: 运行验证**
Run: `bun test packages/vscode-ide-bridge/test/readme.test.ts`
Expected: PASS
Run: `bun test packages/vscode-ide-bridge/test/*.test.ts`
Expected: PASS
手工验证:
Run: `bun run build.ts`
Expected: 构建完成,无本次改动引入的额外错误
手工步骤:
1. 在 VSCode 启动扩展开发宿主
2. 打开本仓库
3. 启动 CLI
4. 执行 `/ide`
5. 在编辑器中选中文本后提问
6. 验证 CLI 可见 IDE 选区上下文
- [ ] **Step 5: Commit**
```bash
git add packages/vscode-ide-bridge/src/extension.ts README.md README_EN.md packages/vscode-ide-bridge/test/readme.test.ts
git commit -m "feat: wire vscode ide bridge activation and docs"
```