Files
superwork/vscode/src/panel/handlers/handoff.ts
T
cruldra b62e58d739 ✨ feat(vscode): 新增第五种工单会话「原型会话」
照测试会话那条链路补一份 prototype:

- 消息与路由:session/start-prototype、session/resume-prototype、
  issue/update-prototype-profile-path,都进只读门禁
- 会话:handleStartPrototypeSession / handleResumePrototypeSession,
  终端名 issue-N-原型;cwd 有 worktree 用 worktree、没有退主仓库根
  (原 resolveTestSessionCwd 改名 resolveWorktreeOrRootCwd 两种会话共用)
- 不要求有 PR,任何阶段都能起;前置工单锁仍然拦
- 提示词:新增 prompts/prototype.md,设置里可改,占位符 {issueNumber}
- profile:工单级 prototypeProfilePath > 设置里的原型默认 profile >
  内置默认,与其它会话互不回退
- 状态字段 prototypeSessionId / prototypeProfilePath / prototypeTabOpen
  贯通 gitea、youtrack、本机 state 与移交包
- 详情面板加「原型会话id」「原型配置文件」两行,设置面板加对应两项

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-14 10:20:30 +08:00

127 lines
4.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, unknown> {
return { handoffAttachmentId: String(attachmentId), handoffFrom: from }
}
/**
* 本机墓碑:移交流程最后一步(本机清理跑完之后)才调用。worktree 删除失败时
* 保留 worktreePath(keepWorktreePath),别把用户还留在磁盘上的 worktree
* 从 state 里抹掉。
*/
export function handoffLocalTombstones(opts: { keepWorktreePath: boolean }): Record<string, string> {
const extra: Record<string, string> = {}
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<string, unknown> {
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<string, any>): Record<string, string> {
const out: Record<string, string> = {}
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<string, unknown> {
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<string, unknown> {
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 } : {}),
}
}