照测试会话那条链路补一份 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
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
/**
|
|
* 机器本地的工单状态(会话 id、worktree 路径、profile 路径等)。
|
|
*
|
|
* 这些字段描述"这台机器上"的会话与文件系统,跨机毫无语义(别人机器的
|
|
* 绝对路径/会话 id 在本机不可用),写进 Gitea 共享评论只会互相污染——
|
|
* 所以落在 workspaceState,按 `source:number` 键控。共享评论里的旧数据
|
|
* 仍按读兜底(本地无记录时透出),写侧从此只产生本地记录。
|
|
*/
|
|
|
|
import type { ExtensionContext } from 'vscode'
|
|
import type { Issue } from '../gitea/types'
|
|
import * as fs from 'node:fs'
|
|
import { resolveWorktreePath } from '../git/worktree'
|
|
|
|
export const LOCAL_STATE_FIELDS = [
|
|
'sessionId',
|
|
'implementSessionId',
|
|
'reviewSessionId',
|
|
'testSessionId',
|
|
'prototypeSessionId',
|
|
'profilePath',
|
|
'brainstormProfilePath',
|
|
'testProfilePath',
|
|
'prototypeProfilePath',
|
|
'worktreePath',
|
|
'prDiffFile',
|
|
] as const
|
|
|
|
const KEY = 'superpowers.localIssueState'
|
|
|
|
type Store = Record<string, Record<string, string>>
|
|
|
|
function refKey(source: string | undefined, issueNumber: number): string {
|
|
return `${source ?? 'gitea'}:${issueNumber}`
|
|
}
|
|
|
|
export function getLocalIssueState(
|
|
ctx: ExtensionContext,
|
|
source: string | undefined,
|
|
issueNumber: number,
|
|
): Record<string, string> {
|
|
const store = ctx.workspaceState.get<Store>(KEY) ?? {}
|
|
return store[refKey(source, issueNumber)] ?? {}
|
|
}
|
|
|
|
/**
|
|
* 空字符串是墓碑:字段已在本机清除,读侧不得回落到共享评论里的旧值
|
|
* (与 state JSON "空串=unset" 的既有约定一致)。null/undefined 视作清除。
|
|
*/
|
|
export async function mergeLocalIssueState(
|
|
ctx: ExtensionContext,
|
|
source: string | undefined,
|
|
issueNumber: number,
|
|
patch: Record<string, unknown>,
|
|
): Promise<void> {
|
|
const store = { ...(ctx.workspaceState.get<Store>(KEY) ?? {}) }
|
|
const key = refKey(source, issueNumber)
|
|
const next = { ...(store[key] ?? {}) }
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
if (typeof v === 'string')
|
|
next[k] = v
|
|
else if (v === null || v === undefined)
|
|
next[k] = ''
|
|
}
|
|
store[key] = next
|
|
await ctx.workspaceState.update(KEY, store)
|
|
}
|
|
|
|
/** 把一次状态写入分成本地字段与共享字段两半。 */
|
|
export function splitLocalStateFields(extra: Record<string, unknown>): {
|
|
local: Record<string, unknown>
|
|
shared: Record<string, unknown>
|
|
} {
|
|
const localSet = new Set<string>(LOCAL_STATE_FIELDS)
|
|
const local: Record<string, unknown> = {}
|
|
const shared: Record<string, unknown> = {}
|
|
for (const [k, v] of Object.entries(extra)) {
|
|
if (localSet.has(k))
|
|
local[k] = v
|
|
else
|
|
shared[k] = v
|
|
}
|
|
return { local, shared }
|
|
}
|
|
|
|
/**
|
|
* 把本地记录叠加到加载出的工单列表上:本地有键即生效('' 墓碑 → 置空)。
|
|
* 覆盖 worktreePath 时同步重算 worktreeExists,口径与 buildIssue 一致。
|
|
*/
|
|
export function overlayLocalIssueState(
|
|
ctx: ExtensionContext,
|
|
issues: Issue[],
|
|
workspaceRoot?: string,
|
|
): Issue[] {
|
|
const store = ctx.workspaceState.get<Store>(KEY) ?? {}
|
|
return issues.map((issue) => {
|
|
const local = store[refKey(issue.source, issue.number)]
|
|
if (!local)
|
|
return issue
|
|
const out = { ...issue } as Issue & Record<string, unknown>
|
|
for (const field of LOCAL_STATE_FIELDS) {
|
|
const v = local[field]
|
|
if (v === undefined)
|
|
continue
|
|
if (v === '')
|
|
delete out[field]
|
|
else
|
|
out[field] = v
|
|
if (field === 'worktreePath') {
|
|
delete out.worktreeExists
|
|
if (v !== '' && workspaceRoot) {
|
|
try {
|
|
out.worktreeExists = fs.existsSync(resolveWorktreePath(v, workspaceRoot))
|
|
}
|
|
catch {
|
|
out.worktreeExists = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
})
|
|
}
|