🎨 style(vscode): eslint 自动修复格式与排序
Claude-Session: https://claude.ai/code/session_01HTMgwaE3woqM5yvsHiuSAa
This commit is contained in:
1 parent
df35bcfd49
commit
8c22a29ce5
41 files changed
+248
-167
No files matched your search
+3
-3
@@ -107,6 +107,9 @@
|
|||||||
"ext:package": "pnpm build && vsce package --no-dependencies",
|
"ext:package": "pnpm build && vsce package --no-dependencies",
|
||||||
"ext:publish:vsce": "npm_config_proxy= npm_config_https_proxy= vsce publish --skip-duplicate"
|
"ext:publish:vsce": "npm_config_proxy= npm_config_https_proxy= vsce publish --skip-duplicate"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"diff": "^9.0.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^6.7.3",
|
"@antfu/eslint-config": "^6.7.3",
|
||||||
"@types/diff": "^8.0.0",
|
"@types/diff": "^8.0.0",
|
||||||
@@ -120,8 +123,5 @@
|
|||||||
"ovsx": "^0.10.7",
|
"ovsx": "^0.10.7",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vitest": "^4.0.16"
|
"vitest": "^4.0.16"
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"diff": "^9.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
shellEmulator: true
|
||||||
|
|
||||||
|
trustPolicy: no-downgrade
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
- webview-ui
|
- webview-ui
|
||||||
|
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ spx 自动找到对应行替换或追加,保留所有其他 marker。**不要
|
|||||||
- 不要创建分支
|
- 不要创建分支
|
||||||
- 不要切换分支
|
- 不要切换分支
|
||||||
- 不要修改当前主 worktree 所在分支
|
- 不要修改当前主 worktree 所在分支
|
||||||
- spec/plan 一律在 main 分支(当前主 worktree)创建
|
- spec/plan 一律在 main 分支(当前主 worktree)创建
|
||||||
@@ -10,4 +10,4 @@
|
|||||||
- 生成后立即 `git add` 提交迁移文件,纳入 PR。
|
- 生成后立即 `git add` 提交迁移文件,纳入 PR。
|
||||||
- **绝不**对共享 dev DB(`192.168.1.4:5433`)或 prod 执行 `alembic upgrade` / `downgrade` 等任何改库命令。
|
- **绝不**对共享 dev DB(`192.168.1.4:5433`)或 prod 执行 `alembic upgrade` / `downgrade` 等任何改库命令。
|
||||||
- 需要运行时验证迁移,就起一个一次性独立库(本地 docker postgres 或唯一命名的 scratch 库),在它上面 upgrade,验证完即丢弃,不要留痕。
|
- 需要运行时验证迁移,就起一个一次性独立库(本地 docker postgres 或唯一命名的 scratch 库),在它上面 upgrade,验证完即丢弃,不要留痕。
|
||||||
- 共享 dev DB 与 prod 的迁移合并后由用户统一执行,时机由用户决定。
|
- 共享 dev DB 与 prod 的迁移合并后由用户统一执行,时机由用户决定。
|
||||||
@@ -24,8 +24,10 @@ export async function deleteToken(ctx: ExtensionContext, host: string): Promise<
|
|||||||
await ctx.secrets.delete(tokenKey(host))
|
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 {
|
export function youtrackTokenKey(host: string): string {
|
||||||
return `youtrack-token:${host}`
|
return `youtrack-token:${host}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,24 +34,24 @@ describe('resolveClaudeBin', () => {
|
|||||||
expect(resolveClaudeBin({ PATH: dir }, '/no-such-home')).toBe(bin)
|
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 home = tempDir()
|
||||||
const bin = touchClaude(path.join(home, '.local', 'bin'))
|
const bin = touchClaude(path.join(home, '.local', 'bin'))
|
||||||
expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin)
|
expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PATH 未命中时回退到 $HOME/.claude/local/claude', () => {
|
it('pATH 未命中时回退到 $HOME/.claude/local/claude', () => {
|
||||||
const home = tempDir()
|
const home = tempDir()
|
||||||
const bin = touchClaude(path.join(home, '.claude', 'local'))
|
const bin = touchClaude(path.join(home, '.claude', 'local'))
|
||||||
expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin)
|
expect(resolveClaudeBin({ PATH: '' }, home)).toBe(bin)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PATH 与候选皆空 → null', () => {
|
it('pATH 与候选皆空 → null', () => {
|
||||||
const home = tempDir()
|
const home = tempDir()
|
||||||
expect(resolveClaudeBin({ PATH: '' }, home)).toBeNull()
|
expect(resolveClaudeBin({ PATH: '' }, home)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('PATH 中目录不存在时跳过并继续', () => {
|
it('pATH 中目录不存在时跳过并继续', () => {
|
||||||
const home = tempDir()
|
const home = tempDir()
|
||||||
const bin = touchClaude(path.join(home, '.local', 'bin'))
|
const bin = touchClaude(path.join(home, '.local', 'bin'))
|
||||||
expect(resolveClaudeBin({ PATH: '/definitely/missing/bin' }, home)).toBe(bin)
|
expect(resolveClaudeBin({ PATH: '/definitely/missing/bin' }, home)).toBe(bin)
|
||||||
|
|||||||
@@ -169,13 +169,11 @@ export async function watchForNewCodexSession(opts: {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
watcher.on('error', (err) => {
|
watcher.on('error', (err) => {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] codexSessionWatcher: watch error', err)
|
console.warn('[superpowers] codexSessionWatcher: watch error', err)
|
||||||
done(null)
|
done(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] codexSessionWatcher: fs.watch threw', err)
|
console.warn('[superpowers] codexSessionWatcher: fs.watch threw', err)
|
||||||
done(null)
|
done(null)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ describe('resolveCommitProfileForRun', () => {
|
|||||||
const result = resolveCommitProfileForRun(
|
const result = resolveCommitProfileForRun(
|
||||||
missing,
|
missing,
|
||||||
p => p,
|
p => p,
|
||||||
p => {
|
(p) => {
|
||||||
try {
|
try {
|
||||||
accessSync(p, constants.R_OK)
|
accessSync(p, constants.R_OK)
|
||||||
return true
|
return true
|
||||||
@@ -77,7 +77,7 @@ describe('resolveCommitProfileForRun', () => {
|
|||||||
const result = resolveCommitProfileForRun(
|
const result = resolveCommitProfileForRun(
|
||||||
file,
|
file,
|
||||||
p => p,
|
p => p,
|
||||||
p => {
|
(p) => {
|
||||||
try {
|
try {
|
||||||
accessSync(p, constants.R_OK)
|
accessSync(p, constants.R_OK)
|
||||||
return true
|
return true
|
||||||
@@ -98,7 +98,7 @@ describe('resolveCommitProfileForRun', () => {
|
|||||||
const result = resolveCommitProfileForRun(
|
const result = resolveCommitProfileForRun(
|
||||||
foreign,
|
foreign,
|
||||||
() => local,
|
() => local,
|
||||||
p => {
|
(p) => {
|
||||||
try {
|
try {
|
||||||
accessSync(p, constants.R_OK)
|
accessSync(p, constants.R_OK)
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import type { PrDiffMode } from './prDiffMode'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import {
|
import {
|
||||||
DEFAULT_PR_DIFF_MODE,
|
DEFAULT_PR_DIFF_MODE,
|
||||||
isPrDiffMode,
|
isPrDiffMode,
|
||||||
parsePrDiffMode,
|
parsePrDiffMode,
|
||||||
type PrDiffMode,
|
|
||||||
} from './prDiffMode'
|
} from './prDiffMode'
|
||||||
|
|
||||||
describe('DEFAULT_PR_DIFF_MODE', () => {
|
describe('dEFAULT_PR_DIFF_MODE', () => {
|
||||||
it('默认是 unified', () => {
|
it('默认是 unified', () => {
|
||||||
expect(DEFAULT_PR_DIFF_MODE).toBe('unified')
|
expect(DEFAULT_PR_DIFF_MODE).toBe('unified')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 os from 'node:os'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export async function watchForNewSession(opts: {
|
|||||||
snapshot = new Set(existing.filter(n => n.endsWith('.jsonl')))
|
snapshot = new Set(existing.filter(n => n.endsWith('.jsonl')))
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] sessionWatcher: readdir failed', err)
|
console.warn('[superpowers] sessionWatcher: readdir failed', err)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -89,13 +88,11 @@ export async function watchForNewSession(opts: {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
watcher.on('error', (err) => {
|
watcher.on('error', (err) => {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] sessionWatcher: watch error', err)
|
console.warn('[superpowers] sessionWatcher: watch error', err)
|
||||||
done(null)
|
done(null)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] sessionWatcher: fs.watch threw', err)
|
console.warn('[superpowers] sessionWatcher: fs.watch threw', err)
|
||||||
done(null)
|
done(null)
|
||||||
return
|
return
|
||||||
@@ -131,7 +128,6 @@ export async function pollForNewSession(opts: {
|
|||||||
snapshot = new Set(existing.filter(n => n.endsWith('.jsonl')))
|
snapshot = new Set(existing.filter(n => n.endsWith('.jsonl')))
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.warn('[superpowers] sessionWatcher: poll readdir failed', err)
|
console.warn('[superpowers] sessionWatcher: poll readdir failed', err)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ function extractClaudePayload(stdout: string): ParsedClaudeJson | null {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function truncateDiagnostic(text: string): string {
|
function truncateDiagnostic(text: string): string {
|
||||||
if (text.length <= EXIT_DIAGNOSTIC_MAX_CHARS)
|
if (text.length <= EXIT_DIAGNOSTIC_MAX_CHARS)
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ import { RelativePattern, workspace } from 'vscode'
|
|||||||
const ENV_PATTERN = '**/.env*'
|
const ENV_PATTERN = '**/.env*'
|
||||||
const EXCLUDE_PATTERN = '{**/node_modules/**,**/.git/**,**/.claude/**}'
|
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
|
* 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<string[]> {
|
export async function findEnvFiles(workspaceRoot: string): Promise<string[]> {
|
||||||
const folder = workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot)
|
const folder = workspace.workspaceFolders?.find(f => f.uri.fsPath === workspaceRoot)
|
||||||
if (!folder)
|
if (!folder)
|
||||||
|
|||||||
@@ -15,14 +15,18 @@
|
|||||||
import { execFile } from 'node:child_process'
|
import { execFile } from 'node:child_process'
|
||||||
|
|
||||||
export interface BranchSyncStatus {
|
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
|
* 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
|
behind: number
|
||||||
devBranch: string
|
devBranch: string
|
||||||
autoBuildBranch: 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
|
unavailable?: boolean
|
||||||
/** Human-readable explanation, suitable for the button's tooltip. */
|
/** Human-readable explanation, suitable for the button's tooltip. */
|
||||||
reason?: string
|
reason?: string
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ export interface HookContext {
|
|||||||
branch: string
|
branch: string
|
||||||
issueNumber: number
|
issueNumber: number
|
||||||
mainBranch: string
|
mainBranch: string
|
||||||
/** User-configured override path. Empty / undefined → use the default
|
/**
|
||||||
* `<workspaceRoot>/.spx/worktree-*.sh` location. */
|
* User-configured override path. Empty / undefined → use the default
|
||||||
|
* `<workspaceRoot>/.spx/worktree-*.sh` location.
|
||||||
|
*/
|
||||||
customScriptPath?: string
|
customScriptPath?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+56
-28
@@ -8,26 +8,36 @@
|
|||||||
export type IssueColumn = 'todo' | 'in-progress' | 'review' | 'done'
|
export type IssueColumn = 'todo' | 'in-progress' | 'review' | 'done'
|
||||||
|
|
||||||
export interface Issue {
|
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
|
* `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
|
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'
|
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
|
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
|
* numeric tail of `externalId` (collides across sources — never use alone as
|
||||||
* a key, pair with `source`). */
|
* a key, pair with `source`).
|
||||||
|
*/
|
||||||
number: number
|
number: number
|
||||||
title: string
|
title: string
|
||||||
column: IssueColumn
|
column: IssueColumn
|
||||||
/** YouTrack attachments (name + absolute URL). Links only; not downloaded. */
|
/** YouTrack attachments (name + absolute URL). Links only; not downloaded. */
|
||||||
attachments?: Array<{ name: string, url: string }>
|
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
|
sessionId?: string
|
||||||
/** 实施会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
/** 实施会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
||||||
profilePath?: string
|
profilePath?: string
|
||||||
@@ -35,21 +45,27 @@ export interface Issue {
|
|||||||
brainstormProfilePath?: string
|
brainstormProfilePath?: string
|
||||||
/** 测试会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
/** 测试会话专用的 Claude 配置文件;空 = DEFAULT_PROFILE_PATH,不回退其它字段。 */
|
||||||
testProfilePath?: string
|
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
|
* surfaced from the Claude session transcript. Lives under
|
||||||
* `docs/superpowers/specs/*.md`. */
|
* `docs/superpowers/specs/*.md`.
|
||||||
|
*/
|
||||||
specFile?: string
|
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
|
* surfaced from the Claude session transcript. Lives under
|
||||||
* `docs/superpowers/plans/*.md`. */
|
* `docs/superpowers/plans/*.md`.
|
||||||
|
*/
|
||||||
planFile?: string
|
planFile?: string
|
||||||
/** Optional workspace-relative path to the generated PR diff summary. */
|
/** Optional workspace-relative path to the generated PR diff summary. */
|
||||||
prDiffFile?: string
|
prDiffFile?: string
|
||||||
/** PR number associated with this issue (set after webhook fires). */
|
/** PR number associated with this issue (set after webhook fires). */
|
||||||
pr?: string
|
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
|
* 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
|
prMerged?: boolean
|
||||||
/** PR 合并时间(ISO 8601,来自 PR API 的 merged_at)。完成列按它降序排序,未合并为 undefined。合并时持久化进 state JSON;完成工单加载时不再实时拉 PR,直接读持久化值(缺失则按工单号回退排序)。 */
|
/** PR 合并时间(ISO 8601,来自 PR API 的 merged_at)。完成列按它降序排序,未合并为 undefined。合并时持久化进 state JSON;完成工单加载时不再实时拉 PR,直接读持久化值(缺失则按工单号回退排序)。 */
|
||||||
prMergedAt?: string
|
prMergedAt?: string
|
||||||
@@ -61,32 +77,44 @@ export interface Issue {
|
|||||||
worktreeExists?: boolean
|
worktreeExists?: boolean
|
||||||
/** Lifecycle of the implementation flow. */
|
/** Lifecycle of the implementation flow. */
|
||||||
implementStatus?: 'running' | 'done' | 'failed'
|
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
|
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`
|
* the first time auto-review runs; reused for subsequent `synchronize`
|
||||||
* webhook callbacks via `codex exec resume`. */
|
* webhook callbacks via `codex exec resume`.
|
||||||
|
*/
|
||||||
reviewSessionId?: string
|
reviewSessionId?: string
|
||||||
/** Whether the codex session file (`~/.codex/sessions/YYYY/MM/DD/rollout-...-<id>.jsonl`)
|
/**
|
||||||
|
* Whether the codex session file (`~/.codex/sessions/YYYY/MM/DD/rollout-...-<id>.jsonl`)
|
||||||
* for `reviewSessionId` exists on this machine; computed on load, never
|
* for `reviewSessionId` exists on this machine; computed on load, never
|
||||||
* persisted. codex resume 只依赖这个文件,与 worktree 无关。 */
|
* persisted. codex resume 只依赖这个文件,与 worktree 无关。
|
||||||
|
*/
|
||||||
reviewSessionFileExists?: boolean
|
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
|
* 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
|
testSessionId?: string
|
||||||
/** Browser URL to the Gitea issue page (escape hatch button in the UI). */
|
/** Browser URL to the Gitea issue page (escape hatch button in the UI). */
|
||||||
htmlUrl: string
|
htmlUrl: string
|
||||||
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
||||||
prerequisite?: number
|
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
|
* 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
|
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
|
* 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
|
autoReview?: boolean
|
||||||
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||||
assignees?: string[]
|
assignees?: string[]
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ class Logger {
|
|||||||
|
|
||||||
const line = `[${full.source}] ${full.message}`
|
const line = `[${full.source}] ${full.message}`
|
||||||
// Mirror to Extension Host console for parity with existing console.warn/error.
|
// Mirror to Extension Host console for parity with existing console.warn/error.
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
if (full.level === 'error')
|
if (full.level === 'error')
|
||||||
console.error(line, full.details ?? '')
|
console.error(line, full.details ?? '')
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
else if (full.level === 'warn')
|
else if (full.level === 'warn')
|
||||||
console.warn(line, full.details ?? '')
|
console.warn(line, full.details ?? '')
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
else
|
else
|
||||||
console.log(line, full.details ?? '')
|
console.log(line, full.details ?? '')
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type { KanbanWebviewPanel } from '../KanbanPanel'
|
|||||||
import { promises as fsp } from 'node:fs'
|
import { promises as fsp } from 'node:fs'
|
||||||
import { window, workspace } from 'vscode'
|
import { window, workspace } from 'vscode'
|
||||||
import { buildCcCommand } from '../../cc/ccCommand'
|
import { buildCcCommand } from '../../cc/ccCommand'
|
||||||
|
import { resolveProfilePath } from '../../cc/profiles'
|
||||||
import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher'
|
import { pollForNewSession, projectsDirFor } from '../../cc/sessionWatcher'
|
||||||
import { logger } from '../../logging/logger'
|
import { logger } from '../../logging/logger'
|
||||||
import { listImportableNamedSessions } from '../../sessions/importableNamedSessions'
|
import { listImportableNamedSessions } from '../../sessions/importableNamedSessions'
|
||||||
import { decideManagedResumeProfile } from '../../sessions/managedResumeProfile'
|
import { decideManagedResumeProfile } from '../../sessions/managedResumeProfile'
|
||||||
import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore'
|
import { readManagedSessions, writeManagedSessions } from '../../sessions/managedStore'
|
||||||
import { inferProfilePathForSession } from '../../sessions/sessionProfileInference'
|
import { inferProfilePathForSession } from '../../sessions/sessionProfileInference'
|
||||||
import { resolveProfilePath } from '../../cc/profiles'
|
|
||||||
import { resolveTerminalLocation } from './terminals'
|
import { resolveTerminalLocation } from './terminals'
|
||||||
|
|
||||||
/** 默认会话名:有 prompt 取前 20 字符,否则用短 id(前 8 位)。 */
|
/** 默认会话名:有 prompt 取前 20 字符,否则用短 id(前 8 位)。 */
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { IssueColumn } from '../../gitea/types'
|
import type { IssueColumn } from '../../gitea/types'
|
||||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
|
||||||
import type { YouTrackAuth } from '../../youtrack/api'
|
import type { YouTrackAuth } from '../../youtrack/api'
|
||||||
|
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||||
import { ThemeIcon, window, workspace } from 'vscode'
|
import { ThemeIcon, window, workspace } from 'vscode'
|
||||||
import { getYouTrackToken } from '../../auth/secrets'
|
import { getYouTrackToken } from '../../auth/secrets'
|
||||||
import { logger } from '../../logging/logger'
|
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 })
|
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<YouTrackAuth | null> {
|
async function resolveAuth(panel: KanbanWebviewPanel): Promise<YouTrackAuth | null> {
|
||||||
const baseUrl = getSettings(panel.context).youtrackBaseUrl.trim()
|
const baseUrl = getSettings(panel.context).youtrackBaseUrl.trim()
|
||||||
if (!baseUrl)
|
if (!baseUrl)
|
||||||
@@ -61,8 +63,10 @@ export async function handleYouTrackColumnChange(panel: KanbanWebviewPanel, exte
|
|||||||
await panel.loadAndPush()
|
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<void> {
|
async function closeYouTrackIssue(panel: KanbanWebviewPanel, auth: YouTrackAuth, externalId: string): Promise<void> {
|
||||||
const configured = getSettings(panel.context).youtrackCloseCommand.trim()
|
const configured = getSettings(panel.context).youtrackCloseCommand.trim()
|
||||||
const command = configured || (await resolvedStateCommand(auth, externalId))
|
const command = configured || (await resolvedStateCommand(auth, externalId))
|
||||||
@@ -74,8 +78,10 @@ async function closeYouTrackIssue(panel: KanbanWebviewPanel, auth: YouTrackAuth,
|
|||||||
await applyCommand(auth, externalId, command)
|
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<void> {
|
export async function handleListProjects(panel: KanbanWebviewPanel, baseUrl: string, token: string): Promise<void> {
|
||||||
const trimmedBase = baseUrl.trim()
|
const trimmedBase = baseUrl.trim()
|
||||||
const trimmedToken = token.trim()
|
const trimmedToken = token.trim()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function pickRandomIssueColor(): string {
|
|||||||
* `isNew` is true the caller is expected to persist the picked color back to
|
* `isNew` is true the caller is expected to persist the picked color back to
|
||||||
* the issue's state JSON so subsequent sessions reuse it.
|
* 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))
|
if (stored && (PALETTE as readonly string[]).includes(stored))
|
||||||
return { id: stored, isNew: false }
|
return { id: stored, isNew: false }
|
||||||
return { id: pickRandomIssueColor(), isNew: true }
|
return { id: pickRandomIssueColor(), isNew: true }
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ export function decideManagedResumeProfile(input: {
|
|||||||
const override = input.overrideProfile?.trim() || undefined
|
const override = input.overrideProfile?.trim() || undefined
|
||||||
const stored = input.storedProfile?.trim() || undefined
|
const stored = input.storedProfile?.trim() || undefined
|
||||||
|
|
||||||
if (override) { // ① 行内覆盖:与 store 不同才写回;忽略 jsonl 推断
|
if (override) { // ① 行内覆盖:与 store 不同才写回;忽略 jsonl 推断
|
||||||
return {
|
return {
|
||||||
effective: override,
|
effective: override,
|
||||||
persist: override !== stored ? override : undefined,
|
persist: override !== stored ? override : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (stored) // ② store 已有 → 沿用,不写回
|
if (stored) // ② store 已有 → 沿用,不写回
|
||||||
return { effective: stored, persist: undefined }
|
return { effective: stored, persist: undefined }
|
||||||
if (input.inferredProfile) // ③ 缺 profile 才用 jsonl 回填并写回
|
if (input.inferredProfile) // ③ 缺 profile 才用 jsonl 回填并写回
|
||||||
return { effective: input.inferredProfile, persist: input.inferredProfile }
|
return { effective: input.inferredProfile, persist: input.inferredProfile }
|
||||||
return { effective: undefined, persist: undefined }
|
return { effective: undefined, persist: undefined }
|
||||||
}
|
}
|
||||||
@@ -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 os from 'node:os'
|
||||||
import * as path from 'node:path'
|
import * as path from 'node:path'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ function normalizeBase(baseUrl: string): string {
|
|||||||
|
|
||||||
function authHeaders(token: string): Record<string, string> {
|
function authHeaders(token: string): Record<string, string> {
|
||||||
return {
|
return {
|
||||||
'Authorization': `Bearer ${token}`,
|
Authorization: `Bearer ${token}`,
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ExtensionContext } from 'vscode'
|
import type { ExtensionContext } from 'vscode'
|
||||||
import { workspace } from 'vscode'
|
|
||||||
import type { Issue, IssueColumn } from '../gitea/types'
|
import type { Issue, IssueColumn } from '../gitea/types'
|
||||||
|
import type { YouTrackIssue } from './api'
|
||||||
|
import { workspace } from 'vscode'
|
||||||
import { getYouTrackToken } from '../auth/secrets'
|
import { getYouTrackToken } from '../auth/secrets'
|
||||||
import { getSettings } from '../settings/store'
|
import { getSettings } from '../settings/store'
|
||||||
import type { YouTrackIssue } from './api'
|
|
||||||
import { issueHtmlUrl, listIssues } from './api'
|
import { issueHtmlUrl, listIssues } from './api'
|
||||||
import { readImportedIds } from './importStore'
|
import { readImportedIds } from './importStore'
|
||||||
import { findLatestState } from './stateComment'
|
import { findLatestState } from './stateComment'
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
"module": "Node16",
|
"module": "Node16",
|
||||||
"moduleResolution": "Node16",
|
"moduleResolution": "Node16",
|
||||||
"lib": ["ES2022"],
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"types": ["node"],
|
||||||
|
"strict": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"types": ["node"]
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "test/**/*.ts"],
|
"include": ["src/**/*.ts", "test/**/*.ts"],
|
||||||
"exclude": ["node_modules", "dist", "webview-ui"]
|
"exclude": ["node_modules", "dist", "webview-ui"]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "webview-ui",
|
"name": "webview-ui",
|
||||||
"private": true,
|
|
||||||
"version": "0.0.0",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite build --watch",
|
"dev": "vite build --watch",
|
||||||
"build": "tsc --noEmit && vite build",
|
"build": "tsc --noEmit && vite build",
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import type { Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import { forwardRef } from 'react'
|
import type { Issue } from '../types'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { Lock } from 'lucide-react'
|
import { Lock } from 'lucide-react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Card } from './ui/card'
|
import { Card } from './ui/card'
|
||||||
import type { Issue } from '../types'
|
|
||||||
|
|
||||||
interface IssueCardProps {
|
interface IssueCardProps {
|
||||||
issue: Issue
|
issue: Issue
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
onSelect?: (id: string) => void
|
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
|
depth?: number
|
||||||
/** 锁定态:前置工单尚未完成。显示锁图标 + tooltip。 */
|
/** 锁定态:前置工单尚未完成。显示锁图标 + tooltip。 */
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { ReactNode } from 'react'
|
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 { ClaudeProfile } from '../hooks/useIssues'
|
||||||
import type { Issue, IssueColumn } from '../types'
|
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 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'
|
import { PropertyGrid } from './property-grid'
|
||||||
|
|
||||||
/** 跨机绝对路径只比 basename(同名 .json)映射到本机 profiles 列表。 */
|
/** 跨机绝对路径只比 basename(同名 .json)映射到本机 profiles 列表。 */
|
||||||
@@ -37,14 +37,20 @@ interface IssueDetailPanelProps {
|
|||||||
globalAutoReview: boolean
|
globalAutoReview: boolean
|
||||||
onOpenInBrowser: (url: string) => void
|
onOpenInBrowser: (url: string) => void
|
||||||
onResumeSession: (sessionId: string, profilePath?: string, cwd?: string, issueNumber?: number) => void
|
onResumeSession: (sessionId: string, profilePath?: string, cwd?: string, issueNumber?: number) => void
|
||||||
/** Open a new terminal that runs `codex resume <id>` for the auto-review
|
/**
|
||||||
* conversation associated with this issue. */
|
* Open a new terminal that runs `codex resume <id>` for the auto-review
|
||||||
|
* conversation associated with this issue.
|
||||||
|
*/
|
||||||
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||||
/** Open a new terminal that runs `claude --resume <id>` for the test
|
/**
|
||||||
* conversation associated with this issue. */
|
* Open a new terminal that runs `claude --resume <id>` for the test
|
||||||
|
* conversation associated with this issue.
|
||||||
|
*/
|
||||||
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
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
|
onStartTestSession: (issueNumber: number) => void
|
||||||
/** Open a workspace-relative file in the editor. */
|
/** Open a workspace-relative file in the editor. */
|
||||||
onOpenFile: (path: string) => void
|
onOpenFile: (path: string) => void
|
||||||
@@ -60,23 +66,31 @@ interface IssueDetailPanelProps {
|
|||||||
onOpenWorktree: (path: string) => void
|
onOpenWorktree: (path: string) => void
|
||||||
/** Delete the worktree (`git worktree remove`) and clear it from state. */
|
/** Delete the worktree (`git worktree remove`) and clear it from state. */
|
||||||
onDeleteWorktree: (issueNumber: number, path: string) => void
|
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 <branch>`) 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 <branch>`) so the user can inspect locally.
|
||||||
|
*/
|
||||||
onMergeBranch: (issueNumber: number, branch: string) => void
|
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
|
onDeleteIssue?: (issueNumber: number) => void
|
||||||
/** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */
|
/** 关闭 Gitea 工单,不清理本地会话、worktree、PR 或分支。 */
|
||||||
onCloseIssue: (issueNumber: number) => void
|
onCloseIssue: (issueNumber: number) => void
|
||||||
/** 将实施中 / 审查中的工单重置回待办(清 worktree / 未合并 PR / feature 分支)。 */
|
/** 将实施中 / 审查中的工单重置回待办(清 worktree / 未合并 PR / feature 分支)。 */
|
||||||
onResetToTodo: (issueNumber: number) => void
|
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
|
* 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
|
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
|
* (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
|
onStartBrainstormSession: (issueNumber: number) => void
|
||||||
/** Persist a per-issue `autoReview` override into the state JSON. */
|
/** Persist a per-issue `autoReview` override into the state JSON. */
|
||||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||||
@@ -649,7 +663,11 @@ export function IssueDetailPanel({
|
|||||||
|
|
||||||
{issue.source === 'youtrack' && issue.attachments && issue.attachments.length > 0 && (
|
{issue.source === 'youtrack' && issue.attachments && issue.attachments.length > 0 && (
|
||||||
<div className="flex flex-col gap-1 text-xs">
|
<div className="flex flex-col gap-1 text-xs">
|
||||||
<span className="opacity-60">附件({issue.attachments.length})</span>
|
<span className="opacity-60">
|
||||||
|
附件(
|
||||||
|
{issue.attachments.length}
|
||||||
|
)
|
||||||
|
</span>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{issue.attachments.map(att => (
|
{issue.attachments.map(att => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
import type { Issue, IssueColumn } from '../types'
|
||||||
import { useDroppable } from '@dnd-kit/core'
|
import { useDroppable } from '@dnd-kit/core'
|
||||||
import { Plus } from 'lucide-react'
|
import { Plus } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Badge } from './ui/badge'
|
|
||||||
import type { Issue, IssueColumn } from '../types'
|
|
||||||
import { COLUMN_LABELS } from '../types'
|
import { COLUMN_LABELS } from '../types'
|
||||||
|
import { Badge } from './ui/badge'
|
||||||
|
|
||||||
interface KanbanColumnProps {
|
interface KanbanColumnProps {
|
||||||
column: IssueColumn
|
column: IssueColumn
|
||||||
|
|||||||
@@ -12,9 +12,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ReactElement } from 'react'
|
import type { ReactElement } from 'react'
|
||||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
|
||||||
import { Copy, Trash2, X } from 'lucide-react'
|
|
||||||
import type { LogEntry } from '../lib/messages'
|
import type { LogEntry } from '../lib/messages'
|
||||||
|
import { Copy, Trash2, X } from 'lucide-react'
|
||||||
|
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
export interface LogModalProps {
|
export interface LogModalProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { getPastedImageFiles, readClipboardImages } from '../lib/imagePaste'
|
import { getPastedImageFiles, readClipboardImages } from '../lib/imagePaste'
|
||||||
|
|
||||||
export interface PastedImage {
|
export interface PastedImage {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
import type { PastedImage } from '../lib/imagePaste'
|
import type { PastedImage } from '../lib/imagePaste'
|
||||||
import { X } from 'lucide-react'
|
import { X } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { getPastedImageFiles, readClipboardImages } from '../lib/imagePaste'
|
|
||||||
import { useProfileAssets } from '../hooks/useProfileAssets'
|
import { useProfileAssets } from '../hooks/useProfileAssets'
|
||||||
|
import { getPastedImageFiles, readClipboardImages } from '../lib/imagePaste'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
keyLabel: string
|
keyLabel: string
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react'
|
|
||||||
import { CheckCircle2, Loader2, X, XCircle } from 'lucide-react'
|
import { CheckCircle2, Loader2, X, XCircle } from 'lucide-react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export interface ToastItem {
|
export interface ToastItem {
|
||||||
id: string
|
id: string
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export function statusColorVar(status: string): string {
|
|||||||
|
|
||||||
/** PR diff 里跳过测试代码:路径任一段为 `tests` 即视为忽略(匹配 `tests/…` 或 `…/tests/…`)。 */
|
/** PR diff 里跳过测试代码:路径任一段为 `tests` 即视为忽略(匹配 `tests/…` 或 `…/tests/…`)。 */
|
||||||
export function isIgnoredPath(path: string): boolean {
|
export function isIgnoredPath(path: string): boolean {
|
||||||
return path.split('/').some(seg => seg === 'tests')
|
return path.split('/').includes('tests')
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ import { SelectMenu } from './ui/select-menu'
|
|||||||
// --- 类型定义 ---
|
// --- 类型定义 ---
|
||||||
|
|
||||||
/** 字段类型枚举 */
|
/** 字段类型枚举 */
|
||||||
export type FieldType =
|
export type FieldType
|
||||||
| 'string'
|
= | 'string'
|
||||||
| 'multiline'
|
| 'multiline'
|
||||||
| 'number'
|
| 'number'
|
||||||
| 'boolean'
|
| 'boolean'
|
||||||
| 'select'
|
| 'select'
|
||||||
| 'color'
|
| 'color'
|
||||||
| 'theme-color'
|
| 'theme-color'
|
||||||
| 'password'
|
| 'password'
|
||||||
| 'action'
|
| 'action'
|
||||||
| 'file-link'
|
| 'file-link'
|
||||||
| 'pr-link'
|
| 'pr-link'
|
||||||
|
|
||||||
/** 属性定义 */
|
/** 属性定义 */
|
||||||
export interface PropertyDef {
|
export interface PropertyDef {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cva, type VariantProps } from 'class-variance-authority'
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { type ClassValue, clsx } from 'clsx'
|
import type { ClassValue } from 'clsx'
|
||||||
|
import { clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]): string {
|
export function cn(...inputs: ClassValue[]): string {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class ErrorBoundary extends React.Component<{ children: React.ReactNode }, Bound
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidCatch(error: Error, info: React.ErrorInfo): void {
|
componentDidCatch(error: Error, info: React.ErrorInfo): void {
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.error('[superpowers webview] render crash:', error, info.componentStack)
|
console.error('[superpowers webview] render crash:', error, info.componentStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,32 @@ export interface Issue {
|
|||||||
column: IssueColumn
|
column: IssueColumn
|
||||||
/** YouTrack attachments (name + absolute URL); links only. */
|
/** YouTrack attachments (name + absolute URL); links only. */
|
||||||
attachments?: Array<{ name: string, url: string }>
|
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
|
sessionId?: string
|
||||||
/** Absolute path to the Claude settings profile used at creation; passed
|
/**
|
||||||
* as --settings on resume. */
|
* Absolute path to the Claude settings profile used at creation; passed
|
||||||
|
* as --settings on resume.
|
||||||
|
*/
|
||||||
/** 实施会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
/** 实施会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
||||||
profilePath?: string
|
profilePath?: string
|
||||||
/** 头脑风暴会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
/** 头脑风暴会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
||||||
brainstormProfilePath?: string
|
brainstormProfilePath?: string
|
||||||
/** 测试会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
/** 测试会话专用的 Claude 配置文件;空 = 默认,不回退其它字段。 */
|
||||||
testProfilePath?: string
|
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
|
* surfaced from the Claude session transcript. Lives under
|
||||||
* `docs/superpowers/specs/*.md`. */
|
* `docs/superpowers/specs/*.md`.
|
||||||
|
*/
|
||||||
specFile?: string
|
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
|
* surfaced from the Claude session transcript. Lives under
|
||||||
* `docs/superpowers/plans/*.md`. */
|
* `docs/superpowers/plans/*.md`.
|
||||||
|
*/
|
||||||
planFile?: string
|
planFile?: string
|
||||||
/** Optional workspace-relative path to the generated PR diff summary. */
|
/** Optional workspace-relative path to the generated PR diff summary. */
|
||||||
prDiffFile?: string
|
prDiffFile?: string
|
||||||
@@ -46,24 +54,32 @@ export interface Issue {
|
|||||||
worktreeExists?: boolean
|
worktreeExists?: boolean
|
||||||
/** Lifecycle of the implementation flow. */
|
/** Lifecycle of the implementation flow. */
|
||||||
implementStatus?: 'running' | 'done' | 'failed'
|
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
|
implementSessionId?: string
|
||||||
/** Backend-agnostic review session id (v1 stores a codex thread id). */
|
/** Backend-agnostic review session id (v1 stores a codex thread id). */
|
||||||
reviewSessionId?: string
|
reviewSessionId?: string
|
||||||
/** 本机是否存在 reviewSessionId 对应的 codex 会话文件;扩展端加载时计算,
|
/**
|
||||||
* 不持久化。codex resume 只依赖这个文件,与 worktree 无关。 */
|
* 本机是否存在 reviewSessionId 对应的 codex 会话文件;扩展端加载时计算,
|
||||||
|
* 不持久化。codex resume 只依赖这个文件,与 worktree 无关。
|
||||||
|
*/
|
||||||
reviewSessionFileExists?: boolean
|
reviewSessionFileExists?: boolean
|
||||||
/** Claude Code session id of the test conversation; manually started after
|
/**
|
||||||
* the PR is merged, resumable from the detail panel. */
|
* Claude Code session id of the test conversation; manually started after
|
||||||
|
* the PR is merged, resumable from the detail panel.
|
||||||
|
*/
|
||||||
testSessionId?: string
|
testSessionId?: string
|
||||||
htmlUrl: string
|
htmlUrl: string
|
||||||
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
/** 前置任务 issue number (Gitea dependencies 取第一个) */
|
||||||
prerequisite?: number
|
prerequisite?: number
|
||||||
/** Terminal/tab color id chosen on first session open, persisted in state JSON. */
|
/** Terminal/tab color id chosen on first session open, persisted in state JSON. */
|
||||||
color?: string
|
color?: string
|
||||||
/** Per-issue override of the global `autoReview` setting. Undefined = follow
|
/**
|
||||||
* global setting; true/false = explicit override stored in state JSON. */
|
* Per-issue override of the global `autoReview` setting. Undefined = follow
|
||||||
|
* global setting; true/false = explicit override stored in state JSON.
|
||||||
|
*/
|
||||||
autoReview?: boolean
|
autoReview?: boolean
|
||||||
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
/** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */
|
||||||
assignees?: string[]
|
assignees?: string[]
|
||||||
@@ -71,8 +87,10 @@ export interface Issue {
|
|||||||
handoffAttachmentId?: string
|
handoffAttachmentId?: string
|
||||||
/** 发起移交的 Gitea login。 */
|
/** 发起移交的 Gitea login。 */
|
||||||
handoffFrom?: string
|
handoffFrom?: string
|
||||||
/** 三类会话 tab 是否当前在编辑器分组里打开(由 extension 通过 issue/patch 同步)。
|
/**
|
||||||
* 仅用于详情面板控制是否在三行会话 id 右侧显示 "关闭 tab" 按钮,不持久化到 state JSON。 */
|
* 三类会话 tab 是否当前在编辑器分组里打开(由 extension 通过 issue/patch 同步)。
|
||||||
|
* 仅用于详情面板控制是否在三行会话 id 右侧显示 "关闭 tab" 按钮,不持久化到 state JSON。
|
||||||
|
*/
|
||||||
brainstormTabOpen?: boolean
|
brainstormTabOpen?: boolean
|
||||||
implementTabOpen?: boolean
|
implementTabOpen?: boolean
|
||||||
reviewTabOpen?: boolean
|
reviewTabOpen?: boolean
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
|
"jsx": "react-jsx",
|
||||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"baseUrl": ".",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"jsx": "react-jsx",
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"useDefineForClassFields": true,
|
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"baseUrl": ".",
|
"esModuleInterop": true,
|
||||||
"paths": {
|
"isolatedModules": true,
|
||||||
"@/*": ["src/*"]
|
"skipLibCheck": true
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }],
|
||||||
"include": ["src"],
|
"include": ["src"],
|
||||||
"exclude": ["src/**/*.test.ts"],
|
"exclude": ["src/**/*.test.ts"]
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "Bundler",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true
|
||||||
"composite": true
|
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user