diff --git a/vscode/package.json b/vscode/package.json index 8863d14..c11de0e 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -107,6 +107,9 @@ "ext:package": "pnpm build && vsce package --no-dependencies", "ext:publish:vsce": "npm_config_proxy= npm_config_https_proxy= vsce publish --skip-duplicate" }, + "dependencies": { + "diff": "^9.0.0" + }, "devDependencies": { "@antfu/eslint-config": "^6.7.3", "@types/diff": "^8.0.0", @@ -120,8 +123,5 @@ "ovsx": "^0.10.7", "typescript": "^5.9.3", "vitest": "^4.0.16" - }, - "dependencies": { - "diff": "^9.0.0" } } diff --git a/vscode/pnpm-workspace.yaml b/vscode/pnpm-workspace.yaml index c849068..9c17f24 100644 --- a/vscode/pnpm-workspace.yaml +++ b/vscode/pnpm-workspace.yaml @@ -1,3 +1,7 @@ +shellEmulator: true + +trustPolicy: no-downgrade + packages: - webview-ui diff --git a/vscode/prompts/brainstorm.md b/vscode/prompts/brainstorm.md index da6eff9..4a07c98 100644 --- a/vscode/prompts/brainstorm.md +++ b/vscode/prompts/brainstorm.md @@ -45,4 +45,4 @@ spx 自动找到对应行替换或追加,保留所有其他 marker。**不要 - 不要创建分支 - 不要切换分支 - 不要修改当前主 worktree 所在分支 -- spec/plan 一律在 main 分支(当前主 worktree)创建 \ No newline at end of file +- spec/plan 一律在 main 分支(当前主 worktree)创建 diff --git a/vscode/prompts/implement-plan.md b/vscode/prompts/implement-plan.md index 09b0ce9..c4e9860 100644 --- a/vscode/prompts/implement-plan.md +++ b/vscode/prompts/implement-plan.md @@ -10,4 +10,4 @@ - 生成后立即 `git add` 提交迁移文件,纳入 PR。 - **绝不**对共享 dev DB(`192.168.1.4:5433`)或 prod 执行 `alembic upgrade` / `downgrade` 等任何改库命令。 - 需要运行时验证迁移,就起一个一次性独立库(本地 docker postgres 或唯一命名的 scratch 库),在它上面 upgrade,验证完即丢弃,不要留痕。 -- 共享 dev DB 与 prod 的迁移合并后由用户统一执行,时机由用户决定。 \ No newline at end of file +- 共享 dev DB 与 prod 的迁移合并后由用户统一执行,时机由用户决定。 diff --git a/vscode/src/auth/secrets.ts b/vscode/src/auth/secrets.ts index e794cfd..86dffd6 100644 --- a/vscode/src/auth/secrets.ts +++ b/vscode/src/auth/secrets.ts @@ -24,8 +24,10 @@ export async function deleteToken(ctx: ExtensionContext, host: string): Promise< await ctx.secrets.delete(tokenKey(host)) } -/** YouTrack permanent tokens, keyed by host so they never collide with the - * gitea token of the same host. */ +/** + * YouTrack permanent tokens, keyed by host so they never collide with the + * gitea token of the same host. + */ export function youtrackTokenKey(host: string): string { return `youtrack-token:${host}` } diff --git a/vscode/src/cc/claudeBin.test.ts b/vscode/src/cc/claudeBin.test.ts index f78aa9e..d53633e 100644 --- a/vscode/src/cc/claudeBin.test.ts +++ b/vscode/src/cc/claudeBin.test.ts @@ -34,24 +34,24 @@ describe('resolveClaudeBin', () => { expect(resolveClaudeBin({ PATH: dir }, '/no-such-home')).toBe(bin) }) - it('PATH 未命中时回退到 $HOME/.local/bin/claude', () => { + it('pATH 未命中时回退到 $HOME/.local/bin/claude', () => { const home = tempDir() const bin = touchClaude(path.join(home, '.local', 'bin')) expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin) }) - it('PATH 未命中时回退到 $HOME/.claude/local/claude', () => { + it('pATH 未命中时回退到 $HOME/.claude/local/claude', () => { const home = tempDir() const bin = touchClaude(path.join(home, '.claude', 'local')) expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin) }) - it('PATH 与候选皆空 → null', () => { + it('pATH 与候选皆空 → null', () => { const home = tempDir() expect(resolveClaudeBin({ PATH: '' }, home)).toBeNull() }) - it('PATH 中目录不存在时跳过并继续', () => { + it('pATH 中目录不存在时跳过并继续', () => { const home = tempDir() const bin = touchClaude(path.join(home, '.local', 'bin')) expect(resolveClaudeBin({ PATH: '/definitely/missing/bin' }, home)).toBe(bin) diff --git a/vscode/src/cc/codexSessionWatcher.ts b/vscode/src/cc/codexSessionWatcher.ts index 45c6a17..635e8d1 100644 --- a/vscode/src/cc/codexSessionWatcher.ts +++ b/vscode/src/cc/codexSessionWatcher.ts @@ -169,13 +169,11 @@ export async function watchForNewCodexSession(opts: { }) }) watcher.on('error', (err) => { - // eslint-disable-next-line no-console console.warn('[superpowers] codexSessionWatcher: watch error', err) done(null) }) } catch (err) { - // eslint-disable-next-line no-console console.warn('[superpowers] codexSessionWatcher: fs.watch threw', err) done(null) return diff --git a/vscode/src/cc/commitProfile.test.ts b/vscode/src/cc/commitProfile.test.ts index 06bd2b9..1b1a2b0 100644 --- a/vscode/src/cc/commitProfile.test.ts +++ b/vscode/src/cc/commitProfile.test.ts @@ -57,7 +57,7 @@ describe('resolveCommitProfileForRun', () => { const result = resolveCommitProfileForRun( missing, p => p, - p => { + (p) => { try { accessSync(p, constants.R_OK) return true @@ -77,7 +77,7 @@ describe('resolveCommitProfileForRun', () => { const result = resolveCommitProfileForRun( file, p => p, - p => { + (p) => { try { accessSync(p, constants.R_OK) return true @@ -98,7 +98,7 @@ describe('resolveCommitProfileForRun', () => { const result = resolveCommitProfileForRun( foreign, () => local, - p => { + (p) => { try { accessSync(p, constants.R_OK) return true diff --git a/vscode/src/cc/prDiffMode.test.ts b/vscode/src/cc/prDiffMode.test.ts index 96efc1c..f9a610e 100644 --- a/vscode/src/cc/prDiffMode.test.ts +++ b/vscode/src/cc/prDiffMode.test.ts @@ -1,12 +1,13 @@ +import type { PrDiffMode } from './prDiffMode' import { describe, expect, it } from 'vitest' import { DEFAULT_PR_DIFF_MODE, isPrDiffMode, parsePrDiffMode, - type PrDiffMode, + } from './prDiffMode' -describe('DEFAULT_PR_DIFF_MODE', () => { +describe('dEFAULT_PR_DIFF_MODE', () => { it('默认是 unified', () => { expect(DEFAULT_PR_DIFF_MODE).toBe('unified') }) diff --git a/vscode/src/cc/profiles.test.ts b/vscode/src/cc/profiles.test.ts index bcd3298..55d53ec 100644 --- a/vscode/src/cc/profiles.test.ts +++ b/vscode/src/cc/profiles.test.ts @@ -1,4 +1,4 @@ -import { accessSync, constants, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { accessSync, constants, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' diff --git a/vscode/src/cc/sessionWatcher.ts b/vscode/src/cc/sessionWatcher.ts index 7f1118f..f9f6ea3 100644 --- a/vscode/src/cc/sessionWatcher.ts +++ b/vscode/src/cc/sessionWatcher.ts @@ -35,7 +35,6 @@ export async function watchForNewSession(opts: { snapshot = new Set(existing.filter(n => n.endsWith('.jsonl'))) } catch (err) { - // eslint-disable-next-line no-console console.warn('[superpowers] sessionWatcher: readdir failed', err) return null } @@ -89,13 +88,11 @@ export async function watchForNewSession(opts: { }) }) watcher.on('error', (err) => { - // eslint-disable-next-line no-console console.warn('[superpowers] sessionWatcher: watch error', err) done(null) }) } catch (err) { - // eslint-disable-next-line no-console console.warn('[superpowers] sessionWatcher: fs.watch threw', err) done(null) return @@ -131,7 +128,6 @@ export async function pollForNewSession(opts: { snapshot = new Set(existing.filter(n => n.endsWith('.jsonl'))) } catch (err) { - // eslint-disable-next-line no-console console.warn('[superpowers] sessionWatcher: poll readdir failed', err) return null } diff --git a/vscode/src/cc/spawnClaude.ts b/vscode/src/cc/spawnClaude.ts index 73effa2..7e2b38e 100644 --- a/vscode/src/cc/spawnClaude.ts +++ b/vscode/src/cc/spawnClaude.ts @@ -105,7 +105,6 @@ function extractClaudePayload(stdout: string): ParsedClaudeJson | null { return null } - function truncateDiagnostic(text: string): string { if (text.length <= EXIT_DIAGNOSTIC_MAX_CHARS) return text diff --git a/vscode/src/files/envLock.ts b/vscode/src/files/envLock.ts index 30b0cde..a08d2d2 100644 --- a/vscode/src/files/envLock.ts +++ b/vscode/src/files/envLock.ts @@ -15,9 +15,11 @@ import { RelativePattern, workspace } from 'vscode' const ENV_PATTERN = '**/.env*' const EXCLUDE_PATTERN = '{**/node_modules/**,**/.git/**,**/.claude/**}' -/** Scan the workspace folder rooted at `workspaceRoot` for all `.env*` +/** + * Scan the workspace folder rooted at `workspaceRoot` for all `.env*` * files, skipping `node_modules`, `.git`, and `.claude`. Returns absolute - * fs paths. Empty array if the workspace folder isn't currently open. */ + * fs paths. Empty array if the workspace folder isn't currently open. + */ export async function findEnvFiles(workspaceRoot: string): Promise { const folder = workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot) if (!folder) diff --git a/vscode/src/git/branchSync.ts b/vscode/src/git/branchSync.ts index 00f92f9..74ed5fa 100644 --- a/vscode/src/git/branchSync.ts +++ b/vscode/src/git/branchSync.ts @@ -15,14 +15,18 @@ import { execFile } from 'node:child_process' export interface BranchSyncStatus { - /** Commits remote `autoBuildBranch` is behind remote `devBranch`. 0 when + /** + * Commits remote `autoBuildBranch` is behind remote `devBranch`. 0 when * already in sync, or when `unavailable` (the field is still numeric so - * callers don't have to special-case it). */ + * callers don't have to special-case it). + */ behind: number devBranch: string autoBuildBranch: string - /** True when sync is structurally impossible (same branch, missing remote - * ref, fetch failed, not a git repo, …). Disables the toolbar button. */ + /** + * True when sync is structurally impossible (same branch, missing remote + * ref, fetch failed, not a git repo, …). Disables the toolbar button. + */ unavailable?: boolean /** Human-readable explanation, suitable for the button's tooltip. */ reason?: string diff --git a/vscode/src/git/worktreeHooks.ts b/vscode/src/git/worktreeHooks.ts index fb69427..174c0e3 100644 --- a/vscode/src/git/worktreeHooks.ts +++ b/vscode/src/git/worktreeHooks.ts @@ -34,8 +34,10 @@ export interface HookContext { branch: string issueNumber: number mainBranch: string - /** User-configured override path. Empty / undefined → use the default - * `/.spx/worktree-*.sh` location. */ + /** + * User-configured override path. Empty / undefined → use the default + * `/.spx/worktree-*.sh` location. + */ customScriptPath?: string } diff --git a/vscode/src/gitea/types.ts b/vscode/src/gitea/types.ts index 2965856..0ad7d8d 100644 --- a/vscode/src/gitea/types.ts +++ b/vscode/src/gitea/types.ts @@ -8,26 +8,36 @@ export type IssueColumn = 'todo' | 'in-progress' | 'review' | 'done' export interface Issue { - /** Stable identifier: `${owner}/${repo}#${index}` (gitea) or + /** + * Stable identifier: `${owner}/${repo}#${index}` (gitea) or * `youtrack:${idReadable}` (youtrack). Used to route mutations to the right - * source when both kinds of cards share the board. */ + * source when both kinds of cards share the board. + */ id: string - /** Which tracker this issue came from. Absent is treated as `'gitea'` for - * backward compatibility with state written before the second source. */ + /** + * Which tracker this issue came from. Absent is treated as `'gitea'` for + * backward compatibility with state written before the second source. + */ source?: 'gitea' | 'youtrack' - /** YouTrack readable id (`LXF-12`) — present only on youtrack issues; needed - * for REST calls that key by readable id. */ + /** + * YouTrack readable id (`LXF-12`) — present only on youtrack issues; needed + * for REST calls that key by readable id. + */ externalId?: string - /** Equals the Gitea issue number / tea's `index` field; for youtrack it's the + /** + * Equals the Gitea issue number / tea's `index` field; for youtrack it's the * numeric tail of `externalId` (collides across sources — never use alone as - * a key, pair with `source`). */ + * a key, pair with `source`). + */ number: number title: string column: IssueColumn /** YouTrack attachments (name + absolute URL). Links only; not downloaded. */ attachments?: Array<{ name: string, url: string }> - /** Optional Claude Code session id stored alongside the column marker in - * the issue's state-JSON comment. Used to resume the conversation. */ + /** + * Optional Claude Code session id stored alongside the column marker in + * the issue's state-JSON comment. Used to resume the conversation. + */ sessionId?: string /** 实施会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */ profilePath?: string @@ -35,21 +45,27 @@ export interface Issue { brainstormProfilePath?: string /** 测试会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */ testProfilePath?: string - /** Optional workspace-relative path to the spec file for this issue, as + /** + * Optional workspace-relative path to the spec file for this issue, as * surfaced from the Claude session transcript. Lives under - * `docs/superpowers/specs/*.md`. */ + * `docs/superpowers/specs/*.md`. + */ specFile?: string - /** Optional workspace-relative path to the plan file for this issue, as + /** + * Optional workspace-relative path to the plan file for this issue, as * surfaced from the Claude session transcript. Lives under - * `docs/superpowers/plans/*.md`. */ + * `docs/superpowers/plans/*.md`. + */ planFile?: string /** Optional workspace-relative path to the generated PR diff summary. */ prDiffFile?: string /** PR number associated with this issue (set after webhook fires). */ pr?: string - /** Whether the associated PR has been merged. Set when the user drags to + /** + * Whether the associated PR has been merged. Set when the user drags to * the 完成 列 (扩展代为合并) or when a `pull_request.closed` webhook arrives - * with `merged === true`. Persisted in the state JSON. */ + * with `merged === true`. Persisted in the state JSON. + */ prMerged?: boolean /** PR 合并时间(ISO 8601,来自 PR API 的 merged_at)。完成列按它降序排序,未合并为 undefined。合并时持久化进 state JSON;完成工单加载时不再实时拉 PR,直接读持久化值(缺失则按工单号回退排序)。 */ prMergedAt?: string @@ -61,32 +77,44 @@ export interface Issue { worktreeExists?: boolean /** Lifecycle of the implementation flow. */ implementStatus?: 'running' | 'done' | 'failed' - /** Session id of the implementation conversation (separate from - * discussion-session `sessionId`). */ + /** + * Session id of the implementation conversation (separate from + * discussion-session `sessionId`). + */ implementSessionId?: string - /** Backend-agnostic review session id (v1 stores a codex thread id). Set + /** + * Backend-agnostic review session id (v1 stores a codex thread id). Set * the first time auto-review runs; reused for subsequent `synchronize` - * webhook callbacks via `codex exec resume`. */ + * webhook callbacks via `codex exec resume`. + */ reviewSessionId?: string - /** Whether the codex session file (`~/.codex/sessions/YYYY/MM/DD/rollout-...-.jsonl`) + /** + * Whether the codex session file (`~/.codex/sessions/YYYY/MM/DD/rollout-...-.jsonl`) * for `reviewSessionId` exists on this machine; computed on load, never - * persisted. codex resume 只依赖这个文件,与 worktree 无关。 */ + * persisted. codex resume 只依赖这个文件,与 worktree 无关。 + */ reviewSessionFileExists?: boolean - /** Session id of the test conversation (a Claude Code session id). Set the + /** + * Session id of the test conversation (a Claude Code session id). Set the * first time the user manually starts a test session after the issue's PR - * is merged; reused for resume from the detail panel. */ + * is merged; reused for resume from the detail panel. + */ testSessionId?: string /** Browser URL to the Gitea issue page (escape hatch button in the UI). */ htmlUrl: string /** 前置任务 issue number (Gitea dependencies 取第一个) */ prerequisite?: number - /** Terminal/tab color id (a `terminal.ansi*` ThemeColor key) chosen the + /** + * Terminal/tab color id (a `terminal.ansi*` ThemeColor key) chosen the * first time a session is opened for this issue, persisted in the state - * JSON so all subsequent sessions reuse the same tone. */ + * JSON so all subsequent sessions reuse the same tone. + */ color?: string - /** Per-issue override of the global `autoReview` setting. When set, the + /** + * Per-issue override of the global `autoReview` setting. When set, the * webhook coordinator uses this value instead of the global flag. Stored - * in the issue's state-JSON comment so it survives reloads. */ + * in the issue's state-JSON comment so it survives reloads. + */ autoReview?: boolean /** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */ assignees?: string[] diff --git a/vscode/src/logging/logger.ts b/vscode/src/logging/logger.ts index 1fca278..1517cc5 100644 --- a/vscode/src/logging/logger.ts +++ b/vscode/src/logging/logger.ts @@ -46,13 +46,13 @@ class Logger { const line = `[${full.source}] ${full.message}` // Mirror to Extension Host console for parity with existing console.warn/error. - // eslint-disable-next-line no-console + if (full.level === 'error') console.error(line, full.details ?? '') - // eslint-disable-next-line no-console + else if (full.level === 'warn') console.warn(line, full.details ?? '') - // eslint-disable-next-line no-console + else console.log(line, full.details ?? '') diff --git a/vscode/src/panel/handlers/managedSessions.ts b/vscode/src/panel/handlers/managedSessions.ts index 0a3d967..7aaef77 100644 --- a/vscode/src/panel/handlers/managedSessions.ts +++ b/vscode/src/panel/handlers/managedSessions.ts @@ -2,13 +2,13 @@ import type { KanbanWebviewPanel } from '../KanbanPanel' import { promises as fsp } from 'node:fs' import { window, workspace } from 'vscode' import { buildCcCommand } from '../../cc/ccCommand' +import { resolveProfilePath } from '../../cc/profiles' import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher' import { logger } from '../../logging/logger' import { listImportableNamedSessions } from '../../sessions/importableNamedSessions' import { decideManagedResumeProfile } from '../../sessions/managedResumeProfile' import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore' import { inferProfilePathForSession } from '../../sessions/sessionProfileInference' -import { resolveProfilePath } from '../../cc/profiles' import { resolveTerminalLocation } from './terminals' /** 默认会话名:有 prompt 取前 20 字符,否则用短 id(前 8 位)。 */ diff --git a/vscode/src/panel/handlers/youtrackIssues.ts b/vscode/src/panel/handlers/youtrackIssues.ts index 233cbc7..8c4b713 100644 --- a/vscode/src/panel/handlers/youtrackIssues.ts +++ b/vscode/src/panel/handlers/youtrackIssues.ts @@ -8,8 +8,8 @@ */ import type { IssueColumn } from '../../gitea/types' -import type { KanbanWebviewPanel } from '../KanbanPanel' import type { YouTrackAuth } from '../../youtrack/api' +import type { KanbanWebviewPanel } from '../KanbanPanel' import { ThemeIcon, window, workspace } from 'vscode' import { getYouTrackToken } from '../../auth/secrets' import { logger } from '../../logging/logger' @@ -24,8 +24,10 @@ function toast(panel: KanbanWebviewPanel, level: 'info' | 'success' | 'error', m panel.postMessage({ type: 'toast/show', id: makeNonce(), level, message, dismissOnTimer: 6000 }) } -/** Resolve {baseUrl, token} from settings + SecretStorage, or null if YouTrack - * isn't fully configured. */ +/** + * Resolve {baseUrl, token} from settings + SecretStorage, or null if YouTrack + * isn't fully configured. + */ async function resolveAuth(panel: KanbanWebviewPanel): Promise { const baseUrl = getSettings(panel.context).youtrackBaseUrl.trim() if (!baseUrl) @@ -61,8 +63,10 @@ export async function handleYouTrackColumnChange(panel: KanbanWebviewPanel, exte await panel.loadAndPush() } -/** Resolve the issue in YouTrack. Uses the configured close command, else - * auto-detects the project's `isResolved` state value. */ +/** + * Resolve the issue in YouTrack. Uses the configured close command, else + * auto-detects the project's `isResolved` state value. + */ async function closeYouTrackIssue(panel: KanbanWebviewPanel, auth: YouTrackAuth, externalId: string): Promise { const configured = getSettings(panel.context).youtrackCloseCommand.trim() const command = configured || (await resolvedStateCommand(auth, externalId)) @@ -74,8 +78,10 @@ async function closeYouTrackIssue(panel: KanbanWebviewPanel, auth: YouTrackAuth, await applyCommand(auth, externalId, command) } -/** Populate the settings project dropdown from the in-form base URL + token - * (lets the user pick a project before saving). */ +/** + * Populate the settings project dropdown from the in-form base URL + token + * (lets the user pick a project before saving). + */ export async function handleListProjects(panel: KanbanWebviewPanel, baseUrl: string, token: string): Promise { const trimmedBase = baseUrl.trim() const trimmedToken = token.trim() diff --git a/vscode/src/panel/issueColor.ts b/vscode/src/panel/issueColor.ts index 11ecc72..62ef08e 100644 --- a/vscode/src/panel/issueColor.ts +++ b/vscode/src/panel/issueColor.ts @@ -34,7 +34,7 @@ export function pickRandomIssueColor(): string { * `isNew` is true the caller is expected to persist the picked color back to * the issue's state JSON so subsequent sessions reuse it. */ -export function resolveIssueColor(stored: string | undefined): { id: string; isNew: boolean } { +export function resolveIssueColor(stored: string | undefined): { id: string, isNew: boolean } { if (stored && (PALETTE as readonly string[]).includes(stored)) return { id: stored, isNew: false } return { id: pickRandomIssueColor(), isNew: true } diff --git a/vscode/src/sessions/managedResumeProfile.ts b/vscode/src/sessions/managedResumeProfile.ts index 3f2ad0c..d3b3346 100644 --- a/vscode/src/sessions/managedResumeProfile.ts +++ b/vscode/src/sessions/managedResumeProfile.ts @@ -6,15 +6,15 @@ export function decideManagedResumeProfile(input: { const override = input.overrideProfile?.trim() || undefined const stored = input.storedProfile?.trim() || undefined - if (override) { // ① 行内覆盖:与 store 不同才写回;忽略 jsonl 推断 + if (override) { // ① 行内覆盖:与 store 不同才写回;忽略 jsonl 推断 return { effective: override, persist: override !== stored ? override : undefined, } } - if (stored) // ② store 已有 → 沿用,不写回 + if (stored) // ② store 已有 → 沿用,不写回 return { effective: stored, persist: undefined } - if (input.inferredProfile) // ③ 缺 profile 才用 jsonl 回填并写回 + if (input.inferredProfile) // ③ 缺 profile 才用 jsonl 回填并写回 return { effective: input.inferredProfile, persist: input.inferredProfile } return { effective: undefined, persist: undefined } } diff --git a/vscode/src/sessions/sessionProfileInference.test.ts b/vscode/src/sessions/sessionProfileInference.test.ts index bb399fe..f0a8dc6 100644 --- a/vscode/src/sessions/sessionProfileInference.test.ts +++ b/vscode/src/sessions/sessionProfileInference.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { afterEach, describe, expect, it } from 'vitest' diff --git a/vscode/src/youtrack/api.ts b/vscode/src/youtrack/api.ts index abdb304..403f4d3 100644 --- a/vscode/src/youtrack/api.ts +++ b/vscode/src/youtrack/api.ts @@ -52,8 +52,8 @@ function normalizeBase(baseUrl: string): string { function authHeaders(token: string): Record { return { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/json', + Authorization: `Bearer ${token}`, + Accept: 'application/json', } } diff --git a/vscode/src/youtrack/issueLoader.ts b/vscode/src/youtrack/issueLoader.ts index 5c6d545..5fd6699 100644 --- a/vscode/src/youtrack/issueLoader.ts +++ b/vscode/src/youtrack/issueLoader.ts @@ -7,11 +7,11 @@ */ import type { ExtensionContext } from 'vscode' -import { workspace } from 'vscode' import type { Issue, IssueColumn } from '../gitea/types' +import type { YouTrackIssue } from './api' +import { workspace } from 'vscode' import { getYouTrackToken } from '../auth/secrets' import { getSettings } from '../settings/store' -import type { YouTrackIssue } from './api' import { issueHtmlUrl, listIssues } from './api' import { readImportedIds } from './importStore' import { findLatestState } from './stateComment' diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json index 278b278..26946ad 100644 --- a/vscode/tsconfig.json +++ b/vscode/tsconfig.json @@ -1,17 +1,17 @@ { "compilerOptions": { "target": "ES2022", + "lib": ["ES2022"], "module": "Node16", "moduleResolution": "Node16", - "lib": ["ES2022"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, - "isolatedModules": true, + "types": ["node"], + "strict": true, "noEmit": true, - "types": ["node"] + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "skipLibCheck": true }, "include": ["src/**/*.ts", "test/**/*.ts"], "exclude": ["node_modules", "dist", "webview-ui"] diff --git a/vscode/webview-ui/package.json b/vscode/webview-ui/package.json index 202f0d1..92db1db 100644 --- a/vscode/webview-ui/package.json +++ b/vscode/webview-ui/package.json @@ -1,8 +1,8 @@ { "name": "webview-ui", - "private": true, - "version": "0.0.0", "type": "module", + "version": "0.0.0", + "private": true, "scripts": { "dev": "vite build --watch", "build": "tsc --noEmit && vite build", diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index d25b05c..439139f 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -1,18 +1,20 @@ import type { Ref } from 'react' -import { forwardRef } from 'react' +import type { Issue } from '../types' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' import { Lock } from 'lucide-react' +import { forwardRef } from 'react' import { cn } from '@/lib/utils' import { Card } from './ui/card' -import type { Issue } from '../types' interface IssueCardProps { issue: Issue selected?: boolean onSelect?: (id: string) => void - /** Visual nesting level for prerequisite-chain rendering in the todo column. - * Each level adds 16px of left margin. Defaults to 0 (flat). */ + /** + * Visual nesting level for prerequisite-chain rendering in the todo column. + * Each level adds 16px of left margin. Defaults to 0 (flat). + */ depth?: number /** 锁定态:前置工单尚未完成。显示锁图标 + tooltip。 */ locked?: boolean diff --git a/vscode/webview-ui/src/components/IssueDetailPanel.tsx b/vscode/webview-ui/src/components/IssueDetailPanel.tsx index d3b2ade..0eae89c 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -1,11 +1,11 @@ import type { ReactNode } from 'react' -import { useMemo, useRef } from 'react' -import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react' import type { ClaudeProfile } from '../hooks/useIssues' import type { Issue, IssueColumn } from '../types' -import { COLUMN_LABELS, COLUMN_ORDER } from '../types' -import { isIssueLocked } from '../lib/dependencies' import type { PropertyGroup } from './property-grid' +import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react' +import { useMemo, useRef } from 'react' +import { isIssueLocked } from '../lib/dependencies' +import { COLUMN_LABELS, COLUMN_ORDER } from '../types' import { PropertyGrid } from './property-grid' /** 跨机绝对路径只比 basename(同名 .json)映射到本机 profiles 列表。 */ @@ -37,14 +37,20 @@ interface IssueDetailPanelProps { globalAutoReview: boolean onOpenInBrowser: (url: string) => void onResumeSession: (sessionId: string, profilePath?: string, cwd?: string, issueNumber?: number) => void - /** Open a new terminal that runs `codex resume ` for the auto-review - * conversation associated with this issue. */ + /** + * Open a new terminal that runs `codex resume ` for the auto-review + * conversation associated with this issue. + */ onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void - /** Open a new terminal that runs `claude --resume ` for the test - * conversation associated with this issue. */ + /** + * Open a new terminal that runs `claude --resume ` for the test + * conversation associated with this issue. + */ onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void - /** Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the - * Play button next to the "测试会话 id" row (only shown when `issue.pr` exists). */ + /** + * Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the + * Play button next to the "测试会话 id" row (only shown when `issue.pr` exists). + */ onStartTestSession: (issueNumber: number) => void /** Open a workspace-relative file in the editor. */ onOpenFile: (path: string) => void @@ -60,23 +66,31 @@ interface IssueDetailPanelProps { onOpenWorktree: (path: string) => void /** Delete the worktree (`git worktree remove`) and clear it from state. */ onDeleteWorktree: (issueNumber: number, path: string) => void - /** Pre-merge the issue's feature branch into the main worktree's current branch - * (`git merge --no-commit --no-ff `) so the user can inspect locally. */ + /** + * Pre-merge the issue's feature branch into the main worktree's current branch + * (`git merge --no-commit --no-ff `) so the user can inspect locally. + */ onMergeBranch: (issueNumber: number, branch: string) => void - /** 硬删整个工单 + 关联资源(worktree / PR / feature branch / cc tabs)。 - * 顶部垃圾桶按钮触发,扩展端做 modal confirm,可选。 */ + /** + * 硬删整个工单 + 关联资源(worktree / PR / feature branch / cc tabs)。 + * 顶部垃圾桶按钮触发,扩展端做 modal confirm,可选。 + */ onDeleteIssue?: (issueNumber: number) => void /** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */ onCloseIssue: (issueNumber: number) => void /** 将实施中 / 审查中的工单重置回待办(清 worktree / 未合并 PR / feature 分支)。 */ onResetToTodo: (issueNumber: number) => void - /** Close the matching session terminal tab. The extension watches + /** + * Close the matching session terminal tab. The extension watches * onDidCloseTerminal and clears the corresponding `*TabOpen` flag, which - * makes the X button vanish on its own. */ + * makes the X button vanish on its own. + */ onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void - /** Spawn a fresh "规划" cc tab for an issue whose sessionId is still empty + /** + * Spawn a fresh "规划" cc tab for an issue whose sessionId is still empty * (created out-of-band via spx without webhook linking). Bound to the Play - * button next to the "头脑风暴会话 id" row. */ + * button next to the "头脑风暴会话 id" row. + */ onStartBrainstormSession: (issueNumber: number) => void /** Persist a per-issue `autoReview` override into the state JSON. */ onUpdateAutoReview: (issueNumber: number, value: boolean) => void @@ -649,7 +663,11 @@ export function IssueDetailPanel({ {issue.source === 'youtrack' && issue.attachments && issue.attachments.length > 0 && (
- 附件({issue.attachments.length}) + + 附件( + {issue.attachments.length} + ) +
{issue.attachments.map(att => (