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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ pnpm run test:integration
| --- | --- |
| `pnpm run build:chat-ui` | 公共 UI 包及类型声明 |
| `pnpm run build:webview` | 独立前端资源,输出 `dist/webview-v2/` |
| `pnpm run build:extension` | 只构建 Extension Host 与 Worker,不重建页面 |
| `pnpm run build` | 扩展、Worker 与六个生产 Webview 入口,页面输出 `dist/webview/` |
| `pnpm run package:vsix` | 串行执行打包前检查与构建,生成统一 VSIX |
| `npm --prefix docs run build` | 文档静态站与站内文件链接检查 |
Expand All @@ -295,6 +296,22 @@ pnpm run test:integration
保留无关改动,不提交密钥、日志、依赖目录、`dist/` 或 VSIX。
产品事实写入 [STATUS](STATUS.md),架构和视觉约束分别更新 [ARCHITECTURE](ARCHITECTURE.md) 与 [DESIGN](DESIGN.md)。

### 日常热更新,不重装 VSIX

已安装同版本扩展时,按改动选择一个命令,同时更新 Microsoft VS Code 和 Cursor:

| 命令 | 更新范围 |
| --- | --- |
| `pnpm run sync:webview` | 公共 UI 与生产页面,正确构建到 `dist/webview/`,不复制后端 |
| `pnpm run sync:extension` | Extension Host 与 Worker,不构建或复制页面 |
| `pnpm run sync:all` | 前后端都改变时,统一构建后同步 |

同步仅更新各编辑器安装登记中的当前同版本扩展,不覆盖旧版本目录、安装清单或用户数据;
内容相同的文件不重复写入。可追加 `--dry-run` 构建并检查目标,不修改安装目录。
编辑器中现有监听会刷新页面;后端变化会重启扩展宿主,可能短暂影响其他扩展,
应在没有进行中的操作时执行。这是构建后自动刷新/宿主重启,不是保存源码即无感替换。
扩展版本、清单注册项或依赖安装布局变化仍使用 VSIX,不用热更新代替正式发布。

<a id="build-install"></a>

## 构建、打包并安装最新源码
Expand Down
7 changes: 4 additions & 3 deletions docs/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
## 版本与交付

- 当前源码版本为 `0.8.20`,在 `0.8.19` 基础上加入繁星/三体/关闭三种深色聊天背景,并修复 AskUser 多选识别、勾选状态与单选填空互斥切换。
- 正式版 `0.8.20` 以 [GitHub Release](https://github.com/fireman-droid/droid-for-vscode/releases/tag/v0.8.20) 与 [Open VSX](https://open-vsx.org/extension/droidvisx/droidvisx/0.8.20) 为准。SDK 0.10.0 与分页已通过 PR #22 合入 main(`663cbffc`);Automations、滚动修复与验证流程调整已通过 PR #23 合入 main(`5c3aaebf`)。Microsoft VS Code 和 Cursor 均已安装 `0.8.20` 试用包,尚未正式发布。当前 `codex/fix-diagnostic-findings` 的诊断修复未打包、未安装;原热更新脚本提交保留在 `codex/fix-hot-reload`。
- 正式版 `0.8.20` 以 [GitHub Release](https://github.com/fireman-droid/droid-for-vscode/releases/tag/v0.8.20) 与 [Open VSX](https://open-vsx.org/extension/droidvisx/droidvisx/0.8.20) 为准。SDK 0.10.0 与分页已通过 PR #22 合入 main;Automations、滚动修复与验证流程调整已通过 PR #23 合入 main。诊断与 Review 滚动修复已通过 PR #24 合入 main(`ca1b9bb8`),必要 CI 通过。Microsoft VS Code 和 Cursor 均已安装 `0.8.20` 试用包,PR #24 的修复尚未安装;双编辑器热更新脚本在本次集成中补齐,尚未正式发布。
- 恢复文件等 composer 回执、附件/引用提示及临时诊断继续保留在消息列表底部状态区域;「Droid 正在工作/回答」、停止中(`Stopping…`)、压缩与等待子代理等运行状态统一移入底部 Composer 输入框的「继续提问」占位位置,未输入草稿时旋转显示、输入时隐藏、结束后恢复占位文字。
- Microsoft Marketplace 管理页在 2026-10-18 仍返回 429/Azure DevOps VSID 并发限流,此前还存在 `The extension 'droidvisx' already exists in the Marketplace`、Owner 列表为空的问题;未改名或覆盖既有产物,微软自动发布保持关闭,未配置 `VSCE_PAT`。
- 本机实测 daemon 自报版本为 `1.2.0`;文档站为 https://fireman-droid.github.io/droid-for-vscode/ 。
Expand Down Expand Up @@ -43,6 +43,7 @@
同名 `PR checks` 汇总所选任务,保留失败门禁,支持仅重试失败任务;已在 PR #23 远端运行通过。
常规测试不调用模型,真实模型评估与 smoke 保持显式运行。
- 编辑器集成测试按必需激活事件逐项断言,允许增加其他合法入口,不再固定旧的三项完整清单。
- 热更新脚本已分开前端/后端/全部入口,修正前端构建与同步目录不一致,并仅同步 VS Code / Cursor 登记的当前同版本安装。3 项脚本检查、语法、类型、文件预算及前后端构建与双编辑器目标预演通过;后端单独构建未改写页面产物。复用已有页面刷新与扩展宿主重启监听,不重装 VSIX;本轮未写入安装目录,实际打开编辑器的自动刷新效果仍待确认。
- `daemon_health` 的 sidecar 启动/预热失败明确标记 `outcome=failed`,缺失原因使用 `unknown`,并保留已有 spawn/connect 阶段;不把失败默认标为 `ok`,不上传原始错误内容。

## 最近验证
Expand All @@ -53,7 +54,7 @@

- SDK 升级:`typecheck`、`lint:budgets` 与 14 个定向测试文件/570 项通过,覆盖权限、压缩/交接、历史证据、Bridge 及 daemon 连接/恢复。最终 VSIX 171 项内容/入口校验与 PR #22 的必需 CI 已通过。未运行真实模型调用、重启 daemon 或追加浏览器验收。

- 历史分页:任务分支已固定顶部加载提示的最小高度,避免出现/消失改变列表布局;导航动画期间不自动抢占分页,顶部上滑手势仍能加载历史。分页优先按已挂载消息一次对齐,加载期间继续滚动会更新阅读锚点,稳定回调依赖避免补偿被无关渲染取消。本轮 3 个文件/8 项定向检查、`typecheck`、`lint:budgets` 通过;本地试用 VSIX 已构建、通过 171 个归档条目及入口校验,并在用户退出 VS Code 后安装成功。尚未在真实页面确认闪屏消失,未正式发布。此前分页的真实 daemon 1.2.0 读取覆盖 129 MiB、121 页/12,090 条,原始 ID 和顺序一致,新增消息后旧游标续读亦一致;该历史证据不作为本轮界面验收。
- 历史分页:main 已固定顶部加载提示的最小高度,避免出现/消失改变列表布局;导航动画期间不自动抢占分页,顶部上滑手势仍能加载历史。分页优先按已挂载消息一次对齐,加载期间继续滚动会更新阅读锚点,稳定回调依赖避免补偿被无关渲染取消。本轮 3 个文件/8 项定向检查、`typecheck`、`lint:budgets` 通过;本地试用 VSIX 已构建、通过 171 个归档条目及入口校验,已安装到 VS Code 和 Cursor。尚未在真实页面确认闪屏消失,未正式发布。此前分页的真实 daemon 1.2.0 读取覆盖 129 MiB、121 页/12,090 条,原始 ID 和顺序一致,新增消息后旧游标续读亦一致;该历史证据不作为本轮界面验收。

- `0.8.20` AskUser 修复:2 个定向测试文件共 28 项、`typecheck` 与 `lint:budgets` 已通过;真实页面点击验收留给用户,未将单元测试视为编辑器验收。发布构建与渠道结果以该版本 Actions 记录为准。

Expand All @@ -71,7 +72,7 @@
同一真实会话读取 2442 条消息、保留 2000 条记录后重放:原错误命令为 failed、对应重复汇总消失,剩余 14 条有内容的 Script 均无外层 part/result 传输包装直接展示。
后续空白思考修复的 6 项定向测试通过,并在用户指定的真实页面位置核对空正文与无效箭头已移除;最终包已安装,安装后未重载编辑器验收。用户指出的 Diff 方法提取行配对已按要求推后,见 PLAN。
- 自动换行完成类型检查、文件预算、一次最终构建及 172 项 VSIX 内容/入口校验;按用户要求未追加页面验收或样式测试。
- Review 长列表:已修复 PR #24 CI 再次暴露的旧滚动复位故障;原有 2 项长列表用例、公共 UI 类型检查及文件预算通过,未放宽断言。真实页面效果尚未验收,合并仍等待修复后的必要 CI。
- Review 长列表:已修复 PR #24 CI 再次暴露的旧滚动复位故障;原有 2 项长列表用例、公共 UI 类型检查及文件预算通过,未放宽断言。PR #24 必要 CI 已通过并合入 main,真实页面效果尚未验收。
- 本轮运行状态、最新活动组合、Edit 归属、长 thinking 与聊天快照恢复的定向测试通过;
类型检查、文件预算通过。长文本测试验证原文完整、增量合并、旧文本节点复用与滚动恢复,不作为编辑器帧率结论。
未追加真实页面验收,旧卡住会话与新界面留待用户在本轮试用包中核对。
Expand Down
4 changes: 3 additions & 1 deletion esbuild.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ appendFileSync('dist/extension/THIRD_PARTY_LICENSES.txt', '\nKilo Code (vendored
appendFileSync('dist/extension/THIRD_PARTY_LICENSES.txt', '\nContinue (Apache-2.0; autocomplete code via Kilo)\n' + readFileSync('third-party/CONTINUE-LICENSE.txt', 'utf8'));
appendFileSync('dist/extension/THIRD_PARTY_LICENSES.txt', '\n' + readFileSync('third-party/TREE-SITTER-GRAMMARS-LICENSES.txt', 'utf8'));

execFileSync(process.execPath, ['scripts/buildWebviewV2.mjs', '--production'], { stdio: 'inherit' });
if (!process.argv.includes('--extension-only')) {
execFileSync(process.execPath, ['scripts/buildWebviewV2.mjs', '--production'], { stdio: 'inherit' });
}

assertExpectedExternals(extensionResult.metafile, {
required: new Set(['vscode']),
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@
"package:chat-ui": "pnpm run build:chat-ui && pnpm --dir packages/chat-ui pack --pack-destination ../../dist",
"typecheck:chat-ui": "pnpm --filter @droidvisx/chat-ui typecheck",
"build": "node esbuild.mjs",
"build:extension": "node esbuild.mjs --extension-only",
"dev:webview": "vite --config vite.webview-v2.config.ts",
"test:extension": "vitest run src/shared src/extension",
"test:webview": "vitest run src/webview-v2",
Expand All @@ -438,7 +439,9 @@
"release:assets": "node scripts/checkRelease.mjs --prepare-assets",
"vscode:prepublish": "pnpm run package:prepare",
"build:webview": "node scripts/buildWebviewV2.mjs",
"sync:webview": "pnpm run build:chat-ui && pnpm run build:webview && node scripts/syncGlobalExtensions.mjs",
"sync:webview": "pnpm run build:chat-ui && pnpm run build:webview --production && node scripts/syncGlobalExtensions.mjs --webview-only",
"sync:extension": "pnpm run build:extension && node scripts/syncGlobalExtensions.mjs --extension-only",
"sync:all": "pnpm run build:chat-ui && pnpm run build && node scripts/syncGlobalExtensions.mjs",
"benchmark:markdown": "node scripts/benchmarkMarkdown.mjs",
"benchmark:review": "node scripts/benchmarkReview.mjs",
"dev:performance": "node scripts/servePerformance.mjs"
Expand Down
6 changes: 6 additions & 0 deletions scripts/syncGlobalExtensions.d.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function syncGlobalExtensions(options?: {
repoRoot?: string;
home?: string;
mode?: 'all' | 'webview' | 'extension';
dryRun?: boolean;
}): Array<{ editor: string; directory: string; changedFiles: number }>;
99 changes: 70 additions & 29 deletions scripts/syncGlobalExtensions.mjs
Original file line number Diff line number Diff line change
@@ -1,42 +1,83 @@
import { cpSync, existsSync, readdirSync } from 'node:fs';
import { cpSync, existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
import { homedir } from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

const includeExtension = !process.argv.includes('--webview-only');
const repoRoot = process.cwd();
const webviewSrc = path.join(repoRoot, 'dist', 'webview');
const extensionSrc = path.join(repoRoot, 'dist', 'extension');
const manifestSrc = path.join(repoRoot, 'package.json');
const repository = fileURLToPath(new URL('../', import.meta.url));
const requiredAssets = {
webview: ['webview.js', 'webview.css', 'management.js', 'models.js', 'review.js', 'mission-control.js', 'session-viewer.js'],
extension: ['extension.cjs', 'sessionCatalogWorker.cjs', 'persistentIdeRelayWorker.cjs', 'btwAnalysisWorker.cjs'],
};
const json = file => JSON.parse(readFileSync(file, 'utf8'));

if (!existsSync(webviewSrc)) {
throw new Error(`Missing dist/webview directory at ${webviewSrc}. Run build:webview first.`);
function changedFiles(source, destination) {
return readdirSync(source, { withFileTypes: true }).flatMap(entry => {
const from = path.join(source, entry.name), to = path.join(destination, entry.name);
if (entry.isDirectory()) return changedFiles(from, to);
if (existsSync(to) && readFileSync(from).equals(readFileSync(to))) return [];
return [{ from, to }];
});
}

const roots = [
path.join(homedir(), '.vscode', 'extensions'),
path.join(homedir(), '.cursor', 'extensions'),
];

let syncedCount = 0;
for (const root of roots) {
if (!existsSync(root)) continue;
const entries = readdirSync(root, { withFileTypes: true })
.filter(entry => entry.isDirectory() && entry.name.startsWith('droidvisx.droidvisx-'))
.map(entry => path.join(root, entry.name));
/** Sync only registered installations; hot updates never rewrite extension manifests. */
export function syncGlobalExtensions({ repoRoot = repository, home = homedir(), mode = 'all', dryRun = false } = {}) {
if (!['all', 'webview', 'extension'].includes(mode)) throw new Error(`Unknown sync mode: ${mode}`);
const groups = mode === 'all' ? ['webview', 'extension'] : [mode];
const manifest = json(path.join(repoRoot, 'package.json'));
const id = `${manifest.publisher}.${manifest.name}`;
for (const group of groups) {
for (const asset of requiredAssets[group]) {
const file = path.join(repoRoot, 'dist', group, asset);
if (!existsSync(file) || !statSync(file).isFile() || statSync(file).size === 0) {
throw new Error(`Missing ${file}. Build the selected ${group} assets before syncing.`);
}
}
}

for (const targetDir of entries) {
cpSync(webviewSrc, path.join(targetDir, 'dist', 'webview'), { recursive: true, force: true });
if (includeExtension && existsSync(extensionSrc)) {
cpSync(extensionSrc, path.join(targetDir, 'dist', 'extension'), { recursive: true, force: true });
if (existsSync(manifestSrc)) {
cpSync(manifestSrc, path.join(targetDir, 'package.json'), { force: true });
const targets = [];
for (const [editor, folder] of [['VS Code', '.vscode'], ['Cursor', '.cursor']]) {
const root = path.join(home, folder, 'extensions');
const registry = path.join(root, 'extensions.json');
if (!existsSync(registry)) continue;
const obsoletePath = path.join(root, '.obsolete');
const obsolete = existsSync(obsoletePath) ? json(obsoletePath) : {};
const locations = new Set(json(registry).filter(entry => entry.identifier?.id === id)
.map(entry => entry.relativeLocation));
for (const location of locations) {
if (typeof location !== 'string' || !location.startsWith(`${id}-`) || /[/\\]/.test(location)) {
throw new Error(`Invalid ${editor} extension location.`);
}
if (obsolete[location]) continue;
const directory = path.join(root, location);
const installed = json(path.join(directory, 'package.json'));
if (installed.version !== manifest.version || installed.publisher !== manifest.publisher || installed.name !== manifest.name) {
throw new Error(`${editor}: installed extension differs from this version. Install a VSIX before using hot updates.`);
}
const files = groups.flatMap(group => changedFiles(
path.join(repoRoot, 'dist', group), path.join(directory, 'dist', group)));
targets.push({ editor, directory, files });
}
syncedCount += 1;
console.log(`Synced webview bundle -> ${targetDir}`);
}
if (!targets.length) throw new Error('No registered Droid installation found in VS Code or Cursor. Install a VSIX first.');
// Finish preflight for both editors before writing to either installation.
if (!dryRun) for (const target of targets) {
for (const file of target.files) cpSync(file.from, file.to, { force: true });
}
return targets.map(({ editor, directory, files }) => ({ editor, directory, changedFiles: files.length }));
}

if (syncedCount === 0) {
console.warn('No installed droidvisx.droidvisx-* extension directories found under ~/.vscode or ~/.cursor.');
if (process.argv[1] && pathToFileURL(path.resolve(process.argv[1])).href === import.meta.url) {
try {
const args = process.argv.slice(2);
if (args.some(arg => !['--webview-only', '--extension-only', '--dry-run'].includes(arg)) ||
args.includes('--webview-only') && args.includes('--extension-only')) throw new Error('Choose one sync mode: --webview-only or --extension-only.');
const mode = args.includes('--webview-only') ? 'webview' : args.includes('--extension-only') ? 'extension' : 'all';
const dryRun = args.includes('--dry-run');
for (const result of syncGlobalExtensions({ mode, dryRun })) {
console.log(`${dryRun ? 'Would sync' : 'Synced'} ${mode}: ${result.editor}, ${result.changedFiles} changed files -> ${result.directory}`);
}
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
}
Loading