import { randomUUID } from "node:crypto"; import { mkdir, open, rename, rm, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { DynamicBorder, getAgentDir, getSelectListTheme, getSettingsListTheme, type ExtensionContext, } from "@earendil-works/pi-coding-agent"; import { clampThinkingLevel } from "@earendil-works/pi-ai"; import { Container, SelectList, SettingsList, Text, truncateToWidth, type SelectItem, type SettingItem, } from "@earendil-works/pi-tui"; import { sanitizeTerminalText } from "./patch.ts"; const SETTINGS_VERSION = 1; const SETTINGS_FILE_NAME = "pi-task-delta.json"; const MAX_SETTINGS_BYTES = 64 * 1024; export type EffectiveEffort = NonNullable; export type EffortPreference = "current" | EffectiveEffort; export type ModelPreference = | { mode: "current" } | { mode: "fixed"; provider: string; id: string }; export interface TaskDeltaSettings { version: typeof SETTINGS_VERSION; model: ModelPreference; effort: EffortPreference; } export interface ExplanationSelection { model: NonNullable | undefined; provider: string; modelId: string; modelName: string; effort: EffectiveEffort; attribution: string; } const EFFORTS: EffectiveEffort[] = [ "off", "minimal", "low", "medium", "high", "xhigh", "max", ]; const DEFAULT_SETTINGS: TaskDeltaSettings = { version: SETTINGS_VERSION, model: { mode: "current" }, effort: "current", }; function cloneDefaultSettings(): TaskDeltaSettings { return { version: SETTINGS_VERSION, model: { mode: "current" }, effort: "current", }; } function isShortString(value: unknown): value is string { return typeof value === "string" && value.length > 0 && Buffer.byteLength(value, "utf8") <= 1_024; } function isTaskDeltaSettings(value: unknown): value is TaskDeltaSettings { if (!value || typeof value !== "object") return false; const settings = value as Partial; if (settings.version !== SETTINGS_VERSION) return false; if (settings.effort !== "current" && !EFFORTS.includes(settings.effort as EffectiveEffort)) { return false; } if (!settings.model || typeof settings.model !== "object") return false; if (settings.model.mode === "current") return true; return ( settings.model.mode === "fixed" && isShortString(settings.model.provider) && isShortString(settings.model.id) ); } export function getTaskDeltaSettingsPath(): string { return join(getAgentDir(), SETTINGS_FILE_NAME); } export async function loadTaskDeltaSettings(): Promise { const path = getTaskDeltaSettingsPath(); let file; try { file = await open(path, "r"); } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return cloneDefaultSettings(); throw error; } try { const stat = await file.stat(); if (stat.size > MAX_SETTINGS_BYTES) { throw new Error(`Task Delta settings exceed ${MAX_SETTINGS_BYTES} bytes.`); } const contents = await file.readFile({ encoding: "utf8" }); if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) { throw new Error(`Task Delta settings exceed ${MAX_SETTINGS_BYTES} bytes.`); } const parsed: unknown = JSON.parse(contents); if (!isTaskDeltaSettings(parsed)) throw new Error("Task Delta settings are invalid."); return { version: SETTINGS_VERSION, model: parsed.model.mode === "current" ? { mode: "current" } : { mode: "fixed", provider: parsed.model.provider, id: parsed.model.id }, effort: parsed.effort, }; } finally { await file.close(); } } export async function saveTaskDeltaSettings(settings: TaskDeltaSettings): Promise { if (!isTaskDeltaSettings(settings)) throw new Error("Refusing to save invalid Task Delta settings."); const normalized: TaskDeltaSettings = { version: SETTINGS_VERSION, model: settings.model.mode === "current" ? { mode: "current" } : { mode: "fixed", provider: settings.model.provider, id: settings.model.id }, effort: settings.effort, }; const path = getTaskDeltaSettingsPath(); const directory = dirname(path); const temporaryPath = `${path}.${process.pid}.${randomUUID()}.tmp`; await mkdir(directory, { recursive: true, mode: 0o700 }); try { await writeFile(temporaryPath, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600, }); await rename(temporaryPath, path); } finally { await rm(temporaryPath, { force: true }); } } export function resolveExplanationSelection( settings: TaskDeltaSettings, ctx: ExtensionContext, ): ExplanationSelection { const requestedEffort = settings.effort === "current" ? (ctx.thinkingLevel ?? "off") : settings.effort; let model: ExplanationSelection["model"]; if (settings.model.mode === "current") { model = ctx.model; } else { const { provider, id } = settings.model; model = ctx.modelRegistry.find(provider, id); } const effort = model ? clampThinkingLevel(model, requestedEffort) : requestedEffort; const provider = model?.provider ?? ( settings.model.mode === "fixed" ? settings.model.provider : "no-provider" ); const modelId = model?.id ?? ( settings.model.mode === "fixed" ? settings.model.id : "no-model" ); const modelName = model?.name || modelId; return { model, provider, modelId, modelName, effort, attribution: `${modelName} • ${effort}`, }; } function modelLabel(model: NonNullable): string { return sanitizeTerminalText(`${model.name} — ${model.provider}/${model.id}`); } function selectionSubmenu( items: SelectItem[], currentValue: string, maxVisible: number, done: (selectedValue?: string) => void, ): SelectList { const list = new SelectList( items, Math.min(items.length, maxVisible), getSelectListTheme(), ); const selectedIndex = items.findIndex((item) => item.value === currentValue); if (selectedIndex >= 0) list.setSelectedIndex(selectedIndex); list.onSelect = (item) => done(item.value); list.onCancel = () => done(); return list; } export async function showTaskDeltaSettings( ctx: ExtensionContext, currentSettings: TaskDeltaSettings, ): Promise { try { await ctx.modelRegistry.refresh(); } catch (error) { const message = error instanceof Error ? error.message : String(error); ctx.ui.notify(`Could not refresh available models: ${message}`, "warning"); } const availableModels = ctx.modelRegistry .getAvailable() .slice() .sort((left, right) => ( left.provider.localeCompare(right.provider) || left.name.localeCompare(right.name) )); const currentModelLabel = ctx.model ? `Current Pi model — ${sanitizeTerminalText(ctx.model.name)}` : "Current Pi model — none selected"; const modelChoices = new Map(); modelChoices.set(currentModelLabel, { mode: "current" }); for (const model of availableModels) { modelChoices.set(modelLabel(model), { mode: "fixed", provider: model.provider, id: model.id, }); } const fixedPreference = currentSettings.model.mode === "fixed" ? currentSettings.model : undefined; if ( fixedPreference && !availableModels.some( (model) => model.provider === fixedPreference.provider && model.id === fixedPreference.id, ) ) { modelChoices.set( `Unavailable — ${sanitizeTerminalText(`${fixedPreference.provider}/${fixedPreference.id}`)}`, fixedPreference, ); } const effortChoices = new Map(); effortChoices.set( `Current Pi effort — ${ctx.thinkingLevel ?? "off"}`, "current", ); for (const effort of EFFORTS) { effortChoices.set(effort[0]!.toUpperCase() + effort.slice(1), effort); } const valueForModel = (): string => { if (currentSettings.model.mode === "current") return currentModelLabel; for (const [label, preference] of modelChoices) { if ( preference.mode === "fixed" && preference.provider === currentSettings.model.provider && preference.id === currentSettings.model.id ) { return label; } } return `${currentSettings.model.provider}/${currentSettings.model.id}`; }; const valueForEffort = (): string => { if (currentSettings.effort === "current") { return `Current Pi effort — ${ctx.thinkingLevel ?? "off"}`; } return currentSettings.effort[0]!.toUpperCase() + currentSettings.effort.slice(1); }; const draft: TaskDeltaSettings = { version: SETTINGS_VERSION, model: { ...currentSettings.model }, effort: currentSettings.effort, }; return ctx.ui.custom((tui, theme, _keybindings, done) => { const maxVisible = Math.max(2, Math.min(14, tui.terminal.rows - 10)); const modelItems: SelectItem[] = [...modelChoices.keys()].map((label) => ({ value: label, label, description: label === currentModelLabel ? "Follow the model selected in Pi" : "Always use this model for Task Delta explanations", })); const effortItems: SelectItem[] = [...effortChoices.keys()].map((label) => ({ value: label, label, description: label.startsWith("Current Pi effort") ? "Follow Pi's current reasoning effort" : "Use this effort for Task Delta explanations", })); const items: SettingItem[] = [ { id: "model", label: "Analysis model", description: "Model used when E or W generates file analysis", currentValue: valueForModel(), submenu: (value, submenuDone) => selectionSubmenu( modelItems, value, maxVisible, submenuDone, ), }, { id: "effort", label: "Analysis effort", description: "Reasoning effort used only for Task Delta model requests", currentValue: valueForEffort(), submenu: (value, submenuDone) => selectionSubmenu( effortItems, value, maxVisible, submenuDone, ), }, ]; const container = new Container(); container.addChild(new DynamicBorder((text) => theme.fg("accent", text))); container.addChild(new Text(theme.fg("accent", theme.bold("Task Delta Settings")), 1, 0)); const settingsList = new SettingsList( items, 8, getSettingsListTheme(), (id, newValue) => { if (id === "model") { const preference = modelChoices.get(newValue); if (preference) draft.model = { ...preference }; } else if (id === "effort") { const preference = effortChoices.get(newValue); if (preference) draft.effort = preference; } }, () => done(undefined), ); container.addChild(settingsList); container.addChild(new Text(theme.fg("dim", "S save • Esc cancel"), 1, 0)); container.addChild(new DynamicBorder((text) => theme.fg("accent", text))); return { render(width: number) { const safeWidth = Math.max(0, Math.floor(width)); if (safeWidth < 12) { return [truncateToWidth( theme.fg("muted", "Widen terminal for Task Delta settings"), safeWidth, "", )]; } return container.render(safeWidth).map( (line) => truncateToWidth(line, safeWidth, ""), ); }, invalidate() { container.invalidate(); }, handleInput(data: string) { if (data === "s" || data === "S") { done(draft); return; } settingsList.handleInput(data); tui.requestRender(); }, }; }, { overlay: true, overlayOptions: { anchor: "center", width: "75%", minWidth: 60, maxHeight: "80%", margin: 2, }, }); } export { DEFAULT_SETTINGS };