diff --git a/vscode/src/panel/KanbanPanel.ts b/vscode/src/panel/KanbanPanel.ts index e4d2c88..b31945e 100644 --- a/vscode/src/panel/KanbanPanel.ts +++ b/vscode/src/panel/KanbanPanel.ts @@ -598,6 +598,10 @@ export class KanbanWebviewPanel { void issues.handleUpdateAutoReview(this, msg.issueNumber, msg.value) return } + if (msg.type === 'issue/update-review-passed') { + void issues.handleUpdateReviewPassed(this, msg.issueNumber, msg.value) + return + } if (msg.type === 'issue/update-profile-path') { void issues.handleUpdateProfilePath(this, msg.issueNumber, msg.profilePath) return diff --git a/vscode/src/panel/handlers/issues.ts b/vscode/src/panel/handlers/issues.ts index be5fd4f..babb7b8 100644 --- a/vscode/src/panel/handlers/issues.ts +++ b/vscode/src/panel/handlers/issues.ts @@ -1893,6 +1893,85 @@ export async function handleUpdateAutoReview(panel: KanbanWebviewPanel, issueNum } } +export async function handleUpdateReviewPassed(panel: KanbanWebviewPanel, issueNumber: number, value: boolean): Promise { + const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath + if (!workspaceRoot) { + panel.postMessage({ + type: 'toast/show', + id: makeNonce(), + level: 'error', + message: '请先打开一个工作区文件夹', + dismissOnTimer: 5000, + }) + return + } + + const remote = await detectRepo(workspaceRoot) + if (!remote) { + panel.postMessage({ + type: 'toast/show', + id: makeNonce(), + level: 'error', + message: '当前工作区没有 Gitea 远程仓库', + dismissOnTimer: 5000, + }) + return + } + + const token = await getToken(panel.context, remote.host) + if (!token) { + panel.postMessage({ + type: 'toast/show', + id: makeNonce(), + level: 'error', + message: '请先完成 Gitea 配置', + dismissOnTimer: 5000, + }) + return + } + + let previousValue: boolean | undefined + try { + const existingState = await panel.readIssueState(issueNumber) + previousValue = typeof existingState.reviewPassed === 'boolean' + ? existingState.reviewPassed + : undefined + } + catch { + previousValue = undefined + } + + try { + await panel.mergeIssueState(issueNumber, { reviewPassed: value }) + logger.add({ + level: 'info', + source: 'panel', + message: `工单 #${issueNumber} reviewPassed=${value} 已持久化`, + }) + } + catch (err) { + const message = err instanceof Error ? err.message : String(err) + logger.add({ + level: 'error', + source: 'panel', + message: `持久化 reviewPassed 失败 (issue #${issueNumber})`, + details: message, + }) + panel.postMessage({ + type: 'toast/show', + id: makeNonce(), + level: 'error', + message: `保存工单 #${issueNumber} 审查通过标记失败: ${message}`, + dismissOnTimer: 6000, + }) + panel.postMessage({ + type: 'issue/patch', + issueNumber, + patch: { reviewPassed: previousValue }, + }) + } +} + /** * Persist a per-issue `profilePath` override into the issue's state JSON * comment. Implement / implement-resume sessions use this profile diff --git a/vscode/src/panel/messages.ts b/vscode/src/panel/messages.ts index fae2d29..fab7d97 100644 --- a/vscode/src/panel/messages.ts +++ b/vscode/src/panel/messages.ts @@ -228,6 +228,7 @@ export type WebviewToExtension | { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number } | { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number } | { type: 'issue/update-auto-review', issueNumber: number, value: boolean } + | { type: 'issue/update-review-passed', issueNumber: number, value: boolean } | { type: 'issue/update-profile-path', issueNumber: number, profilePath: string } | { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string } | { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' } diff --git a/vscode/src/panel/readOnlyGuard.test.ts b/vscode/src/panel/readOnlyGuard.test.ts index 462d506..fd07948 100644 --- a/vscode/src/panel/readOnlyGuard.test.ts +++ b/vscode/src/panel/readOnlyGuard.test.ts @@ -8,6 +8,7 @@ describe('isOwnerOnlyMessage', () => { expect(isOwnerOnlyMessage('issue/close')).toBe(true) expect(isOwnerOnlyMessage('dependency/set')).toBe(true) expect(isOwnerOnlyMessage('issue/update-auto-review')).toBe(true) + expect(isOwnerOnlyMessage('issue/update-review-passed')).toBe(true) expect(isOwnerOnlyMessage('issue/clear-session')).toBe(true) }) diff --git a/vscode/src/panel/readOnlyGuard.ts b/vscode/src/panel/readOnlyGuard.ts index 6add462..b3c9a5d 100644 --- a/vscode/src/panel/readOnlyGuard.ts +++ b/vscode/src/panel/readOnlyGuard.ts @@ -27,6 +27,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet = new Set([ 'issue/close', 'issue/generate-pr-diff-summary', 'issue/update-auto-review', + 'issue/update-review-passed', 'issue/update-profile-path', 'issue/update-brainstorm-profile-path', 'issue/clear-session', diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 133fbf6..e56fa5b 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -69,6 +69,7 @@ export function App() { setDependency, clearDependency, updateIssueAutoReview, + updateIssueReviewPassed, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, @@ -399,6 +400,7 @@ export function App() { onColumnChange={changeColumn} onDependencySet={setDependency} onDependencyClear={clearDependency} + onSetReviewPassed={updateIssueReviewPassed} me={me} avatars={avatars} /> @@ -435,6 +437,7 @@ export function App() { onContinueBrainstorm={continueBrainstorm} onClearSession={clearSession} onUpdateAutoReview={updateIssueAutoReview} + onUpdateReviewPassed={updateIssueReviewPassed} onUpdateProfilePath={updateIssueProfilePath} onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath} onUpdateTestProfilePath={updateIssueTestProfilePath} diff --git a/vscode/webview-ui/src/components/BottomTabs.tsx b/vscode/webview-ui/src/components/BottomTabs.tsx index 4215c94..d75c491 100644 --- a/vscode/webview-ui/src/components/BottomTabs.tsx +++ b/vscode/webview-ui/src/components/BottomTabs.tsx @@ -55,6 +55,7 @@ export interface BottomTabsProps { onContinueBrainstorm: (issueNumber: number) => void onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void onUpdateAutoReview: (issueNumber: number, value: boolean) => void + onUpdateReviewPassed: (issueNumber: number, value: boolean) => void onUpdateProfilePath: (issueNumber: number, profilePath: string) => void onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void @@ -171,6 +172,7 @@ export function BottomTabs(props: BottomTabsProps) { onContinueBrainstorm={props.onContinueBrainstorm} onClearSession={props.onClearSession} onUpdateAutoReview={props.onUpdateAutoReview} + onUpdateReviewPassed={props.onUpdateReviewPassed} onUpdateProfilePath={props.onUpdateProfilePath} onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath} onUpdateTestProfilePath={props.onUpdateTestProfilePath} diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index f4e3380..2f4895c 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -2,7 +2,7 @@ import type { Ref } from 'react' import type { Issue } from '../types' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { Lock } from 'lucide-react' +import { Check, Lock } from 'lucide-react' import { forwardRef } from 'react' import { cn } from '@/lib/utils' import { Card } from './ui/card' @@ -26,6 +26,8 @@ interface IssueCardProps { ownerLogin?: string /** 归属人头像 data URI;缺失时回退首字母圆点。 */ ownerAvatar?: string + /** 审查列:手动标通过或改回审查中。 */ + onSetReviewPassed?: (issueNumber: number, value: boolean) => void } /** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */ @@ -52,7 +54,7 @@ function OwnerBadge({ login, avatar }: { login: string, avatar?: string }) { } export const IssueCard = forwardRef( - ({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar }, ref) => { + ({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar, onSetReviewPassed }, ref) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly }) @@ -72,6 +74,9 @@ export const IssueCard = forwardRef( (ref as React.MutableRefObject).current = node } + const showMark = issue.column === 'review' && !readOnly && issue.source !== 'youtrack' && onSetReviewPassed != null + const cornerCount = (showMark ? 1 : 0) + (locked ? 1 : 0) + (readOnly && ownerLogin ? 1 : 0) + return ( } @@ -91,6 +96,26 @@ export const IssueCard = forwardRef( : undefined} >
+ {showMark + ? ( + + ) + : null} {locked ? ( ( : null}
= 3 && 'pr-14', issue.column === 'review' && (issue.reviewPassed === true ? 'text-green-400' : 'text-amber-400'), - )}> + )} + > {issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`} diff --git a/vscode/webview-ui/src/components/IssueDetailPanel.tsx b/vscode/webview-ui/src/components/IssueDetailPanel.tsx index 8714970..0c2eb02 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -119,6 +119,8 @@ interface IssueDetailPanelProps { onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void /** Persist a per-issue `autoReview` override into the state JSON. */ onUpdateAutoReview: (issueNumber: number, value: boolean) => void + /** 审查列手动标记通过。 */ + onUpdateReviewPassed: (issueNumber: number, value: boolean) => void /** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */ profiles: ClaudeProfile[] /** Persist a per-issue `profilePath` override into the state JSON. */ @@ -180,6 +182,7 @@ export function IssueDetailPanel({ onContinueBrainstorm, onClearSession, onUpdateAutoReview, + onUpdateReviewPassed, profiles, onUpdateProfilePath, onUpdateBrainstormProfilePath, @@ -559,6 +562,14 @@ export function IssueDetailPanel({ type: 'boolean', description: `打开新 PR 时是否自动启动 codex 审查;未设置时跟随全局(当前全局:${globalAutoReview ? '开' : '关'})`, }, + ...(issue?.column === 'review' && issue.source !== 'youtrack' + ? [{ + key: 'reviewPassed', + label: '审查通过', + type: 'boolean' as const, + description: '手动标成已通过,标题变绿。新的提交会清掉这个标记。', + }] + : []), { key: 'color', label: '颜色', @@ -772,6 +783,7 @@ export function IssueDetailPanel({ testSessionId: issue.testSessionId ?? null, prototypeSessionId: issue.prototypeSessionId ?? null, autoReview: issue.autoReview ?? globalAutoReview, + reviewPassed: issue.reviewPassed === true, color: issue.color ?? null, // 展示/选中值按 basename 映射到本机 profiles,避免跨机绝对路径变成「自定义」项。 brainstormProfilePath: matchProfileOption(issue.brainstormProfilePath, profiles) || null, @@ -1008,6 +1020,8 @@ export function IssueDetailPanel({ return if (key === 'autoReview' && issue && typeof value === 'boolean') onUpdateAutoReview(issue.number, value) + if (key === 'reviewPassed' && issue && typeof value === 'boolean') + onUpdateReviewPassed(issue.number, value) if (key === 'brainstormProfilePath' && issue && typeof value === 'string') onUpdateBrainstormProfilePath(issue.number, value) if (key === 'profilePath' && issue && typeof value === 'string') diff --git a/vscode/webview-ui/src/components/KanbanBoard.tsx b/vscode/webview-ui/src/components/KanbanBoard.tsx index d0ef0f5..6bfb8eb 100644 --- a/vscode/webview-ui/src/components/KanbanBoard.tsx +++ b/vscode/webview-ui/src/components/KanbanBoard.tsx @@ -41,6 +41,7 @@ interface KanbanBoardProps { onColumnChange?: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void onDependencySet?: (issueNumber: number, prerequisiteNumber: number) => void onDependencyClear?: (issueNumber: number, prerequisiteNumber: number) => void + onSetReviewPassed?: (issueNumber: number, value: boolean) => void /** 当前 Gitea login;用于判定哪些卡是同事的(只读)。 */ me?: string /** Gitea login → 头像 data URI。 */ @@ -117,6 +118,7 @@ export function KanbanBoard({ onColumnChange, onDependencySet, onDependencyClear, + onSetReviewPassed, me, avatars, }: KanbanBoardProps) { @@ -281,6 +283,7 @@ export function KanbanBoard({ readOnly={readOnly} ownerLogin={ownerLogin} ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined} + onSetReviewPassed={onSetReviewPassed} /> ) })} diff --git a/vscode/webview-ui/src/hooks/useIssues.ts b/vscode/webview-ui/src/hooks/useIssues.ts index cd920b3..cb500d4 100644 --- a/vscode/webview-ui/src/hooks/useIssues.ts +++ b/vscode/webview-ui/src/hooks/useIssues.ts @@ -232,6 +232,7 @@ export interface UseIssuesResult { setDependency: (issueNumber: number, prerequisiteNumber: number) => void clearDependency: (issueNumber: number, prerequisiteNumber: number) => void updateIssueAutoReview: (issueNumber: number, value: boolean) => void + updateIssueReviewPassed: (issueNumber: number, value: boolean) => void /** * 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后 * postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。 @@ -730,6 +731,20 @@ export function useIssues(): UseIssuesResult { postMessage({ type: 'issue/update-auto-review', issueNumber, value }) }, []) + const updateIssueReviewPassed = useCallback((issueNumber: number, value: boolean): void => { + setState((prev) => { + if (prev.status !== 'ready') + return prev + return { + status: 'ready', + issues: prev.issues.map(i => + i.number === issueNumber ? { ...i, reviewPassed: value } : i, + ), + } + }) + postMessage({ type: 'issue/update-review-passed', issueNumber, value }) + }, []) + const updateIssueProfilePath = useCallback((issueNumber: number, profilePath: string): void => { // Optimistically update local state so the dropdown reflects immediately // without remounting the issues list / detail panel via a full reload. @@ -1097,5 +1112,5 @@ export function useIssues(): UseIssuesResult { ]) }, [state, me]) - return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, openPrototypeInFigma, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, pendingSelectTab, clearPendingSelectTab, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning, askHandoff, cancelHandoffRequest, acceptHandoffRequest, rejectHandoffRequest } + return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, openPrototypeInFigma, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueReviewPassed, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, pendingSelectTab, clearPendingSelectTab, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning, askHandoff, cancelHandoffRequest, acceptHandoffRequest, rejectHandoffRequest } } diff --git a/vscode/webview-ui/src/lib/messages.ts b/vscode/webview-ui/src/lib/messages.ts index 29373c7..f470acc 100644 --- a/vscode/webview-ui/src/lib/messages.ts +++ b/vscode/webview-ui/src/lib/messages.ts @@ -267,6 +267,7 @@ export type WebviewToExtension | { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number } | { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number } | { type: 'issue/update-auto-review', issueNumber: number, value: boolean } + | { type: 'issue/update-review-passed', issueNumber: number, value: boolean } | { type: 'issue/update-profile-path', issueNumber: number, profilePath: string } | { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string } | { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }