import type { HandoffProfiles, HandoffSessions } from '../../cc/handoffManifest' import type { IssueColumn } from '../../gitea/types' import { LOCAL_STATE_FIELDS } from '../../issues/localState' export function canStartHandoff(issue: { source?: 'gitea' | 'youtrack' column: IssueColumn handoffAttachmentId?: string assignees?: string[] }, me: string | undefined): boolean { const assignees = issue.assignees ?? [] const mine = assignees.length === 0 || (me !== undefined && assignees.includes(me)) return issue.source !== 'youtrack' && issue.column !== 'done' && !issue.handoffAttachmentId && mine } export function canAcceptHandoff(issue: { source?: 'gitea' | 'youtrack' handoffAttachmentId?: string assignees?: string[] }, me: string | undefined): boolean { return issue.source !== 'youtrack' && !!issue.handoffAttachmentId && me !== undefined && (issue.assignees ?? []).includes(me) } /** 移交前 worktree 必须「干净 + 全部 push」,否则接管方从远端重建会丢工作。 */ export function checkWorktreePushed(input: { statusPorcelain: string localHead: string remoteHead: string }): { ok: true } | { ok: false, reason: string } { if (input.statusPorcelain.trim().length > 0) return { ok: false, reason: '工作区有未提交改动,请先提交并 push' } if (input.localHead.trim() !== input.remoteHead.trim()) return { ok: false, reason: '本地分支与远端不一致,请先 push' } return { ok: true } } /** * 共享墓碑:只写 handoffAttachmentId / handoffFrom 两个共享字段。本机 * sid/profile/worktreePath 墓碑要等本机清理跑完(成功指派后)才落盘, * 见 handoffLocalTombstones —— 否则④/⑤失败回滚时本机字段已经被清空, * 用户会话恢复不了。 */ export function handoffStartedStateExtra(attachmentId: number, from: string): Record { return { handoffAttachmentId: String(attachmentId), handoffFrom: from } } /** * 本机墓碑:移交流程最后一步(本机清理跑完之后)才调用。worktree 删除失败时 * 保留 worktreePath(keepWorktreePath),别把用户还留在磁盘上的 worktree * 从 state 里抹掉。 */ export function handoffLocalTombstones(opts: { keepWorktreePath: boolean }): Record { const extra: Record = {} for (const f of LOCAL_STATE_FIELDS) { if (f === 'worktreePath' && opts.keepWorktreePath) continue extra[f] = '' } return extra } export function handoffStartedUiPatch(attachmentId: number, from: string, to: string, opts: { keepWorktreePath: boolean }): Record { return { handoffAttachmentId: String(attachmentId), handoffFrom: from, assignees: [to], sessionId: null, implementSessionId: null, reviewSessionId: null, testSessionId: null, prototypeSessionId: null, profilePath: null, brainstormProfilePath: null, testProfilePath: null, prototypeProfilePath: null, prDiffFile: null, // worktree 没删掉时(opts.keepWorktreePath)不碰这两个字段,让 UI 继续显示孤儿 worktree。 ...(opts.keepWorktreePath ? {} : { worktreePath: null, worktreeExists: false }), } } function definedEntries(o: Record): Record { const out: Record = {} for (const [k, v] of Object.entries(o)) { if (v) out[k] = v } return out } export function handoffAcceptedStateExtra(local: { sessions: HandoffSessions profiles: HandoffProfiles worktreePath?: string }): Record { return { handoffAttachmentId: '', handoffFrom: '', ...definedEntries(local.sessions), ...definedEntries(local.profiles), ...(local.worktreePath ? { worktreePath: local.worktreePath } : {}), } } export function handoffAcceptedUiPatch(local: { sessions: HandoffSessions profiles: HandoffProfiles worktreePath?: string worktreeExists: boolean reviewSessionFileExists?: boolean }): Record { return { handoffAttachmentId: null, handoffFrom: null, ...definedEntries(local.sessions), ...definedEntries(local.profiles), // 显式 null 而非省略:省略会让 webview 侧留着接管前的旧 worktreePath, // 与 worktreeExists: false 对不上。 worktreePath: local.worktreePath ?? null, worktreeExists: local.worktreeExists, ...(local.reviewSessionFileExists !== undefined ? { reviewSessionFileExists: local.reviewSessionFileExists } : {}), } }