From 6770570f09ddbc53416f3171024653a7b40f8efe Mon Sep 17 00:00:00 2001 From: cruldra Date: Wed, 26 Aug 2026 16:41:35 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(vscode):=20webview=20=E7=A7=BB?= =?UTF-8?q?=E4=BA=A4/=E6=8E=A5=E7=AE=A1=E6=8C=89=E9=92=AE=E3=80=81?= =?UTF-8?q?=E9=80=89=E4=BA=BA=E5=BC=B9=E7=AA=97=E4=B8=8E=E5=BE=85=E6=8E=A5?= =?UTF-8?q?=E7=AE=A1=E8=A7=92=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_011cEyL6k351U2BzX1Qmygph --- vscode/webview-ui/src/App.tsx | 25 ++++++ .../webview-ui/src/components/BottomTabs.tsx | 8 ++ .../src/components/HandoffModal.tsx | 89 +++++++++++++++++++ .../webview-ui/src/components/IssueCard.tsx | 10 +++ .../src/components/IssueDetailPanel.tsx | 41 ++++++++- vscode/webview-ui/src/hooks/useIssues.ts | 56 +++++++++++- 6 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 vscode/webview-ui/src/components/HandoffModal.tsx diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index 7bdbc15..61fe910 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -2,6 +2,7 @@ import type { PastedImage } from './components/NewIssueModal' import type { Issue } from './types' import { useEffect, useMemo, useRef, useState } from 'react' import { BottomTabs } from './components/BottomTabs' +import { HandoffModal } from './components/HandoffModal' import { KanbanBoard } from './components/KanbanBoard' import { LogModal } from './components/LogModal' import { NewIssueModal } from './components/NewIssueModal' @@ -76,6 +77,12 @@ export function App() { envLockRunning, envLockTitle, toggleEnvLock, + me, + handoffUsers, + requestHandoffUsers, + startHandoff, + acceptHandoff, + isHandoffRunning, } = useIssues() const { profiles: profileData, saveProfiles, openProfileValue } = useProfiles() const { @@ -92,6 +99,7 @@ export function App() { } = useManagedSessions() const [showNewIssueModal, setShowNewIssueModal] = useState(false) const [showLogs, setShowLogs] = useState(false) + const [handoffIssue, setHandoffIssue] = useState(null) const [selectedId, setSelectedId] = useState(null) // 程序化设置的选中(反向选中 / pendingSelectId)记在这里,让下面的自动聚焦 // effect 跳过它——否则 选中→聚焦→终端激活→反向选中 会死循环、CPU 飙升。 @@ -374,6 +382,13 @@ export function App() { onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath} onUpdateTestProfilePath={updateIssueTestProfilePath} onOpenLogs={() => setShowLogs(true)} + me={me} + onStartHandoff={(n) => { + setHandoffIssue(n) + requestHandoffUsers(n) + }} + onAcceptHandoff={acceptHandoff} + isHandoffRunning={isHandoffRunning} profileData={profileData} onProfileSave={saveProfiles} onProfileOpen={openProfileValue} @@ -448,6 +463,16 @@ export function App() { onSubmit={handleSubmitNewIssue} profiles={profiles} /> + setHandoffIssue(null)} + onSubmit={(n, to) => { + startHandoff(n, to) + setHandoffIssue(null) + }} + /> void onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void onOpenLogs: () => void + me?: string + onStartHandoff: (issueNumber: number) => void + onAcceptHandoff: (issueNumber: number) => void + isHandoffRunning: (issueNumber: number) => boolean // Profile tab props profileData: ProfilesData @@ -138,6 +142,10 @@ export function BottomTabs(props: BottomTabsProps) { onUpdateTestProfilePath={props.onUpdateTestProfilePath} profiles={props.profiles} onOpenLogs={props.onOpenLogs} + me={props.me} + onStartHandoff={props.onStartHandoff} + onAcceptHandoff={props.onAcceptHandoff} + isHandoffRunning={props.isHandoffRunning} />
void + onSubmit: (issueNumber: number, to: string) => void +} + +export function HandoffModal({ open, issueNumber, users, onCancel, onSubmit }: Props) { + const [to, setTo] = useState('') + + useEffect(() => { + if (!open) { + setTo('') + return + } + function handleKey(e: KeyboardEvent): void { + if (e.key === 'Escape') + onCancel() + } + document.addEventListener('keydown', handleKey) + return () => document.removeEventListener('keydown', handleKey) + }, [open, onCancel]) + + useEffect(() => { + if (users && users.length > 0 && !to) + setTo(users[0]) + }, [users, to]) + + if (!open || issueNumber === null) + return null + + return ( +
+
+
+

+ 移交 # + {issueNumber} +

+ +
+

+ 会把本机的 worktree 删除、会话记录打包挂到工单附件,并把工单指派给对方。移交前分支必须已全部 push。 +

+ {users === null + ? ( +
+ + 读取可指派用户… +
+ ) + : users.length === 0 + ?
没有可指派的其他用户
+ : ( + ({ value: u, label: u }))} + onChange={setTo} + /> + )} +
+ + +
+
+
+ ) +} diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index a69f176..d25b05c 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -67,6 +67,16 @@ export const IssueCard = forwardRef( ) : null} + {issue.handoffAttachmentId + ? ( + + 待接管 + + ) + : null}
{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 512d58a..4a96d8c 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { useMemo, useRef } from 'react' -import { CircleSlash, ExternalLink, GitMerge, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react' +import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react' import type { ClaudeProfile } from '../hooks/useIssues' import type { Issue, IssueColumn } from '../types' import { COLUMN_LABELS, COLUMN_ORDER } from '../types' @@ -90,6 +90,11 @@ interface IssueDetailPanelProps { onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void /** Open the in-webview log modal. */ onOpenLogs: () => void + /** 当前 Gitea login;undefined = 身份未知(接管按钮不显示)。 */ + me?: string + onStartHandoff: (issueNumber: number) => void + onAcceptHandoff: (issueNumber: number) => void + isHandoffRunning: (issueNumber: number) => boolean } /** @@ -128,6 +133,10 @@ export function IssueDetailPanel({ onUpdateBrainstormProfilePath, onUpdateTestProfilePath, onOpenLogs, + me, + onStartHandoff, + onAcceptHandoff, + isHandoffRunning, }: IssueDetailPanelProps) { // 前置工单未完成 → 锁定。锁定时禁用"实施"等启动新工作的动作按钮, // 但已存在的会话恢复链接保持可用(属于用户已操作过的入口)。 @@ -575,6 +584,36 @@ export function IssueDetailPanel({ avoid acting on a gitea issue of the same number. */} {issue.source !== 'youtrack' && ( <> + {issue.handoffAttachmentId && me !== undefined && (issue.assignees ?? []).includes(me) && ( + + )} + {!issue.handoffAttachmentId && issue.column !== 'done' && ( + + )} + {issue.handoffAttachmentId && !(me !== undefined && (issue.assignees ?? []).includes(me)) && ( + + 待接管 + + )} {(issue.column === 'in-progress' || issue.column === 'review') && (