diff --git a/vscode/cli/cmd/spx/main.go b/vscode/cli/cmd/spx/main.go index 1a56872..e9ddafd 100644 --- a/vscode/cli/cmd/spx/main.go +++ b/vscode/cli/cmd/spx/main.go @@ -425,6 +425,7 @@ var knownStateFields = map[string]struct{}{ "implementStatus": {}, "color": {}, "autoReview": {}, + "reviewPassed": {}, "handoffAttachmentId": {}, "handoffFrom": {}, "handoffRequestFrom": {}, diff --git a/vscode/cli/internal/state/schema.json b/vscode/cli/internal/state/schema.json index 2ba504c..029748b 100644 --- a/vscode/cli/internal/state/schema.json +++ b/vscode/cli/internal/state/schema.json @@ -118,6 +118,10 @@ "type": "boolean", "description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。" }, + "reviewPassed": { + "type": "boolean", + "description": "最近一次审查是否认为可以过。true 表示通过;false 或不写表示还在审,或审出了必须改的问题。PR 有新提交时合并成 false。不改变看板列。" + }, "handoffAttachmentId": { "type": "string", "description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。" diff --git a/vscode/cli/internal/state/schema_test.go b/vscode/cli/internal/state/schema_test.go index f2e2094..8fefb89 100644 --- a/vscode/cli/internal/state/schema_test.go +++ b/vscode/cli/internal/state/schema_test.go @@ -87,13 +87,23 @@ func TestValidateAcceptsFullState(t *testing.T) { "worktreePath":"/home/x/Sources/worktree/proj/slug", "implementStatus":"done", "color":"terminal.ansiBlue", - "autoReview":true + "autoReview":true, + "reviewPassed":true }` if err := Validate([]byte(full)); err != nil { t.Fatalf("完整合法 state 应当通过校验,得到: %v", err) } } +func TestValidateAcceptsReviewPassed(t *testing.T) { + if err := Validate([]byte(`{"reviewPassed":true}`)); err != nil { + t.Fatalf("reviewPassed=true 应当通过校验,得到: %v", err) + } + if err := Validate([]byte(`{"reviewPassed":false}`)); err != nil { + t.Fatalf("reviewPassed=false 应当通过校验,得到: %v", err) + } +} + func TestValidateRejectsBadPrototypeFile(t *testing.T) { err := Validate([]byte(`{"prototypeFile":"docs/prototype/login/figma-plugin/manifest.md"}`)) if err == nil { diff --git a/vscode/prompts/review.md b/vscode/prompts/review.md index 3918e74..2c6d2f0 100644 --- a/vscode/prompts/review.md +++ b/vscode/prompts/review.md @@ -22,6 +22,20 @@ opencli spx pr review-comment --pr {{prNumber}} --body-file /tmp/review-{{prNumb 审查结束以后,删除所有审查过程中创建的临时 worktree(`git worktree remove --force <路径>`),你当前所在的 worktree 除外。 +## 审查结论 + +没有必须改的问题才算通过。小建议不算不通过。 + +通过: + +``` +opencli spx issue state merge --issue {{issueNumber}} --state-json '{"reviewPassed":true}' +``` + +有必须改的问题:同一条命令,把 `reviewPassed` 写成 `false`。 + +这条合并发生在审查评论发出之后。不要建议合并 PR,不要执行合并。 + ## 严禁 - 不要在审查意见里建议"合并 PR"或"merge" diff --git a/vscode/schemas/state-json.schema.json b/vscode/schemas/state-json.schema.json index 2ba504c..029748b 100644 --- a/vscode/schemas/state-json.schema.json +++ b/vscode/schemas/state-json.schema.json @@ -118,6 +118,10 @@ "type": "boolean", "description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。" }, + "reviewPassed": { + "type": "boolean", + "description": "最近一次审查是否认为可以过。true 表示通过;false 或不写表示还在审,或审出了必须改的问题。PR 有新提交时合并成 false。不改变看板列。" + }, "handoffAttachmentId": { "type": "string", "description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空;清空时写空串。" diff --git a/vscode/src/gitea/issueComment.test.ts b/vscode/src/gitea/issueComment.test.ts index a8d708e..ee572ea 100644 --- a/vscode/src/gitea/issueComment.test.ts +++ b/vscode/src/gitea/issueComment.test.ts @@ -10,6 +10,11 @@ describe('systemCommentText', () => { expect(systemCommentText('{"column":"review","color":"terminal.ansiBlue"}')).toBe('更新了工单状态,放到审查') }) + it('审查是否通过不单写一句', () => { + expect(systemCommentText('{"reviewPassed":true}')).toBe('更新了工单状态') + expect(systemCommentText('{"column":"review","reviewPassed":false}')).toBe('更新了工单状态,放到审查') + }) + it('普通评论不是系统消息', () => { expect(systemCommentText('这个方案可以')).toBeUndefined() }) diff --git a/vscode/src/gitea/issueComment.ts b/vscode/src/gitea/issueComment.ts index 9be9312..c463d59 100644 --- a/vscode/src/gitea/issueComment.ts +++ b/vscode/src/gitea/issueComment.ts @@ -23,6 +23,7 @@ const STATE_KEYS = new Set([ 'worktreePath', 'color', 'autoReview', + 'reviewPassed', ]) /** 插件自己写的评论。返回系统气泡文案;普通讨论返回 undefined。 */ diff --git a/vscode/src/gitea/issueLoader.ts b/vscode/src/gitea/issueLoader.ts index c0e2304..ddb2dfe 100644 --- a/vscode/src/gitea/issueLoader.ts +++ b/vscode/src/gitea/issueLoader.ts @@ -99,6 +99,7 @@ function parseColumnFromComments(comments: GiteaComment[]): { prototypeSessionId?: string color?: string autoReview?: boolean + reviewPassed?: boolean handoffAttachmentId?: string handoffFrom?: string handoffRequestFrom?: string @@ -150,6 +151,7 @@ function parseColumnFromComments(comments: GiteaComment[]): { prototypeSessionId?: unknown color?: unknown autoReview?: unknown + reviewPassed?: unknown handoffAttachmentId?: unknown handoffFrom?: unknown handoffRequestFrom?: unknown @@ -209,6 +211,7 @@ function parseColumnFromComments(comments: GiteaComment[]): { ? obj.color : undefined const autoReview = typeof obj.autoReview === 'boolean' ? obj.autoReview : undefined + const reviewPassed = typeof obj.reviewPassed === 'boolean' ? obj.reviewPassed : undefined const handoffAttachmentId = typeof obj.handoffAttachmentId === 'string' && obj.handoffAttachmentId.length > 0 ? obj.handoffAttachmentId : undefined @@ -244,6 +247,7 @@ function parseColumnFromComments(comments: GiteaComment[]): { prototypeSessionId, color, autoReview, + reviewPassed, handoffAttachmentId, handoffFrom, handoffRequestFrom, @@ -390,6 +394,7 @@ async function buildIssue(opts: { prototypeSessionId, color, autoReview, + reviewPassed, handoffAttachmentId, handoffFrom, handoffRequestFrom, @@ -450,6 +455,7 @@ async function buildIssue(opts: { ...(prerequisite !== undefined ? { prerequisite } : {}), ...(color ? { color } : {}), ...(autoReview !== undefined ? { autoReview } : {}), + ...(reviewPassed !== undefined ? { reviewPassed } : {}), ...(handoffAttachmentId ? { handoffAttachmentId } : {}), ...(handoffFrom ? { handoffFrom } : {}), ...(handoffRequestFrom ? { handoffRequestFrom } : {}), diff --git a/vscode/src/gitea/stateJson.ts b/vscode/src/gitea/stateJson.ts index cc25a47..5714b89 100644 --- a/vscode/src/gitea/stateJson.ts +++ b/vscode/src/gitea/stateJson.ts @@ -19,7 +19,7 @@ import { listIssueComments, postIssueComment } from './api' * 普通文本评论、审查评论、Gitea 自动关联评论都不含这些字段,因此从尾往前 * 扫描时会被跳过,避免它们插队后下一次 merge 从空状态开始丢历史。 */ -const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const +const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'reviewPassed', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const /** * body 能 parse 成 object 且含至少一个已知 state 字段 → 认为是 state JSON。 diff --git a/vscode/src/gitea/types.ts b/vscode/src/gitea/types.ts index d0895ec..cb1d5e5 100644 --- a/vscode/src/gitea/types.ts +++ b/vscode/src/gitea/types.ts @@ -131,6 +131,8 @@ export interface Issue { * in the issue's state-JSON comment so it survives reloads. */ autoReview?: boolean + /** 最近一次审查是否通过。只影响审查列卡片上的小字,不改列。 */ + reviewPassed?: boolean /** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */ assignees?: string[] /** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */ diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 9833ee9..fae2d29 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -54,7 +54,7 @@ export type ExtensionToWebview | { type: 'issues/loaded' } | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record } | { type: 'issues/error', message: string } - | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } } + | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, reviewPassed?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } } /** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */ | { type: 'session/tab-settled', sessionId: string } | { type: 'issue/pr-diff-summary-done', issueNumber: number } diff --git a/vscode/src/settings/previousReviewPrompt.ts b/vscode/src/settings/previousReviewPrompt.ts new file mode 100644 index 0000000..6495e4e --- /dev/null +++ b/vscode/src/settings/previousReviewPrompt.ts @@ -0,0 +1,2 @@ +/** 改「审查结论」之前的默认审查提示词。设置里若原样存着这份,整段换成新文件。 */ +export const PREVIOUS_DEFAULT_REVIEW_PROMPT = '审查这个仓库的 PR #{{prNumber}}。\n\n用 `tea pulls {{prNumber}}` 看 PR 元信息(标题/描述/分支)。看代码差异用 git(当前目录就是 PR 分支的 worktree):先 `git fetch origin main`,再 `git diff origin/main...HEAD`(概览可加 --stat)。注意 tea 没有 `diff` 子命令,不要尝试 `tea pulls diff`。\n\n## 审查方式\n\nECC 的 `/ecc:review-pr` 和 `/ecc:code-review` 的 PR 模式都走 `gh`,对 Gitea 不可用,不要调。直接把上面的 diff 喂给 ECC 的审查子代理,按改动涉及的语言挑:\n\n- `ecc:code-reviewer`(必跑)\n- `ecc:silent-failure-hunter`、`ecc:pr-test-analyzer`(必跑)\n- 语言专项:TypeScript 用 `ecc:typescript-reviewer`,Python 用 `ecc:python-reviewer`,React 用 `ecc:react-reviewer`,其余同理\n\n汇总时去重、按严重程度排序,只报置信度高的问题。\n\n## 提交审查意见\n\n把审查意见(markdown 格式)写到 `/tmp/review-{{prNumber}}.md`,调 spx:\n\n```\nopencli spx pr review-comment --pr {{prNumber}} --body-file /tmp/review-{{prNumber}}.md\n```\n\n审查结束以后,删除所有审查过程中创建的临时 worktree(`git worktree remove --force <路径>`),你当前所在的 worktree 除外。\n\n## 严禁\n\n- 不要在审查意见里建议"合并 PR"或"merge"\n- 不要执行 `tea pulls merge` 或任何合并命令\n- 不要 push 到 main / dev 分支\n\n合并权完全在用户手上,你的工作只是指出问题或确认通过。\n' diff --git a/vscode/src/settings/promptMigration.test.ts b/vscode/src/settings/promptMigration.test.ts index 9a6bae8..4962a41 100644 --- a/vscode/src/settings/promptMigration.test.ts +++ b/vscode/src/settings/promptMigration.test.ts @@ -1,9 +1,11 @@ import { readFileSync } from 'node:fs' import * as path from 'node:path' import { describe, expect, it } from 'vitest' +import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt' import { composeBrainstormTemplate, planPromptMigration, + planReviewPromptMigration, PROMPT_TEMPLATE_VERSION, splitIfBlock, upgradePlaceholders, @@ -147,3 +149,43 @@ describe('planPromptMigration', () => { expect(merged).toContain('工单已有原型插件 {{prototypeFile}}') }) }) + +describe('planReviewPromptMigration', () => { + const nextDefault = readFileSync( + path.resolve(__dirname, '../../prompts/review.md'), + 'utf-8', + ) + + it('和改之前的默认一样时,整段换成现在的文件', () => { + const result = planReviewPromptMigration( + PREVIOUS_DEFAULT_REVIEW_PROMPT, + PREVIOUS_DEFAULT_REVIEW_PROMPT, + nextDefault, + ) + expect(result).not.toBeNull() + expect(result!.prompt).toBe(nextDefault) + expect(result!.prompt).toContain('{"reviewPassed":true}') + expect(result!.notes[0]?.message).toContain('旧版默认') + }) + + it('用户改过、还没有 reviewPassed 时,只把审查结论接到末尾', () => { + const custom = '先看 diff,再写意见。\n不要跑无关命令。\n' + const result = planReviewPromptMigration(custom, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault) + expect(result).not.toBeNull() + expect(result!.prompt.startsWith('先看 diff,再写意见。\n不要跑无关命令。\n\n## 审查结论\n')).toBe(true) + expect(result!.prompt).toContain('reviewPassed') + expect(result!.prompt).toContain('小建议不算不通过') + expect(result!.prompt).not.toContain('## 严禁') + expect(result!.prompt).not.toContain('tea pulls') + }) + + it('已经写了 reviewPassed 的不改', () => { + const custom = '我自己的审查。\nreviewPassed 我写过了。\n' + expect(planReviewPromptMigration(custom, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull() + }) + + it('没存审查提示词时不改', () => { + expect(planReviewPromptMigration('', PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull() + expect(planReviewPromptMigration(undefined, PREVIOUS_DEFAULT_REVIEW_PROMPT, nextDefault)).toBeNull() + }) +}) diff --git a/vscode/src/settings/promptMigration.ts b/vscode/src/settings/promptMigration.ts index 9142ccb..8f33d2f 100644 --- a/vscode/src/settings/promptMigration.ts +++ b/vscode/src/settings/promptMigration.ts @@ -188,3 +188,52 @@ export function planPromptMigration( next.promptTemplateVersion = PROMPT_TEMPLATE_VERSION return { blob: next, notes } } + +function normalizePrompt(text: string): string { + return text.replace(/\r\n/g, '\n').trim() +} + +/** 新版审查提示词里「审查结论」那一节,到下一个同级标题为止。 */ +export function reviewConclusionSection(nextDefault: string): string | null { + const normalized = nextDefault.replace(/\r\n/g, '\n') + const heading = '## 审查结论' + const start = normalized.indexOf(heading) + if (start < 0) + return null + const rest = normalized.slice(start) + const relative = rest.slice(heading.length).search(/\n## /) + const section = relative < 0 ? rest : rest.slice(0, heading.length + relative) + const trimmed = section.trim() + return trimmed.length > 0 ? trimmed : null +} + +/** + * 设置里已经存了一份审查提示词时:和改之前的默认一样就整段换成新文件; + * 用户改过、且还没有 reviewPassed,只把「审查结论」接到末尾。 + * 返回 null 表示不用写盘。 + */ +export function planReviewPromptMigration( + stored: unknown, + previousDefault: string, + nextDefault: string, +): { prompt: string, notes: MigrationNote[] } | null { + if (typeof stored !== 'string' || stored.length === 0) + return null + if (normalizePrompt(stored) === normalizePrompt(previousDefault)) { + if (normalizePrompt(nextDefault) === normalizePrompt(previousDefault)) + return null + return { + prompt: nextDefault, + notes: [{ level: 'info', message: '审查提示词与旧版默认一致,已换成带审查结论的新版' }], + } + } + if (stored.includes('reviewPassed')) + return null + const section = reviewConclusionSection(nextDefault) + if (!section) + return null + return { + prompt: `${stored.replace(/\s+$/, '')}\n\n${section}\n`, + notes: [{ level: 'info', message: '审查提示词末尾已追加「审查结论」,其余段落未改' }], + } +} diff --git a/vscode/src/settings/store.ts b/vscode/src/settings/store.ts index 2e17381..928c9d7 100644 --- a/vscode/src/settings/store.ts +++ b/vscode/src/settings/store.ts @@ -18,7 +18,8 @@ import path from 'node:path' import { seedCommitProfilePath } from '../cc/commitProfile' import { listClaudeProfiles } from '../cc/profiles' import { normalizeReleasePrefix } from '../git/release' -import { planPromptMigration } from './promptMigration' +import { PREVIOUS_DEFAULT_REVIEW_PROMPT } from './previousReviewPrompt' +import { planPromptMigration, planReviewPromptMigration } from './promptMigration' /** * Fallback prompt strings used when the on-disk markdown can't be read. @@ -513,10 +514,19 @@ export async function saveSettings(ctx: ExtensionContext, next: Partial, + const stored = readStoredSettings(ctx) as Record + const planned = planPromptMigration( + stored, readDefaultPrompt(ctx.extensionPath, 'brainstorm'), ) + const review = planReviewPromptMigration( + planned.blob?.reviewPrompt ?? stored.reviewPrompt, + PREVIOUS_DEFAULT_REVIEW_PROMPT, + readDefaultPrompt(ctx.extensionPath, 'review'), + ) + const blob = review + ? { ...(planned.blob ?? stored), reviewPrompt: review.prompt } + : planned.blob if (!blob) return [] const out: MigrationNote[] = [] @@ -535,7 +545,11 @@ export function migratePromptTemplates(ctx: ExtensionContext): MigrationNote[] { } } writeStoredSettings(ctx, blob as Partial) - out.push(...notes, { level: 'info', message: '提示词模板已迁到 Handlebars' }) + out.push(...planned.notes) + if (review) + out.push(...review.notes) + if (planned.blob) + out.push({ level: 'info', message: '提示词模板已迁到 Handlebars' }) return out } diff --git a/vscode/src/webhook/coordinator.ts b/vscode/src/webhook/coordinator.ts index 83297c7..6e5c8e1 100644 --- a/vscode/src/webhook/coordinator.ts +++ b/vscode/src/webhook/coordinator.ts @@ -1158,8 +1158,9 @@ class WebhookCoordinator { // Per-issue override takes precedence over the global setting. let effectiveAutoReview = getSettings(this.ctx).autoReview let autoReviewSource: 'issue' | 'global' = 'global' + let stateJson: Record | undefined try { - const stateJson = await readStateJsonComment({ + stateJson = await readStateJsonComment({ host: ctx.host, token: ctx.token, owner: ctx.owner, @@ -1180,6 +1181,34 @@ class WebhookCoordinator { details: message, }) } + // 新提交让上一次「已通过」作废。已经是 false 或没写过就不再发一条状态评论。 + // 读失败时不知道旧值,也清一次,避免卡片继续挂着已通过。 + if (stateJson === undefined || stateJson.reviewPassed === true) { + try { + await mergeStateJsonComment({ + host: ctx.host, + token: ctx.token, + owner: ctx.owner, + repo: ctx.repo, + issueNumber: event.issueNumber, + extra: { reviewPassed: false }, + }) + this.activePanel?.postMessage({ + type: 'issue/patch', + issueNumber: event.issueNumber, + patch: { reviewPassed: false }, + }) + } + catch (err) { + const message = err instanceof Error ? err.message : String(err) + logger.add({ + level: 'warn', + source: 'webhook', + message: `新提交未能清掉审查通过 #${event.issueNumber}`, + details: message, + }) + } + } if (!effectiveAutoReview) { logger.add({ level: 'info', diff --git a/vscode/src/youtrack/issueLoader.ts b/vscode/src/youtrack/issueLoader.ts index 66461ca..20d8215 100644 --- a/vscode/src/youtrack/issueLoader.ts +++ b/vscode/src/youtrack/issueLoader.ts @@ -85,6 +85,7 @@ function toIssue(baseUrl: string, it: YouTrackIssue): Issue { ? state.implementStatus : undefined, autoReview: typeof state.autoReview === 'boolean' ? state.autoReview : undefined, + reviewPassed: typeof state.reviewPassed === 'boolean' ? state.reviewPassed : undefined, } } diff --git a/vscode/src/youtrack/stateComment.ts b/vscode/src/youtrack/stateComment.ts index 6111088..e91d2ed 100644 --- a/vscode/src/youtrack/stateComment.ts +++ b/vscode/src/youtrack/stateComment.ts @@ -20,7 +20,7 @@ import { addComment, listComments } from './api' export const STATE_MARKER = '' /** Same field set as the Gitea state blob — kept independent to avoid coupling. */ -const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const +const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'prototypeSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'prototypeProfilePath', 'specFile', 'planFile', 'prototypeFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'reviewPassed', 'handoffAttachmentId', 'handoffFrom', 'handoffRequestFrom', 'handoffRequestError'] as const /** * Parse a comment body into a state object, or null if it isn't a state diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index 10b2e1c..076257f 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -118,6 +118,13 @@ export const IssueCard = forwardRef( {' '} {issue.title} + {issue.column === 'review' + ? ( +
+ {issue.reviewPassed === true ? '已通过' : '审查中'} +
+ ) + : null} ) }, diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index f6971e4..29373c7 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -93,7 +93,7 @@ export type ExtensionToWebview | { type: 'issues/loaded' } | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record } | { type: 'issues/error', message: string } - | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } } + | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, reviewPassed?: boolean, specFile?: string, planFile?: string, prototypeFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, missingSessionIds?: string[], testSessionId?: string | null, prototypeSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, prototypeTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, prototypeProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null, handoffRequestFrom?: string | null, handoffRequestError?: string | null } } /** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */ | { type: 'session/tab-settled', sessionId: string } | { type: 'issue/pr-diff-summary-done', issueNumber: number } diff --git a/vscode/webview-ui/src/types.ts b/vscode/webview-ui/src/types.ts index 168a1c5..74122a7 100644 --- a/vscode/webview-ui/src/types.ts +++ b/vscode/webview-ui/src/types.ts @@ -95,6 +95,8 @@ export interface Issue { * global setting; true/false = explicit override stored in state JSON. */ autoReview?: boolean + /** 最近一次审查是否通过。只影响审查列卡片上的小字,不改列。 */ + reviewPassed?: boolean /** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */ assignees?: string[] /** Gitea 工单创建者 login;无 assignee 的工单靠它判定归属与卡片头像。 */