diff --git a/docs/superpowers/specs/issue-handoff/spec.md b/docs/superpowers/specs/issue-handoff/spec.md index da21337..e5ea33f 100644 --- a/docs/superpowers/specs/issue-handoff/spec.md +++ b/docs/superpowers/specs/issue-handoff/spec.md @@ -90,7 +90,7 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有) 进度 toast 从核对分支开始,同一条提示随步骤改文案(核对分支、检查会话大小、压缩检查、关闭终端、收集会话、打包、上传、写入状态、指派、清理本机)。移交结束前不自动消失。 -审查列里,负责人不是自己、也没有待接管包的 Gitea 工单,卡片上可以「申请移交」。同一张卡同时只有一条申请。同事收到提示,卡片上可以同意或拒绝。同意走上面的移交,对象就是申请人;申请人仍自己点接管。拒绝后申请消失,申请人看到被拒绝。申请人可以在回应前取消。同意后若移交失败,申请留着,两边都看到原因,修好可以再点同意。工单离开审查列时清掉未处理的申请。共享字段 `handoffRequestFrom` / `handoffRequestError`(拒绝时 error 为 `rejected`)。 +审查列里,负责人不是自己、也没有待接管包的 Gitea 工单,工单详情标题栏可以「申请移交」。同一张工单同时只有一条申请。同事收到提示,打开工单详情可以同意或拒绝。同意走上面的移交,对象就是申请人;申请人仍自己点接管。拒绝后申请消失,申请人看到被拒绝。申请人可以在回应前取消。同意后若移交失败,申请留着,两边都看到原因,修好可以再点同意。工单离开审查列时清掉未处理的申请。共享字段 `handoffRequestFrom` / `handoffRequestError`(拒绝时 error 为 `rejected`)。 回滚:大小检查失败 → 什么都不改;打包/上传失败 → toast 终止(终端已关,无其他改动);写状态失败 → 删附件;指派失败 → 清回两个共享字段、删附件。结束 `toast/dismiss`。 diff --git a/vscode/webview-ui/src/App.tsx b/vscode/webview-ui/src/App.tsx index d1f3734..8f19ec3 100644 --- a/vscode/webview-ui/src/App.tsx +++ b/vscode/webview-ui/src/App.tsx @@ -393,10 +393,6 @@ export function App() { onDependencyClear={clearDependency} me={me} avatars={avatars} - onAskHandoff={askHandoff} - onCancelHandoffRequest={cancelHandoffRequest} - onAcceptHandoffRequest={acceptHandoffRequest} - onRejectHandoffRequest={rejectHandoffRequest} />
@@ -441,6 +437,10 @@ export function App() { requestHandoffUsers(n) }} onAcceptHandoff={acceptHandoff} + onAskHandoff={askHandoff} + onCancelHandoffRequest={cancelHandoffRequest} + onAcceptHandoffRequest={acceptHandoffRequest} + onRejectHandoffRequest={rejectHandoffRequest} isHandoffRunning={isHandoffRunning} profileData={profileData} onProfileSave={saveProfiles} diff --git a/vscode/webview-ui/src/components/BottomTabs.tsx b/vscode/webview-ui/src/components/BottomTabs.tsx index 6786416..3f02247 100644 --- a/vscode/webview-ui/src/components/BottomTabs.tsx +++ b/vscode/webview-ui/src/components/BottomTabs.tsx @@ -62,6 +62,10 @@ interface BottomTabsProps { avatars?: Record onStartHandoff: (issueNumber: number) => void onAcceptHandoff: (issueNumber: number) => void + onAskHandoff: (issueNumber: number) => void + onCancelHandoffRequest: (issueNumber: number) => void + onAcceptHandoffRequest: (issueNumber: number) => void + onRejectHandoffRequest: (issueNumber: number) => void isHandoffRunning: (issueNumber: number) => boolean // Profile tab props @@ -166,6 +170,10 @@ export function BottomTabs(props: BottomTabsProps) { avatars={props.avatars} onStartHandoff={props.onStartHandoff} onAcceptHandoff={props.onAcceptHandoff} + onAskHandoff={props.onAskHandoff} + onCancelHandoffRequest={props.onCancelHandoffRequest} + onAcceptHandoffRequest={props.onAcceptHandoffRequest} + onRejectHandoffRequest={props.onRejectHandoffRequest} isHandoffRunning={props.isHandoffRunning} />
diff --git a/vscode/webview-ui/src/components/IssueCard.tsx b/vscode/webview-ui/src/components/IssueCard.tsx index f522639..10b2e1c 100644 --- a/vscode/webview-ui/src/components/IssueCard.tsx +++ b/vscode/webview-ui/src/components/IssueCard.tsx @@ -1,11 +1,10 @@ -import type { ReactNode, Ref } from 'react' +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 { forwardRef } from 'react' import { cn } from '@/lib/utils' -import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest } from '../../../src/panel/handlers/handoffRequest' import { Card } from './ui/card' interface IssueCardProps { @@ -27,11 +26,6 @@ interface IssueCardProps { ownerLogin?: string /** 归属人头像 data URI;缺失时回退首字母圆点。 */ ownerAvatar?: string - me?: string - onAskHandoff?: (issueNumber: number) => void - onCancelHandoffRequest?: (issueNumber: number) => void - onAcceptHandoffRequest?: (issueNumber: number) => void - onRejectHandoffRequest?: (issueNumber: number) => void } /** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */ @@ -57,69 +51,8 @@ function OwnerBadge({ login, avatar }: { login: string, avatar?: string }) { ) } -function HandoffRequestActions({ - issue, - me, - onAsk, - onCancel, - onAccept, - onReject, -}: { - issue: Issue - me?: string - onAsk?: (issueNumber: number) => void - onCancel?: (issueNumber: number) => void - onAccept?: (issueNumber: number) => void - onReject?: (issueNumber: number) => void -}) { - const button = 'rounded border border-[var(--vscode-contrastBorder)] px-1.5 py-0.5 text-[10px] hover:bg-[var(--vscode-toolbar-hoverBackground)]' - let body: ReactNode = null - if (canAnswerHandoffRequest(issue, me)) { - body = ( - <> - - {issue.handoffRequestFrom} - {' '} - 想要 - - {issue.handoffRequestError && issue.handoffRequestError !== 'rejected' - ? {issue.handoffRequestError} - : null} - - - - ) - } - else if (canCancelHandoffRequest(issue, me)) { - body = ( - <> - - {issue.handoffRequestError && issue.handoffRequestError !== 'rejected' ? issue.handoffRequestError : '已申请'} - - - - ) - } - else if (canAskHandoff(issue, me)) { - body = - } - else if (issue.column === 'review' && issue.handoffRequestFrom && issue.source !== 'youtrack' && !issue.handoffAttachmentId) { - body = 申请中 - } - if (!body) - return null - return ( -
event.stopPropagation()} - > - {body} -
- ) -} - export const IssueCard = forwardRef( - ({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar, me, onAskHandoff, onCancelHandoffRequest, onAcceptHandoffRequest, onRejectHandoffRequest }, ref) => { + ({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar }, ref) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly }) @@ -178,14 +111,6 @@ export const IssueCard = forwardRef( ) : 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 947f016..5dc2a5b 100644 --- a/vscode/webview-ui/src/components/IssueDetailPanel.tsx +++ b/vscode/webview-ui/src/components/IssueDetailPanel.tsx @@ -3,6 +3,7 @@ import type { Issue, IssueColumn } from '../types' import type { PropertyGroup, RowAction } from './property-grid' import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react' import { useMemo, useRef } from 'react' +import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest } from '../../../src/panel/handlers/handoffRequest' import { isIssueLocked } from '../lib/dependencies' import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership' import { isSessionMissing } from '../lib/missingSession' @@ -134,6 +135,10 @@ interface IssueDetailPanelProps { me?: string onStartHandoff: (issueNumber: number) => void onAcceptHandoff: (issueNumber: number) => void + onAskHandoff: (issueNumber: number) => void + onCancelHandoffRequest: (issueNumber: number) => void + onAcceptHandoffRequest: (issueNumber: number) => void + onRejectHandoffRequest: (issueNumber: number) => void isHandoffRunning: (issueNumber: number) => boolean /** Gitea login → 头像 data URI;只读工单在标题旁贴归属人头像。 */ avatars?: Record @@ -185,6 +190,10 @@ export function IssueDetailPanel({ isSessionTabPending, onStartHandoff, onAcceptHandoff, + onAskHandoff, + onCancelHandoffRequest, + onAcceptHandoffRequest, + onRejectHandoffRequest, isHandoffRunning, avatars, }: IssueDetailPanelProps) { @@ -847,6 +856,68 @@ export function IssueDetailPanel({ {issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'} + {canAskHandoff(issue, me) && ( + + )} + {canCancelHandoffRequest(issue, me) && ( + <> + + {issue.handoffRequestError && issue.handoffRequestError !== 'rejected' ? issue.handoffRequestError : '已申请'} + + + + )} + {canAnswerHandoffRequest(issue, me) && ( + <> + + {issue.handoffRequestFrom} + {' '} + 想要这个工单 + + {issue.handoffRequestError && issue.handoffRequestError !== 'rejected' + ? ( + + {issue.handoffRequestError} + + ) + : null} + + + + )} {/* Close/delete write to gitea by number — hide for youtrack cards to avoid acting on a gitea issue of the same number. */} {issue.source !== 'youtrack' && !readOnly && ( diff --git a/vscode/webview-ui/src/components/KanbanBoard.tsx b/vscode/webview-ui/src/components/KanbanBoard.tsx index a50c356..d0ef0f5 100644 --- a/vscode/webview-ui/src/components/KanbanBoard.tsx +++ b/vscode/webview-ui/src/components/KanbanBoard.tsx @@ -45,10 +45,6 @@ interface KanbanBoardProps { me?: string /** Gitea login → 头像 data URI。 */ avatars?: Record - onAskHandoff?: (issueNumber: number) => void - onCancelHandoffRequest?: (issueNumber: number) => void - onAcceptHandoffRequest?: (issueNumber: number) => void - onRejectHandoffRequest?: (issueNumber: number) => void } const kanbanCollisionDetection: CollisionDetection = (args) => { @@ -123,10 +119,6 @@ export function KanbanBoard({ onDependencyClear, me, avatars, - onAskHandoff, - onCancelHandoffRequest, - onAcceptHandoffRequest, - onRejectHandoffRequest, }: KanbanBoardProps) { const [activeId, setActiveId] = useState(null) @@ -289,11 +281,6 @@ export function KanbanBoard({ readOnly={readOnly} ownerLogin={ownerLogin} ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined} - me={me} - onAskHandoff={onAskHandoff} - onCancelHandoffRequest={onCancelHandoffRequest} - onAcceptHandoffRequest={onAcceptHandoffRequest} - onRejectHandoffRequest={onRejectHandoffRequest} /> ) })}