/** * 机器本地的工单状态(会话 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> function refKey(source: string | undefined, issueNumber: number): string { return `${source ?? 'gitea'}:${issueNumber}` } export function getLocalIssueState( ctx: ExtensionContext, source: string | undefined, issueNumber: number, ): Record { const store = ctx.workspaceState.get(KEY) ?? {} return store[refKey(source, issueNumber)] ?? {} } /** * 空字符串是墓碑:字段已在本机清除,读侧不得回落到共享评论里的旧值 * (与 state JSON "空串=unset" 的既有约定一致)。null/undefined 视作清除。 */ export async function mergeLocalIssueState( ctx: ExtensionContext, source: string | undefined, issueNumber: number, patch: Record, ): Promise { const store = { ...(ctx.workspaceState.get(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): { local: Record shared: Record } { const localSet = new Set(LOCAL_STATE_FIELDS) const local: Record = {} const shared: Record = {} 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(KEY) ?? {} return issues.map((issue) => { const local = store[refKey(issue.source, issue.number)] if (!local) return issue const out = { ...issue } as Issue & Record 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 }) }