From 4b66d567b0546269a5e29046e128397e6b95e342 Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 26 Aug 2026 15:20:23 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20state=20JSON=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20handoffAttachmentId/handoffFrom=EF=BC=8CIs?= =?UTF-8?q?sue=20=E9=80=8F=E5=87=BA=20assignees?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph --- vscode/cli/cmd/spx/main.go | 2 ++ vscode/cli/internal/state/schema.json | 10 ++++++++++ vscode/schemas/state-json.schema.json | 10 ++++++++++ vscode/src/gitea/issueLoader.ts | 17 +++++++++++++++++ vscode/src/gitea/stateJson.ts | 2 +- vscode/src/gitea/types.ts | 6 ++++++ vscode/src/panel/KanbanPanel.ts | 3 +++ vscode/src/panel/messages.ts | 4 ++-- vscode/src/youtrack/stateComment.ts | 2 +- vscode/webview-ui/src/lib/messages.ts | 4 ++-- vscode/webview-ui/src/types.ts | 6 ++++++ 11 files changed, 60 insertions(+), 6 deletions(-) diff --git a/vscode/cli/cmd/spx/main.go b/vscode/cli/cmd/spx/main.go index cf41bf5..00fbf11 100644 --- a/vscode/cli/cmd/spx/main.go +++ b/vscode/cli/cmd/spx/main.go @@ -408,6 +408,8 @@ var knownStateFields = map[string]struct{}{ "implementStatus": {}, "color": {}, "autoReview": {}, + "handoffAttachmentId": {}, + "handoffFrom": {}, } // parseStateComment returns the parsed map when body is a JSON object carrying diff --git a/vscode/cli/internal/state/schema.json b/vscode/cli/internal/state/schema.json index 13c709c..9515b68 100644 --- a/vscode/cli/internal/state/schema.json +++ b/vscode/cli/internal/state/schema.json @@ -101,6 +101,16 @@ "autoReview": { "type": "boolean", "description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。" + }, + "handoffAttachmentId": { + "type": "string", + "description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空。", + "minLength": 1 + }, + "handoffFrom": { + "type": "string", + "description": "发起移交的 Gitea login。", + "minLength": 1 } } } diff --git a/vscode/schemas/state-json.schema.json b/vscode/schemas/state-json.schema.json index 13c709c..9515b68 100644 --- a/vscode/schemas/state-json.schema.json +++ b/vscode/schemas/state-json.schema.json @@ -101,6 +101,16 @@ "autoReview": { "type": "boolean", "description": "本工单是否启用自动审查(覆盖全局 autoReview 设置)。" + }, + "handoffAttachmentId": { + "type": "string", + "description": "移交会话包在 Gitea 的附件 id;存在即「待接管」,接管完成后清空。", + "minLength": 1 + }, + "handoffFrom": { + "type": "string", + "description": "发起移交的 Gitea login。", + "minLength": 1 } } } diff --git a/vscode/src/gitea/issueLoader.ts b/vscode/src/gitea/issueLoader.ts index d20819e..adc7b69 100644 --- a/vscode/src/gitea/issueLoader.ts +++ b/vscode/src/gitea/issueLoader.ts @@ -88,6 +88,8 @@ function parseColumnFromComments(comments: GiteaComment[]): { testSessionId?: string color?: string autoReview?: boolean + handoffAttachmentId?: string + handoffFrom?: string } { if (comments.length === 0) return { column: null } @@ -132,6 +134,8 @@ function parseColumnFromComments(comments: GiteaComment[]): { testSessionId?: unknown color?: unknown autoReview?: unknown + handoffAttachmentId?: unknown + handoffFrom?: unknown } if (isIssueColumn(obj.column)) { const sessionId = typeof obj.sessionId === 'string' && obj.sessionId.length > 0 @@ -180,6 +184,12 @@ function parseColumnFromComments(comments: GiteaComment[]): { ? obj.color : undefined const autoReview = typeof obj.autoReview === 'boolean' ? obj.autoReview : undefined + const handoffAttachmentId = typeof obj.handoffAttachmentId === 'string' && obj.handoffAttachmentId.length > 0 + ? obj.handoffAttachmentId + : undefined + const handoffFrom = typeof obj.handoffFrom === 'string' && obj.handoffFrom.length > 0 + ? obj.handoffFrom + : undefined return { column: obj.column, sessionId, @@ -200,6 +210,8 @@ function parseColumnFromComments(comments: GiteaComment[]): { testSessionId, color, autoReview, + handoffAttachmentId, + handoffFrom, } } } @@ -326,6 +338,8 @@ async function buildIssue(opts: { testSessionId, color, autoReview, + handoffAttachmentId, + handoffFrom, } = parseColumnFromComments(comments) // 优先用 PR API 实时结果;失败时退回 state JSON 持久化值。 const prMerged = liveMerged !== undefined ? liveMerged : persistedMerged @@ -392,6 +406,9 @@ async function buildIssue(opts: { ...(prerequisite !== undefined ? { prerequisite } : {}), ...(color ? { color } : {}), ...(autoReview !== undefined ? { autoReview } : {}), + ...(handoffAttachmentId ? { handoffAttachmentId } : {}), + ...(handoffFrom ? { handoffFrom } : {}), + assignees: (issue.assignees ?? []).map(a => a.login), htmlUrl: issue.html_url, } } diff --git a/vscode/src/gitea/stateJson.ts b/vscode/src/gitea/stateJson.ts index 6ddc985..f5194d1 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', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview'] as const +const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] as const /** * body 能 parse 成 object 且含至少一个已知 state 字段 → 认为是 state JSON。 diff --git a/vscode/src/gitea/types.ts b/vscode/src/gitea/types.ts index 70328f6..2965856 100644 --- a/vscode/src/gitea/types.ts +++ b/vscode/src/gitea/types.ts @@ -88,6 +88,12 @@ export interface Issue { * webhook coordinator uses this value instead of the global flag. Stored * in the issue's state-JSON comment so it survives reloads. */ autoReview?: boolean + /** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */ + assignees?: string[] + /** 移交会话包的 Gitea 附件 id(字符串);存在即「待接管」。 */ + handoffAttachmentId?: string + /** 发起移交的 Gitea login。 */ + handoffFrom?: string /** Whether the brainstorm terminal tab is currently alive in this VS Code window. */ brainstormTabOpen?: boolean /** Whether the implementation terminal tab is currently alive in this VS Code window. */ diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index fed8bbc..83b2652 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -13,6 +13,7 @@ import { randomBytes } from 'node:crypto' import * as fs from 'node:fs' import * as path from 'node:path' import { env, TabInputTerminal, ThemeColor, Uri, ViewColumn, window, workspace } from 'vscode' +import { loginForToken } from '../auth/identity' import { deleteToken, getToken } from '../auth/secrets' import { detectRepo } from '../git/remote' import { @@ -882,12 +883,14 @@ export class KanbanWebviewPanel { issues.map(i => [i.number, { source: i.source ?? 'gitea', externalId: i.externalId }] as const), ) const ytSettings = getSettings(this.context) + const me = await loginForToken(host, token) this.postMessage({ type: 'issues/update', issues, scope, globalAutoReview: ytSettings.autoReview, youtrackConfigured: ytSettings.youtrackBaseUrl.trim() !== '' && ytSettings.youtrackProjectShortName.trim() !== '', + me, }) // Re-publish the cached git state so a slow webview boot (or a // refresh after git events already fired) still picks up the latest diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index 8c402f2..5b259fd 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -40,9 +40,9 @@ export type ToastLevel = 'info' | 'success' | 'error' export type ExtensionToWebview = | { type: 'issues/loading' } - | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean } + | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string } | { type: 'issues/error', message: string } - | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string, 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, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } } + | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: 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, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } } | { type: 'issue/pr-diff-summary-done', issueNumber: number } | { type: 'issue/append', issue: Issue, select?: boolean } | { type: 'issue/select-by-number', issueNumber: number } diff --git a/vscode/src/youtrack/stateComment.ts b/vscode/src/youtrack/stateComment.ts index cbee03b..cf3ba04 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', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview'] as const +const KNOWN_STATE_FIELDS = ['column', 'sessionId', 'implementSessionId', 'reviewSessionId', 'testSessionId', 'profilePath', 'brainstormProfilePath', 'testProfilePath', 'specFile', 'planFile', 'prDiffFile', 'pr', 'prMerged', 'prMergedAt', 'branch', 'worktreePath', 'implementStatus', 'color', 'autoReview', 'handoffAttachmentId', 'handoffFrom'] 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/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index 2db3ffb..9b06eaa 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -55,9 +55,9 @@ export interface ProfilesData { export type ExtensionToWebview = | { type: 'issues/loading' } - | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean } + | { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string } | { type: 'issues/error', message: string } - | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string, 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, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string } } + | { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: 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, profilePath?: string, brainstormProfilePath?: string, testProfilePath?: string, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } } | { type: 'issue/pr-diff-summary-done', issueNumber: number } | { type: 'issue/append', issue: Issue, select?: boolean } | { type: 'issue/select-by-number', issueNumber: number } diff --git a/vscode/webview-ui/src/types.ts b/vscode/webview-ui/src/types.ts index 087f352..0ab06d3 100644 --- a/vscode/webview-ui/src/types.ts +++ b/vscode/webview-ui/src/types.ts @@ -65,6 +65,12 @@ export interface Issue { /** Per-issue override of the global `autoReview` setting. Undefined = follow * global setting; true/false = explicit override stored in state JSON. */ autoReview?: boolean + /** Gitea assignee login 列表;接管按钮只对 assignee 含当前登录者的工单显示。 */ + assignees?: string[] + /** 移交会话包的 Gitea 附件 id(字符串);存在即「待接管」。 */ + handoffAttachmentId?: string + /** 发起移交的 Gitea login。 */ + handoffFrom?: string /** 三类会话 tab 是否当前在编辑器分组里打开(由 extension 通过 issue/patch 同步)。 * 仅用于详情面板控制是否在三行会话 id 右侧显示 "关闭 tab" 按钮,不持久化到 state JSON。 */ brainstormTabOpen?: boolean