/** * @pi-unipi/fusion — extension entry * * Commands * /unipi:model Devin-style picker over the curated preset * /unipi:settings → Fusion → "Edit fusion presets…" Curate the preset * * Autocomplete: when the user types `/model`, `/unipi:model` is pinned as the * first suggestion (pi's own `/model` cannot be overridden by extensions). * * Runtime (this package, later step): the `sidekick` tool + lead policy. * For now, confirming a Fusion pair sets the lead as the session model and * records the pair; the footer shows `Fusion · Lead ◆ Sidekick`. */ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; import type { Api, Model } from "@earendil-works/pi-ai"; import type { AutocompleteProvider, AutocompleteSuggestions } from "@earendil-works/pi-tui"; import { createSpinnerLine, recordChildUsage, harnessToolResultDetails, registerCommandRunner, stateDir, UNIPI_EVENTS, UNIPI_PREFIX, HUB_OVERLAY_OPTIONS, HUB_PICKER_OVERLAY_OPTIONS, bus } from "@pi-unipi/core"; import { join } from "node:path"; import { effortLabel, globalPresetPath, isEffortLevel, loadPreset, mirrorStartupFromPi, modelKey, pushRecent, saveCuration, saveRuntimeState, splitModelKey, type ActiveSelection, type EffortLevel, type FusionPreset, } from "./preset.js"; import { ModelPicker, type PickerModel, type PickerResult } from "./picker.js"; import { PresetEditor, type PresetEditorResult } from "./preset-editor.js"; import { SidekickRuntime } from "./sidekick-runtime.js"; import { estimateSavings } from "./savings.js"; import { EDIT_NUDGE, bashNudge, leadPolicy, sidekickSystemPrompt, type FusionIdentity } from "./prompts.js"; import { isTrivialShell, BASH_NUDGE_EVERY } from "./nudge.js"; import { registerFusionTools } from "./tools.js"; import { duration, renderSidekickStep } from "./transcript.js"; import { persistDefaultModel, piSettingsPath } from "./pi-settings.js"; import { ensureReadSubagentTool, setReadSubagentDemand } from "@pi-unipi/core/child-agent.js"; import { readFileSync } from "node:fs"; import { publishFusionApi, type FusionApiModel, type FusionApiPicker } from "./api.js"; export const MODEL_COMMAND = `${UNIPI_PREFIX}model`; export function sidekickSessionPath(leadSessionId?: string): string { // Session-scoped: the sidekick transcript is ephemeral child state that dies // with the lead session. Living under sessions//fusion/ means the // startup orphan-sweep reaps it after a crash — no more unbounded pile of // abandoned sidekick .jsonl files. return join(stateDir("fusion", "session"), "sidekick", `${leadSessionId ?? "default"}.jsonl`); } export const SIDEKICK_WAKE_WIDGET_KEY = "fusion-sidekick-wake"; /** * The lead is idle when pi says so. Any failure (including a context without * `isIdle`) counts as idle — same contract as the background-tasks wake line. */ export function isLeadIdle(ctx: { isIdle(): boolean }): boolean { try { return ctx.isIdle(); } catch { return true; } } /** Body of the live line. `undefined` collapses it without disposing the widget. */ export function sidekickWakeText(progress: { toolCalls: number; startedAt: number } | undefined): string | undefined { if (!progress) return undefined; return `sidekick working · ${String(progress.toolCalls)} tool calls · ${duration(Date.now() - progress.startedAt)} — resumes automatically when done`; } export interface SidekickWakeLine { publish(ctx: ExtensionContext | undefined): void; clear(ctx: ExtensionContext | undefined): void; } /** * Self-animating "sidekick still working" line, shown while the sidekick is * busy but the lead's turn has already ended (pi stopped its own loader). * * The widget owns an 80 ms frame timer, so it is installed ONCE while the * condition holds and removed when it stops — never re-installed per progress * tick. */ export function createSidekickWakeLine(deps: { isBusy: () => boolean; progress: () => { toolCalls: number; startedAt: number } | undefined; }): SidekickWakeLine { let installed = false; const removable = (ctx: ExtensionContext | undefined): ctx is ExtensionContext => Boolean(ctx?.hasUI) && typeof ctx?.ui.setWidget === "function"; // UNI-162: the herdr `working` claim while this line is up used to be made // HERE (key "fusion-sidekick"). It is now the core pending-work monitor's // job (installPendingWorkMonitor), reading the "fusion" wait source // (tools.ts's BackgroundHandoffTracker, which already covers exactly this // case — a detached/non-blocking handoff still running) — one claim across // bg wake + subagents + fusion instead of three. return { publish(ctx) { if (!removable(ctx)) return; const want = deps.isBusy() && isLeadIdle(ctx); if (want && !installed) { ctx.ui.setWidget( SIDEKICK_WAKE_WIDGET_KEY, createSpinnerLine({ text: () => sidekickWakeText(deps.progress()) }), { placement: "aboveEditor" }, ); installed = true; } else if (!want && installed) { ctx.ui.setWidget(SIDEKICK_WAKE_WIDGET_KEY, undefined); installed = false; } }, clear(ctx) { if (!installed) return; installed = false; if (!removable(ctx)) return; ctx.ui.setWidget(SIDEKICK_WAKE_WIDGET_KEY, undefined); }, }; } type Registry = { getAvailable(): Model[]; find(provider: string, id: string): Model | undefined }; /** Keep /unipi:settings → Fusion → Startup showing pi's real default. */ function mirrorStartup(cwd: string): void { try { mirrorStartupFromPi(cwd); } catch { /* display mirror only */ } } function registryOf(ctx: ExtensionContext): Registry | undefined { const r = (ctx as unknown as { modelRegistry?: Registry }).modelRegistry; return r && typeof r.getAvailable === "function" ? r : undefined; } /** Key-based lookup (provider ids can contain slashes, so splitModelKey+find is unreliable). */ const modelBykey = new Map>(); function findModel(reg: Registry | undefined, key: string): Model | undefined { // Refill on a miss too: providers (e.g. bridges) can register models after the first lookup. if (!modelBykey.has(key) && reg) { modelBykey.clear(); for (const m of reg.getAvailable()) modelBykey.set(modelKey(m), m); } return modelBykey.get(key); } function costOf(m: Model | undefined, override?: PickerModel["cost"]): PickerModel["cost"] { const cost = override ?? (m?.cost && typeof m.cost.input === "number" ? { input: m.cost.input, cachedInput: m.cost.cacheRead ?? 0, output: m.cost.output } : undefined); return cost && (cost.input > 0 || cost.cachedInput > 0 || cost.output > 0) ? cost : undefined; } function toPickerModel(m: Model, badge?: FusionPreset["badges"][string], override?: PickerModel["cost"]): PickerModel { return { key: modelKey(m), name: m.name || m.id, provider: m.provider, badge, cost: costOf(m, override), reasoning: Boolean(m.reasoning), }; } function currentEffort(ctx: ExtensionContext, pi: ExtensionAPI): EffortLevel { const fromCtx = (ctx as { thinkingLevel?: unknown }).thinkingLevel; if (isEffortLevel(fromCtx)) return fromCtx; try { const level = pi.getThinkingLevel(); if (isEffortLevel(level)) return level; } catch { /* not available in this context */ } return "medium"; } /** Pin `/unipi:model` first whenever the user is typing `/model…`. */ export function createModelBoostProvider(current: AutocompleteProvider): AutocompleteProvider { return { ...current, async getSuggestions(lines, cursorLine, cursorCol, options): Promise { const base = await current.getSuggestions(lines, cursorLine, cursorCol, options); const text = (lines[cursorLine] ?? "").slice(0, cursorCol); if (!/^\/m(o(d(e(l)?)?)?)?$/iu.test(text)) return base; if (!base) return base; const idx = base.items.findIndex((i) => i.value === MODEL_COMMAND); if (idx <= 0) return base; const items = [...base.items]; const [boosted] = items.splice(idx, 1); if (boosted === undefined) return base; return { ...base, items: [boosted, ...items] }; }, }; } export const FUSION_TOOLS = ["sidekick"] as const; /** * Adds the fusion tools to pi's active set while Fusion is active, removes * them otherwise — touching ONLY `sidekick`/`read_subagent`, never another * module's tools. Compares against the live active set (pi rebuilds it on * session changes), and skips `setActiveTools` when nothing would change. */ export function makeFusionToolSync(pi: Pick): (want: boolean) => void { return (want: boolean) => { const current = pi.getActiveTools(); if (FUSION_TOOLS.every((tool) => current.includes(tool) === want)) return; const set = new Set(current); for (const tool of FUSION_TOOLS) { if (want) set.add(tool); else set.delete(tool); } pi.setActiveTools([...set]); }; } export default function fusionExtension(pi: ExtensionAPI): void { if (process.env.UNIPI_FUSION_CHILD === "1") return; let active: ActiveSelection | undefined; let runtime: SidekickRuntime | undefined; let lastCtx: ExtensionContext | undefined; let leadToolCalls = 0; let editNudgedThisTurn = false; let bashStreak = 0; // The shell-delegation nudge fires at most ONCE PER HANDOFF GAP: the latch // survives new prompts (before_agent_start restarts only the streak) and // clears when a sidekick/read_subagent handoff completes. The nudge means // "non-trivial shell work since the last handoff" — repeating it after a // mere new prompt says nothing the lead hasn't just been told (it made // Fusion sessions warn on nearly every shell-heavy prompt). let bashNudgedSinceHandoff = false; const wakeLine = createSidekickWakeLine({ isBusy: () => runtime?.isBusy() === true, progress: () => runtime?.progress(), }); const syncTools = makeFusionToolSync(pi); // `read_subagent` presence is demand-driven across owners (core registry): // fusion wants it only while a Fusion pair is active. const syncFusionTools = () => { const fusion = active?.kind === "fusion"; syncTools(fusion); setReadSubagentDemand(pi, "fusion", fusion); }; ensureReadSubagentTool(pi); function identity(ctx: ExtensionContext): FusionIdentity { const reg = registryOf(ctx); const names = (k: string) => findModel(reg, k)?.name || splitModelKey(k)?.id || k; return { leadName: names(active?.kind === "fusion" ? active.lead : ""), leadEffort: active?.kind === "fusion" ? effortLabel(active.leadEffort ?? "medium") : "", sidekickName: names(active?.kind === "fusion" ? active.sidekick : ""), sidekickEffort: active?.kind === "fusion" ? effortLabel(active.sidekickEffort ?? "medium") : "", }; } function statusSavings(ctx: ExtensionContext): number | undefined { if (active?.kind !== "fusion" || runtime === undefined) return undefined; const reg = registryOf(ctx); const preset = loadPreset(ctx.cwd ?? process.cwd()).preset; const lead = findModel(reg, active.lead); const side = findModel(reg, active.sidekick); return estimateSavings(runtime.usage, costOf(lead, preset.prices[active.lead]), costOf(side, preset.prices[active.sidekick])).savedUsd; } function publishStatus(ctx: ExtensionContext): void { lastCtx = ctx; const reg = registryOf(ctx); const names = (k: string) => findModel(reg, k)?.name || splitModelKey(k)?.id || k; if (active?.kind === "fusion") { // Displayed in the input-box model slot (footer glance frame): // the working lead lit, the sidekick muted. bus.emit(UNIPI_EVENTS.FUSION_STATUS, { leadName: names(active.lead), leadKey: active.lead, sidekickKey: active.sidekick, leadEffort: active.leadEffort ?? "", sidekickName: names(active.sidekick), sidekickEffort: active.sidekickEffort ?? "", savedUsd: statusSavings(ctx), busy: runtime?.isBusy() ?? false, leadToolCalls, sidekickToolCalls: runtime?.totalToolCalls() ?? 0, }); } else { bus.emit(UNIPI_EVENTS.FUSION_STATUS, undefined); } } function publishStatusLater(ctx?: ExtensionContext): void { const target = ctx ?? lastCtx; if (target) { publishStatus(target); wakeLine.publish(target); } } function leadSessionId(ctx: ExtensionContext): string { const manager = ctx.sessionManager as { getSessionId?: () => string | undefined } | undefined; return manager?.getSessionId?.() ?? "default"; } function getRuntime(ctx: ExtensionContext): SidekickRuntime | undefined { if (active?.kind !== "fusion") return undefined; if (runtime === undefined) { runtime = new SidekickRuntime({ cwd: ctx.cwd ?? process.cwd(), model: active.sidekick, thinking: active.sidekickEffort ?? "medium", sessionFile: sidekickSessionPath(leadSessionId(ctx)), systemPrompt: sidekickSystemPrompt(identity(ctx)), onProgress: () => publishStatusLater(), onStep: (step) => { try { // Group per handoff so resumed handoffs render as separate panels // even with no lead turn between them; old entries without a // group fall back to the shared "sidekick" run. const handoffId = runtime?.latest()?.id; pi.appendEntry("sidekick-step", { ...step, group: `sidekick:${handoffId ?? "default"}`, label: "Sidekick" }); } catch { /* entry rendering never affects the handoff */ } }, // Sidekick spend belongs in the lead session's cost. onUsage: (delta) => recordChildUsage((type, data) => pi.appendEntry(type, data), { source: "fusion", id: runtime?.latest()?.id ?? "sidekick", model: active?.kind === "fusion" ? active.sidekick : undefined }, delta), }); } return runtime; } function stopRuntime(): void { wakeLine.clear(lastCtx); runtime?.kill(); runtime = undefined; leadToolCalls = 0; } function savingsStats(ctx: ExtensionContext): string { if (active?.kind !== "fusion" || runtime === undefined) return "Fusion is not active — pick a Fusion pair with /unipi:model."; const reg = registryOf(ctx); const preset = loadPreset(ctx.cwd ?? process.cwd()).preset; const leadCost = costOf(findModel(reg, active.lead), preset.prices[active.lead]); const sidekickCost = costOf(findModel(reg, active.sidekick), preset.prices[active.sidekick]); const savings = estimateSavings(runtime.usage, leadCost, sidekickCost); const pricing = leadCost === undefined && sidekickCost === undefined ? '\nPricing unavailable from provider — set "prices" in ~/.unipi/config/fusion/preset.json to estimate savings.' : ""; return `Sidekick tokens: in ${String(runtime.usage.input)} · out ${String(runtime.usage.output)} · cached ${String(runtime.usage.cacheRead)} · cache write ${String(runtime.usage.cacheWrite)}\nSidekick cost: $${savings.sidekickUsd.toFixed(2)} · at lead prices: $${savings.atLeadUsd.toFixed(2)} · saved: $${savings.savedUsd.toFixed(2)}\nHandoffs: ${String(runtime.reports.size)} · runtime alive: ${String(runtime.isAlive())} · busy: ${String(runtime.isBusy())}${pricing}`; } pi.registerEntryRenderer("sidekick-step", (entry: { data?: { group?: string; label?: string } }, options: { expanded?: boolean }, theme) => renderSidekickStep( entry.data as never, options.expanded === true, theme as never, { group: entry.data?.group ?? "sidekick", label: entry.data?.label }, )); registerFusionTools(pi, { getRuntime, onReport: (ctx) => publishStatusLater(ctx), onHandoffStart: (ctx) => publishStatus(ctx), onAttach: (ctx) => publishStatusLater(ctx), onDetach: (ctx) => publishStatusLater(ctx), }); pi.registerCommand("unipi:fusion-stats", { description: "Estimated Fusion savings (sidekick tokens priced at lead rates)", handler: async (_args, ctx) => ctx.ui.notify(savingsStats(ctx), "info"), }); // Lead policy rides as a named system-prompt SECTION so pi records the // section diff in the transcript (docs/extensions.md prefers sections over // replacing `systemPrompt`, which nukes the cached prefix). The section is // present only while Fusion is active. pi.on("before_agent_start", (event, ctx) => { bashStreak = 0; // each user prompt starts a fresh non-trivial count syncFusionTools(); // safety net: re-sync after any module re-juggled tools if (active?.kind === "fusion") { event.systemPromptOptions.sections["fusion-lead-policy"] = leadPolicy(identity(ctx)); } else { delete event.systemPromptOptions.sections["fusion-lead-policy"]; } return undefined; }); pi.on("turn_start", () => { editNudgedThisTurn = false; }); // The wake line's condition flips when the LEAD goes idle, not when the // sidekick reports progress, so re-evaluate on both turn end and settle. pi.on("turn_end", (_event, ctx) => publishStatusLater(ctx)); pi.on("agent_settled", (_event, ctx) => publishStatusLater(ctx)); pi.on("tool_result", (event) => { if (active?.kind !== "fusion") return; const toolName: string = event.toolName; if (toolName === "sidekick" || toolName === "read_subagent") { bashStreak = 0; bashNudgedSinceHandoff = false; // a completed handoff re-arms the nudge return; } leadToolCalls += 1; publishStatusLater(); if (toolName === "edit" || toolName === "write") { if (editNudgedThisTurn) return; editNudgedThisTurn = true; return { content: [...event.content, { type: "text" as const, text: EDIT_NUDGE }], details: harnessToolResultDetails( (event as { details?: unknown }).details, { source: "Fusion", title: "Delegate edits", synopsis: "Direct edit instead of sidekick delegation", severity: "warning" }, "boundary", EDIT_NUDGE, ), }; } if (toolName !== "bash") return; const command = typeof event.input.command === "string" ? event.input.command : ""; if (isTrivialShell(command)) return; if (bashNudgedSinceHandoff) return; bashStreak += 1; if (bashStreak < BASH_NUDGE_EVERY) return; bashNudgedSinceHandoff = true; const annotation = bashNudge(bashStreak); const synopsis = "Non-trivial shell work since last handoff"; const content = [...event.content, { type: "text" as const, text: annotation }]; bashStreak = 0; return { content, details: harnessToolResultDetails( (event as { details?: unknown }).details, { source: "Fusion", title: "Delegate shell work", synopsis, severity: "warning" }, "boundary", annotation, ), }; }); async function applyResult( ctx: ExtensionContext, result: PickerResult, preset: FusionPreset, loaded: { globalPath: string; projectPath: string; hasProjectLayer: boolean }, opts: { silent?: boolean } = {}, ): Promise<{ ok: true } | { ok: false; message: string }> { const notifyErr = (message: string) => { if (!opts.silent && ctx.hasUI) ctx.ui.notify(message, "error"); return { ok: false as const, message }; }; if (result.type === "cancelled") return { ok: true }; const samePair = result.type === "fusion" && active?.kind === "fusion" && active.lead === result.lead && active.sidekick === result.sidekick; const reg = registryOf(ctx); const targetKey = result.type === "single" ? result.model : result.lead; const model = findModel(reg, targetKey); if (!model) return notifyErr(`Model ${targetKey} is not available. Run /login or fix the preset.`); const ok = await pi.setModel(model); if (!ok) return notifyErr(`Could not switch to ${targetKey}.`); if (!samePair) stopRuntime(); else if ( result.type === "fusion" && active?.kind === "fusion" && active.sidekickEffort !== result.sidekickEffort ) { // Same pair, new sidekick strength: a live child gets it over rpc // (set_thinking_level); the value also lands in the spawn args for the // next process start. runtime?.setThinking(result.sidekickEffort); } const effort = result.type === "single" ? result.effort : result.leadEffort; try { pi.setThinkingLevel(effort); } catch { /* provider may not support thinking */ } active = result.type === "single" ? { kind: "single", model: result.model } : { kind: "fusion", lead: result.lead, sidekick: result.sidekick, leadEffort: result.leadEffort, sidekickEffort: result.sidekickEffort, }; const recent = pushRecent(preset.recent, targetKey); if (result.type === "fusion") { // Remember the confirmed pair as the preset default (the preset editor // never edits defaults; confirming here is the natural place). const layerPath = loaded.hasProjectLayer ? loaded.projectPath : loaded.globalPath; saveCuration(layerPath, { lead: preset.lead.includes(result.lead) ? preset.lead : [result.lead, ...preset.lead], sidekick: preset.sidekick.includes(result.sidekick) ? preset.sidekick : [result.sidekick, ...preset.sidekick], default: { lead: result.lead, sidekick: result.sidekick }, }); } saveRuntimeState(loaded.globalPath, { effort: result.effortMap, recent, active }); syncFusionTools(); publishStatus(ctx); if (!opts.silent && ctx.hasUI) { const label = result.type === "single" ? `${model.name || model.id} · ${effortLabel(effort)}` : `Fusion · ${model.name || model.id} ${effortLabel(effort)} ◆ ${ findModel(reg, result.sidekick)?.name ?? splitModelKey(result.sidekick)?.id ?? result.sidekick } ${effortLabel(result.sidekickEffort)}`; ctx.ui.notify(label, "info"); } return { ok: true }; } pi.registerCommand("unipi:model", { description: "Pick a model or a Fusion lead+sidekick pair (Devin-style picker over your preset)", handler: async (_args, ctx) => { if (!ctx.hasUI) throw new Error(`/${MODEL_COMMAND} needs the interactive TUI`); const reg = registryOf(ctx); if (!reg) { ctx.ui.notify("Model registry unavailable in this context.", "error"); return; } const cwd = ctx.cwd ?? process.cwd(); const loaded = loadPreset(cwd); const preset = loaded.preset; const models = reg.getAvailable().map((m) => toPickerModel(m, preset.badges[modelKey(m)], preset.prices[modelKey(m)])); if (models.length === 0) { ctx.ui.notify("No models available. Use /login to add a provider.", "warning"); return; } const currentKey = ctx.model ? modelKey(ctx.model) : undefined; // pi's persisted startup default (settings.json) marks the `default` row. let defaultKey: string | undefined; try { const raw = JSON.parse(readFileSync(piSettingsPath(), "utf8")) as { defaultProvider?: string; defaultModel?: string }; if (typeof raw.defaultProvider === "string" && typeof raw.defaultModel === "string") { defaultKey = `${raw.defaultProvider}/${raw.defaultModel}`; } } catch { /* no settings file yet */ } // Session truth wins over persisted state: if the user switched via pi's // own /model since, show that as the pinned row. if (active === undefined && preset.active !== undefined) active = preset.active; if (currentKey !== undefined) { if (active === undefined || (active.kind === "single" && active.model !== currentKey) || (active.kind === "fusion" && active.lead !== currentKey)) { active = { kind: "single", model: currentKey }; } } const fallbackEffort = currentEffort(ctx, pi); const result = await ctx.ui.custom( (tui, theme, _kb, done) => new ModelPicker({ state: { models, fusionLeads: preset.lead, fusionSidekicks: preset.sidekick, fusionDefault: preset.default, recent: preset.recent, active, currentModelKey: currentKey, defaultModelKey: defaultKey, effort: preset.effort, fallbackEffort, }, theme: { fg: (c, s) => theme.fg(c as never, s), bold: (s) => theme.bold(s) }, onDone: done, onSetDefault: (r) => { if (r.type === "cancelled") return; const saveDefault = (key: string, effort: string): boolean => { const parts = splitModelKey(key); if (parts === undefined) { ctx.ui.notify(`Could not save default: bad model key ${key}`, "error"); return false; } try { persistDefaultModel({ provider: parts.provider, model: parts.id, thinkingLevel: effort }); return true; } catch (error) { ctx.ui.notify(`Could not save default: ${error instanceof Error ? error.message : String(error)}`, "error"); return false; } }; if (r.type === "single") { if (saveDefault(r.model, r.effort)) ctx.ui.notify(`Default: ${r.model} · ${r.effort}`, "info"); mirrorStartup(cwd); } else if (saveDefault(r.lead, r.leadEffort)) { mirrorStartup(cwd); saveRuntimeState(loaded.globalPath, { effort: r.effortMap, recent: preset.recent, active: { kind: "fusion", lead: r.lead, sidekick: r.sidekick, leadEffort: r.leadEffort, sidekickEffort: r.sidekickEffort }, }); ctx.ui.notify(`Default: Fusion (${r.lead} + ${r.sidekick})`, "info"); } }, onRenderRequest: () => tui.requestRender(), }), HUB_PICKER_OVERLAY_OPTIONS, ); await applyResult(ctx, result, preset, loaded); }, }); // fusion-preset — hub action runner ("Edit fusion presets…" row in the // Fusion group). The old /unipi:fusion-preset slash command is gone. const PRESET_COMMAND = "unipi:fusion-preset"; registerCommandRunner(PRESET_COMMAND, async (rawCtx: unknown) => { const ctx = rawCtx as ExtensionContext; if (!ctx.hasUI) throw new Error("Edit fusion presets needs the interactive TUI"); const reg = registryOf(ctx); if (!reg) { ctx.ui.notify("Model registry unavailable in this context.", "error"); return; } const cwd = ctx.cwd ?? process.cwd(); const loaded = loadPreset(cwd); const models = reg.getAvailable().map((m) => ({ key: modelKey(m), name: m.name || m.id })); const result = await ctx.ui.custom( (tui, theme, _kb, done) => new PresetEditor({ models, initial: { lead: loaded.preset.lead, sidekick: loaded.preset.sidekick, default: loaded.preset.default }, active, initialTarget: loaded.hasProjectLayer ? "project" : "global", theme: { fg: (c, s) => theme.fg(c as never, s), bold: (s) => theme.bold(s) }, onDone: done, onRenderRequest: () => tui.requestRender(), }), HUB_OVERLAY_OPTIONS, ); if (result.type !== "saved") return; const path = result.target === "project" ? loaded.projectPath : loaded.globalPath; saveCuration(path, result.curation); ctx.ui.notify( `Saved preset → ${path}\n${String(result.curation.lead.length)} lead · ${String(result.curation.sidekick.length)} sidekick`, "info", ); }); // UI-free API (UNI-160 "session control centre"): lets the app bridge read // the preset and apply a picker result without the TUI overlay. `ctx` is // always the latest from session_start / events — events fire only while // pi is alive, so this is never stale by more than one turn. publishFusionApi({ getPicker(): FusionApiPicker | undefined { if (!lastCtx) return undefined; const cwd = lastCtx.cwd ?? process.cwd(); const loaded = loadPreset(cwd); const preset = loaded.preset; const reg = registryOf(lastCtx); const named = (keys: readonly string[]): FusionApiModel[] => keys.map((key) => ({ key, name: findModel(reg, key)?.name || splitModelKey(key)?.id || key })); const seen = new Set(); const curatedKeys = [...preset.recent, ...preset.lead, ...preset.sidekick].filter((key) => { if (seen.has(key) || !findModel(reg, key)) return false; seen.add(key); return true; }); return { leads: named(preset.lead), sidekicks: named(preset.sidekick), curated: named(curatedKeys), default: { lead: preset.default.lead, sidekick: preset.default.sidekick }, effort: preset.effort, active, }; }, async apply(result) { if (!lastCtx) return { ok: false, message: "No active pi session." }; const cwd = lastCtx.cwd ?? process.cwd(); const loaded = loadPreset(cwd); return applyResult(lastCtx, result, loaded.preset, loaded, { silent: true }); }, }); pi.on("session_start", async (_e, ctx) => { stopRuntime(); editNudgedThisTurn = false; bashStreak = 0; bashNudgedSinceHandoff = false; modelBykey.clear(); active = loadPreset(ctx.cwd ?? process.cwd()).preset.active; mirrorStartup(ctx.cwd ?? process.cwd()); if (active?.kind === "fusion" && (!ctx.model || modelKey(ctx.model) !== active.lead)) { const leadKey = active.lead; const lead = findModel(registryOf(ctx), leadKey); const restored = lead !== undefined && await pi.setModel(lead); if (restored) { try { pi.setThinkingLevel(active.leadEffort ?? "medium"); } catch { /* provider may not support thinking */ } } else { active = undefined; if (ctx.hasUI) ctx.ui.notify(`Fusion lead ${leadKey} unavailable — Fusion off`, "warning"); } } syncFusionTools(); publishStatus(ctx); if (ctx.hasUI) ctx.ui.addAutocompleteProvider(createModelBoostProvider); }); pi.on("session_shutdown", () => { stopRuntime(); bus.emit(UNIPI_EVENTS.FUSION_STATUS, undefined); }); pi.on("model_select", (event, ctx) => { // The user switched through pi's own /model or Ctrl+P: leave Fusion mode // unless the new model is still the lead. if (active?.kind === "fusion" && modelKey(event.model) !== active.lead) { stopRuntime(); active = { kind: "single", model: modelKey(event.model) }; const loaded = loadPreset(ctx.cwd ?? process.cwd()); saveRuntimeState(globalPresetPath(), { effort: loaded.preset.effort, recent: loaded.preset.recent, active }); syncFusionTools(); publishStatus(ctx); } }); } export { loadPreset } from "./preset.js"; export type { ActiveSelection } from "./preset.js";