import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { AutocompleteProvider, AutocompleteSuggestions } from "@earendil-works/pi-tui"; import { fileURLToPath } from "node:url"; import { resolveWorkspaceIdentity } from "./workspace.ts"; const ENTRY = "delivery-activation"; type Activation = { enabled: boolean; tools?: string[] }; type Runtime = { initialize(): Promise; showStatus(ctx: ExtensionContext): Promise; assertCanExit(ctx: ExtensionContext): Promise; cleanup?(ctx: ExtensionContext): Promise }; function prioritizeDeliveryShape(suggestions: AutocompleteSuggestions | null): AutocompleteSuggestions | null { if (!suggestions) return suggestions; const shapeIndex = suggestions.items.findIndex((item) => item.value === "delivery-shape"); const firstDeliveryIndex = suggestions.items.findIndex((item) => item.value.startsWith("delivery-")); if (shapeIndex < 0 || firstDeliveryIndex < 0 || shapeIndex <= firstDeliveryIndex) return suggestions; const items = [...suggestions.items]; const [shape] = items.splice(shapeIndex, 1); items.splice(firstDeliveryIndex, 0, shape!); return { ...suggestions, items }; } function shapeFirstAutocomplete(current: AutocompleteProvider): AutocompleteProvider { return { triggerCharacters: current.triggerCharacters, getSuggestions: async (...args: Parameters) => prioritizeDeliveryShape(await current.getSuggestions(...args)), applyCompletion: (...args: Parameters) => current.applyCompletion(...args), ...(current.shouldTriggerFileCompletion ? { shouldTriggerFileCompletion: (...args: Parameters>) => current.shouldTriggerFileCompletion!(...args) } : {}), }; } // 只保存用户选择的入口和原工具集合,不保存或恢复批准。 export function installActivation(pi: ExtensionAPI, start: (ctx: ExtensionContext) => Runtime): void { let runtime: Runtime | undefined; let originalTools: string[] = []; let restoreTools: string[] | undefined; let changing = false; let autocompleteInstalled = false; const enter = async (ctx: ExtensionContext) => { runtime ??= start(ctx); await runtime.initialize(); }; pi.on("session_start", async (event, ctx) => { if (ctx.hasUI && !autocompleteInstalled) { ctx.ui.addAutocompleteProvider(shapeFirstAutocomplete); autocompleteInstalled = true; } const entry = ctx.sessionManager.getEntries().findLast((row) => row.type === "custom" && row.customType === ENTRY); if (entry?.type !== "custom") return; const state = entry.data as Activation; if (state.enabled) { originalTools = state.tools ?? []; await enter(ctx); if (event.reason === "reload") ctx.ui.notify("交付扩展已重载。旧实施确认已失效;重新确认方案后再继续。需要退出时使用 /delivery-status。", "info"); } else if (state.tools) { restoreTools = state.tools; } }); // 此事件在所有 session_start 钩子之后运行,避免原插件启动时覆盖用户停用选择。 pi.on("resources_discover", () => { if (!restoreTools) return; pi.setActiveTools(restoreTools.filter((name) => pi.getAllTools().some((tool) => tool.name === name))); pi.appendEntry(ENTRY, { enabled: false }); restoreTools = undefined; }); pi.registerCommand("delivery-status", { description: "查看交付进度和详情,结束交付", handler: async (_args, ctx) => { if (!runtime) { ctx.ui.notify("交付未启用,当前沿用 Pi 原有工具。使用 /delivery-shape 进入交付流程。", "info"); return; } if (!await runtime.showStatus(ctx)) return; if (changing || !ctx.isIdle() || ctx.hasPendingMessages()) { ctx.ui.notify("暂不能结束交付:仍有任务或占用未完成。先说“继续”完成恢复,或等待当前回合结束。", "warning"); return; } changing = true; try { await runtime.assertCanExit(ctx); await runtime.cleanup?.(ctx); } catch (error) { changing = false; ctx.ui.notify(`暂不能结束交付:${String(error)}`, "warning"); return; } pi.appendEntry(ENTRY, { enabled: false, tools: originalTools }); pi.sendMessage({ customType: "delivery-mode", content: "用户已在状态面板中选择结束本轮受控交付。后续普通请求沿用 Pi 原有工具与项目规则,不再要求交付阶段确认;旧交付记录只供查阅,不授予新权限。", display: false }, { triggerTurn: false }); pi.setActiveTools(originalTools); ctx.ui.notify("交付已结束,正在重载并恢复原工具;执行记录保留,旧批准不再有效。", "info"); await ctx.reload(); }, }); pi.registerCommand("delivery-shape", { description: "启用当前会话的受控交付,可附带需求", handler: async (args, ctx) => { if (changing || !ctx.isIdle() || ctx.hasPendingMessages()) { ctx.ui.notify("请等待当前回合和排队消息结束后再进入交付。", "warning"); return; } changing = true; try { // 交付以 worktree 为单位记录 writer;非 Git 目录在首次批准或写入时才失败会误导用户。 try { await resolveWorkspaceIdentity(ctx.cwd); } catch (error) { ctx.ui.notify(`交付需要 Git 仓库(writer lease 以 worktree 为单位);当前目录未能确认 Git 工作区:${error instanceof Error ? error.message : String(error)}\n继续使用普通 Pi,未启用交付。`, "warning"); return; } if (!runtime) { originalTools = pi.getActiveTools(); pi.appendEntry(ENTRY, { enabled: true, tools: originalTools }); await enter(ctx); } ctx.ui.notify("交付已启用。确认方案后会自动实施;中断后直接说“继续”。需要退出时使用 /delivery-status。", "info"); if (args.trim()) pi.sendUserMessage(`先读取并遵循 ${fileURLToPath(new URL("../../../skills/adaptive-delivery/SKILL.md", import.meta.url))},核实项目事实并对齐需求;明确需求可以零追问,简单任务无须规划文档。当前需求:\n${args}`, { expandPromptTemplates: false }); } finally { changing = false; } }, }); }