diff --git a/vscode/esbuild.mjs b/vscode/esbuild.mjs index c8ddf6e..ed18453 100644 --- a/vscode/esbuild.mjs +++ b/vscode/esbuild.mjs @@ -1,3 +1,5 @@ +/* eslint-disable no-console -- 构建脚本,向终端打印进度是它的正当输出 */ +import process from 'node:process' import * as esbuild from 'esbuild' const production = process.argv.includes('--production') @@ -17,11 +19,18 @@ const options = { logLevel: 'info', } -if (watch) { - const ctx = await esbuild.context(options) - await ctx.watch() - console.log('[esbuild] watching...') -} -else { - await esbuild.build(options) +async function main() { + if (watch) { + const ctx = await esbuild.context(options) + await ctx.watch() + console.log('[esbuild] watching...') + } + else { + await esbuild.build(options) + } } + +main().catch((err) => { + console.error(err) + process.exit(1) +}) diff --git a/vscode/src/cc/claudeBin.ts b/vscode/src/cc/claudeBin.ts index 66cb2d0..0edd143 100644 --- a/vscode/src/cc/claudeBin.ts +++ b/vscode/src/cc/claudeBin.ts @@ -1,6 +1,7 @@ import { existsSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' +import process from 'node:process' /** * 解析 claude 可执行文件绝对路径;找不到返回 null。 diff --git a/vscode/src/cc/profiles.ts b/vscode/src/cc/profiles.ts index 8ea2c47..6acea91 100644 --- a/vscode/src/cc/profiles.ts +++ b/vscode/src/cc/profiles.ts @@ -19,6 +19,7 @@ import { accessSync, constants } from 'node:fs' import { readdir } from 'node:fs/promises' import { homedir } from 'node:os' import * as path from 'node:path' +import process from 'node:process' import { expandTilde } from '../git/worktree' export interface ClaudeProfile { diff --git a/vscode/src/cc/spawnClaude.ts b/vscode/src/cc/spawnClaude.ts index 7e2b38e..cfc4252 100644 --- a/vscode/src/cc/spawnClaude.ts +++ b/vscode/src/cc/spawnClaude.ts @@ -20,8 +20,10 @@ * https://code.claude.com/docs/en/agent-sdk/streaming-vs-single-mode */ +import type { Buffer } from 'node:buffer' import { spawn } from 'node:child_process' import * as path from 'node:path' +import process from 'node:process' import { resolveClaudeBin } from './claudeBin' import { summarizeClaudeStreamLine } from './claudeProgress' diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 4f56b85..33aa3ad 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -7,6 +7,7 @@ import type { import { commands, EventEmitter, window } from 'vscode' import { initIdentity } from './auth/identity' import { setProfilesDirOverride } from './cc/profiles' +import { logger } from './logging/logger' import { KanbanWebviewPanel } from './panel/KanbanPanel' import { getSettings } from './settings/store' import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler' @@ -35,7 +36,7 @@ export function activate(context: ExtensionContext): void { // Start the webhook server immediately so PR callbacks are received even // when the Kanban panel is closed. Disposed automatically via subscriptions. webhookCoordinator.init(context) - context.subscriptions.push({ + context.subscriptions.push(logger, { dispose: () => { void webhookCoordinator.dispose() }, }) // 状态栏常驻展示当前 Gitea 身份;token 用错人(同步/复制)时第一眼能看出来。 diff --git a/vscode/src/git/worktree.ts b/vscode/src/git/worktree.ts index bdf53f5..5e3ae69 100644 --- a/vscode/src/git/worktree.ts +++ b/vscode/src/git/worktree.ts @@ -7,6 +7,7 @@ import { execFile } from 'node:child_process' import { promises as fsp, readdirSync, readFileSync, readlinkSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' +import process from 'node:process' import { deleteLocalBranch } from './branchSync' /** diff --git a/vscode/src/git/worktreeHooks.ts b/vscode/src/git/worktreeHooks.ts index 174c0e3..55a1e36 100644 --- a/vscode/src/git/worktreeHooks.ts +++ b/vscode/src/git/worktreeHooks.ts @@ -21,6 +21,7 @@ import { execFile } from 'node:child_process' import { existsSync } from 'node:fs' import * as path from 'node:path' +import process from 'node:process' const DEFAULT_POST_CREATE_REL = '.spx/worktree-post-create.sh' const DEFAULT_PRE_REMOVE_REL = '.spx/worktree-pre-remove.sh' diff --git a/vscode/src/logging/logger.ts b/vscode/src/logging/logger.ts index 1517cc5..adae101 100644 --- a/vscode/src/logging/logger.ts +++ b/vscode/src/logging/logger.ts @@ -3,15 +3,16 @@ * * Both the extension host and the webview share access to these entries * via the panel's message bridge; the webview surfaces them in a modal - * opened from the IssueDetailPanel. Entries are also mirrored to the - * Extension Host console so they remain visible during local debugging. + * opened from the IssueDetailPanel. Entries are also mirrored to a VS Code + * LogOutputChannel ("Superpowers" in the Output panel) so they remain + * visible without the webview and honour the user's log level. * * No filesystem persistence — buffer is capped at the last 500 entries * and lost on extension reload. */ -import type { Event } from 'vscode' -import { EventEmitter } from 'vscode' +import type { Event, LogOutputChannel } from 'vscode' +import { EventEmitter, window } from 'vscode' export type LogLevel = 'info' | 'warn' | 'error' @@ -30,13 +31,14 @@ class Logger { private buf: LogEntry[] = [] private readonly cap = 500 private readonly emitter = new EventEmitter() + private readonly channel: LogOutputChannel = window.createOutputChannel('Superpowers', { log: true }) readonly onEntry: Event = this.emitter.event /** * Record a new entry. Stamps with the current time, trims the buffer to * the last `cap` entries, fires the change event, and mirrors to the - * Extension Host console. + * output channel. */ add(entry: Omit): void { const full: LogEntry = { ...entry, ts: Date.now() } @@ -45,16 +47,13 @@ class Logger { this.buf.splice(0, this.buf.length - this.cap) const line = `[${full.source}] ${full.message}` - // Mirror to Extension Host console for parity with existing console.warn/error. - + const args = full.details === undefined ? [] : [full.details] if (full.level === 'error') - console.error(line, full.details ?? '') - + this.channel.error(line, ...args) else if (full.level === 'warn') - console.warn(line, full.details ?? '') - + this.channel.warn(line, ...args) else - console.log(line, full.details ?? '') + this.channel.info(line, ...args) this.emitter.fire(full) } @@ -68,6 +67,11 @@ class Logger { clear(): void { this.buf = [] } + + dispose(): void { + this.channel.dispose() + this.emitter.dispose() + } } export const logger = new Logger() diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index c60afff..998111b 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -1134,7 +1134,7 @@ export class KanbanWebviewPanel { // 重写 src 和 href 的相对路径为 webview URI。vite 产物是固定名(无 hash), // webview 按 URL 缓存资源——必须按文件 mtime 加 ?v= 版本号,否则 reload 后 // 仍会跑缓存里的旧 index.js / index.css。 - html = html.replace(/(src|href)="(\/[^"]+|\.\/[^"]+|[^"/][^"]*)"/g, (_m, attr, p) => { + html = html.replace(/(src|href)="(\/[^"]+|[^"/][^"]*)"/g, (_m, attr, p) => { const cleaned = p.replace(/^\.?\//, '') const fileUri = Uri.joinPath(distRoot, cleaned) let version = '' diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index e3484a4..b42b23e 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -2304,7 +2304,7 @@ export async function handleGeneratePrDiffSummary(panel: KanbanWebviewPanel, iss return } const profilePath = resolution.profilePath - const prompt = `你在一个 VS Code 扩展启动的后台 Claude 会话中工作。\n\n硬性限制:\n- 不要修改代码。\n- 不要创建或切换分支。\n- 不要提交。\n- 不要 push。\n- 不要写任何文件。\n- 只分析当前 PR 的代码变动,并把 Markdown 摘要正文直接输出到 stdout。\n\n任务:\n- 当前工作目录是工单 #\${issueNumber} 的 worktree。\n- 分析 PR #\${pr} 与主分支的代码差异。\n- 用 git 拿差异:先 git fetch origin main,再 git diff origin/main...HEAD(概览可加 --stat)。注意 tea 没有 diff 子命令,不要尝试 tea pulls diff。\n\n输出必须是合法 Markdown,预览器要能渲染出标题层级与代码块高亮,结构如下:\n1. 第一行是一级标题:# PR #\${pr} 代码变更摘要\n2. 紧接着用 1~2 句话概述本次变更:做了什么、新增了哪些包/模块、关键依赖方向(例如「stockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入」)。\n3. 然后写一行图例:图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n4. 然后是一棵统一的目录树,放进单个围栏代码块(语言标注用 text):树前一行写三个反引号紧跟 text,树后一行写三个反引号。\n\n目录树规则:\n- 用 box-drawing 连线字符画树:├──、└──、│,缩进对齐,像 tree 命令的输出,不要只用纯空格缩进糊一棵没有连线的树。\n- 从仓库根开始保留真实目录层级。\n- 每个文件行以标记开头:新建文件用「+ 」、修改文件用「✎ 」,后接文件名 + 若干空格对齐 + 一句中文目的描述(简短,点出关键符号/端点/职责)。\n- 目录节点行尾可加可选的阶段/分组标签(如 [Phase N]),仅当本次变更本身有清晰阶段时才加;没有就不加,不要硬编。\n- 关键:整棵树一定要写在围栏代码块之内,否则 Markdown 预览器会折叠空白、丢掉层级与连线。\n\n排序与重点:\n- 按架构重要性排列:核心业务/领域代码(实现逻辑、入口、API routes、数据模型、service/repository)靠前。\n- alembic 数据库迁移文件、测试文件不算重点:放在各自模块或整棵树的靠后位置,照常用 +/✎ 标记列出,但不要放在最前、不要强调。\n- 测试文件判定:文件名形如 test_* / *_test.* / *.test.* / *.spec.*,或路径包含 tests/、__tests__/ 目录,或 conftest.py、fixture、mock 文件。\n- 迁移文件判定:路径在 alembic/、migrations/、versions/ 之下的文件。\n\n格式示例(请照此结构输出,不要照抄内容):\n\n# PR #123 代码变更摘要\n\nstockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入。\n\n图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n\n\\\`\\\`\\\`text\nbackend/\n└── src/luffy_agent/commerce/\n ├── models.py ✎ +StockPool;ProductCatalog 加 pool_id\n ├── stockpool/\n │ ├── service.py + reserve_pool_or_raise + compute_pool_views\n │ └── routes.py + /admin/commerce/stock-pools\n ├── orders/service.py ✎ create_order 绑定池分支\n └── alembic/versions/ + stock_pool 建表 + pool_id 加列\n\\\`\\\`\\\`\n\n要求:\n- 每个新增/修改文件一行,一句话说明新增/修改了什么及其目的。\n- 整棵树只用一个围栏代码块,用 box-drawing 连线保留真实目录层级。\n- 只输出上述 Markdown 正文本身,不要写文件,不要任何前导说明/寒暄/结尾总结,第一行就是 # PR #\${pr} 代码变更摘要 标题。` + const prompt = `你在一个 VS Code 扩展启动的后台 Claude 会话中工作。\n\n硬性限制:\n- 不要修改代码。\n- 不要创建或切换分支。\n- 不要提交。\n- 不要 push。\n- 不要写任何文件。\n- 只分析当前 PR 的代码变动,并把 Markdown 摘要正文直接输出到 stdout。\n\n任务:\n- 当前工作目录是工单 #\${issueNumber} 的 worktree。\n- 分析 PR #\${pr} 与主分支的代码差异。\n- 用 git 拿差异:先 git fetch origin main,再 git diff origin/main...HEAD(概览可加 --stat)。注意 tea 没有 diff 子命令,不要尝试 tea pulls diff。\n\n输出必须是合法 Markdown,预览器要能渲染出标题层级与代码块高亮,结构如下:\n1. 第一行是一级标题:# PR #\${pr} 代码变更摘要\n2. 紧接着用 1~2 句话概述本次变更:做了什么、新增了哪些包/模块、关键依赖方向(例如「stockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入」)。\n3. 然后写一行图例:图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n4. 然后是一棵统一的目录树,放进单个围栏代码块(语言标注用 text):树前一行写三个反引号紧跟 text,树后一行写三个反引号。\n\n目录树规则:\n- 用 box-drawing 连线字符画树:├──、└──、│,缩进对齐,像 tree 命令的输出,不要只用纯空格缩进糊一棵没有连线的树。\n- 从仓库根开始保留真实目录层级。\n- 每个文件行以标记开头:新建文件用「+ 」、修改文件用「✎ 」,后接文件名 + 若干空格对齐 + 一句中文目的描述(简短,点出关键符号/端点/职责)。\n- 目录节点行尾可加可选的阶段/分组标签(如 [Phase N]),仅当本次变更本身有清晰阶段时才加;没有就不加,不要硬编。\n- 关键:整棵树一定要写在围栏代码块之内,否则 Markdown 预览器会折叠空白、丢掉层级与连线。\n\n排序与重点:\n- 按架构重要性排列:核心业务/领域代码(实现逻辑、入口、API routes、数据模型、service/repository)靠前。\n- alembic 数据库迁移文件、测试文件不算重点:放在各自模块或整棵树的靠后位置,照常用 +/✎ 标记列出,但不要放在最前、不要强调。\n- 测试文件判定:文件名形如 test_* / *_test.* / *.test.* / *.spec.*,或路径包含 tests/、__tests__/ 目录,或 conftest.py、fixture、mock 文件。\n- 迁移文件判定:路径在 alembic/、migrations/、versions/ 之下的文件。\n\n格式示例(请照此结构输出,不要照抄内容):\n\n# PR #123 代码变更摘要\n\nstockpool/ 是与 orders/ catalog/ 平级的新子域包,其 service 只单向依赖 orders/catalog 的 repository,无循环导入。\n\n图例:\\\`+\\\` = 新建文件 \\\`✎\\\` = 修改文件\n\n\\\`\\\`\\\`text\nbackend/\n└── src/luffy_agent/commerce/\n ├── models.py ✎ +StockPool;ProductCatalog 加 pool_id\n ├── stockpool/\n │ ├── service.py + reserve_pool_or_raise + compute_pool_views\n │ └── routes.py + /admin/commerce/stock-pools\n ├── orders/service.py ✎ create_order 绑定池分支\n └── alembic/versions/ + stock_pool 建表 + pool_id 加列\n\\\`\\\`\\\`\n\n要求:\n- 每个新增/修改文件一行,一句话说明新增/修改了什么及其目的。\n- 整棵树只用一个围栏代码块,用 box-drawing 连线保留真实目录层级。\n- 只输出上述 Markdown 正文本身,不要写文件,不要任何前导说明/寒暄/结尾总结,第一行就是 # PR #\${pr} 代码变更摘要 标题。` const result = await spawnClaude({ prompt, diff --git a/vscode/src/panel/handlers/profileAssets.ts b/vscode/src/panel/handlers/profileAssets.ts index 2acba44..68dfb23 100644 --- a/vscode/src/panel/handlers/profileAssets.ts +++ b/vscode/src/panel/handlers/profileAssets.ts @@ -10,6 +10,7 @@ */ import type { KanbanWebviewPanel } from '../KanbanPanel' +import { Buffer } from 'node:buffer' import { createHash } from 'node:crypto' import { promises as fsp } from 'node:fs' import * as path from 'node:path' diff --git a/vscode/src/panel/handlers/settings.ts b/vscode/src/panel/handlers/settings.ts index 8499db1..d55d44f 100644 --- a/vscode/src/panel/handlers/settings.ts +++ b/vscode/src/panel/handlers/settings.ts @@ -4,6 +4,7 @@ import { execFile } from 'node:child_process' import * as fs from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' +import process from 'node:process' import { commands, env, Uri, workspace } from 'vscode' import { clearIdentityCache, initIdentity } from '../../auth/identity' import { getToken, getYouTrackToken, setToken, setYouTrackToken } from '../../auth/secrets' diff --git a/vscode/src/panel/handlers/worktree.ts b/vscode/src/panel/handlers/worktree.ts index f7ef9d3..254f5a6 100644 --- a/vscode/src/panel/handlers/worktree.ts +++ b/vscode/src/panel/handlers/worktree.ts @@ -232,10 +232,18 @@ export async function dispatchWorktreeHook( ): Promise { let result switch (phase) { - case 'post-create': result = await runPostCreateHook(ctx); break - case 'pre-remove': result = await runPreRemoveHook(ctx); break - case 'impl-tab-pre-create': result = await runImplTabPreCreateHook(ctx); break - case 'impl-tab-post-close': result = await runImplTabPostCloseHook(ctx); break + case 'post-create': + result = await runPostCreateHook(ctx) + break + case 'pre-remove': + result = await runPreRemoveHook(ctx) + break + case 'impl-tab-pre-create': + result = await runImplTabPreCreateHook(ctx) + break + case 'impl-tab-post-close': + result = await runImplTabPostCloseHook(ctx) + break } // 'skipped' = the script simply isn't on disk. That's the intended diff --git a/vscode/src/panel/issueColor.ts b/vscode/src/panel/issueColor.ts index 62ef08e..4d7bad8 100644 --- a/vscode/src/panel/issueColor.ts +++ b/vscode/src/panel/issueColor.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer' import { ThemeColor, Uri } from 'vscode' // 12 saturated ansi colors, chosen for tab-icon contrast. diff --git a/vscode/src/webhook/server.ts b/vscode/src/webhook/server.ts index 77b1a97..fecd86e 100644 --- a/vscode/src/webhook/server.ts +++ b/vscode/src/webhook/server.ts @@ -158,7 +158,7 @@ export class WebhookServer { const server = createServer((req, res) => this.handle(req, res)) await new Promise((resolve, reject) => { - const onError = (err: Error): void => { + function onError(err: Error): void { server.off('listening', onListening) logger.add({ level: 'error', @@ -168,7 +168,7 @@ export class WebhookServer { }) reject(err) } - const onListening = (): void => { + function onListening(): void { server.off('error', onError) resolve() } diff --git a/vscode/test/gitea/stateJson.test.ts b/vscode/test/gitea/stateJson.test.ts index 52484fd..580becf 100644 --- a/vscode/test/gitea/stateJson.test.ts +++ b/vscode/test/gitea/stateJson.test.ts @@ -1,3 +1,4 @@ +import { Buffer } from 'node:buffer' import { EventEmitter } from 'node:events' import { delimiter } from 'node:path' import { beforeEach, describe, expect, it, vi } from 'vitest' diff --git a/vscode/webview-ui/src/components/SettingsModal.tsx b/vscode/webview-ui/src/components/SettingsModal.tsx index bbd7a23..f5ab49d 100644 --- a/vscode/webview-ui/src/components/SettingsModal.tsx +++ b/vscode/webview-ui/src/components/SettingsModal.tsx @@ -247,7 +247,6 @@ export function SettingsModal({ setYoutrackCloseCommand(initialYoutrackCloseCommand) setYoutrackToken('') setErrors({}) - // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) // ESC closes when allowed.