import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Key, matchesKey } from "@earendil-works/pi-tui"; import { resolveProfileDefinition } from "../profile-catalogue.ts"; import { agentProfileCatalogueStore } from "../session.ts"; import type { AgentRunRegistry, AgentRunRegistrySnapshot, BatchTaskResult, } from "../tool/agent_run/registry.ts"; import type { ProfileCatalogue, ProfileDiagnostic, ProfileSourceDirectories } from "../types.ts"; import { AgentsDialog } from "./agents-overlay.ts"; import { AGENTS_OVERLAY_MAX_HEIGHT_PERCENT, type AgentsOverlayData, type AgentsOverlayProfile, type AgentsOverlayRun, } from "./agents-overlay-data.ts"; const MAX_OVERLAY_DIAGNOSTICS = 20; /** Register the TUI-only /agents Agent Run inspector. */ export function registerAgentsCommand( pi: ExtensionAPI, registry: AgentRunRegistry, ): AgentsCommandLifecycle { let askUserActive = false; let removeAskUserEvents: (() => void) | undefined; const activate = (): void => { askUserActive = false; removeAskUserEvents?.(); removeAskUserEvents = subscribeAskUserEvents( pi, () => { askUserActive = true; }, () => { askUserActive = false; }, ); }; const deactivate = (): void => { askUserActive = false; removeAskUserEvents?.(); removeAskUserEvents = undefined; }; activate(); pi.registerCommand("agents", { description: "Inspect Agent Runs and Agent Profiles", handler: async (_args, ctx) => { if (ctx.mode !== "tui") { ctx.ui.notify("/agents is available only in TUI mode.", "warning"); return; } const catalogue = agentProfileCatalogueStore.get(); let closeOverlay: (() => void) | undefined; // Pi gives temporary non-overlay prompts keyboard focus while this overlay stays visible. // Close /agents first so Esc does not cancel an active ask_user form. const removeInputListener = ctx.ui.onTerminalInput?.((data) => { if (!askUserActive || !closeOverlay || !matchesKey(data, Key.escape)) return; closeOverlay(); return { consume: true }; }); try { await ctx.ui.custom( (tui, theme, _keybindings, done) => { closeOverlay = () => done(undefined); return new AgentsDialog(buildOverlayData(catalogue, registry.snapshot()), { theme, tui, done: () => done(undefined), onSteer: (taskId, message) => registry.steer(taskId, message), onStop: (taskId) => registry.stop(taskId), subscribe: (listener) => registry.subscribe((snapshot) => listener(buildOverlayData(catalogue, snapshot))), }); }, { overlay: true, overlayOptions: { anchor: "center", width: "80%", minWidth: 60, maxHeight: `${AGENTS_OVERLAY_MAX_HEIGHT_PERCENT}%`, visible: (terminalWidth: number) => terminalWidth >= 60, }, }, ); } finally { removeInputListener?.(); } }, }); return { activate, deactivate }; } interface AgentsCommandLifecycle { activate(): void; deactivate(): void; } function subscribeAskUserEvents( pi: ExtensionAPI, onStart: () => void, onEnd: () => void, ): () => void { const removeStart = pi.events.on("supi:ask-user:start", onStart); const removeEnd = pi.events.on("supi:ask-user:end", onEnd); return () => { removeStart(); removeEnd(); }; } function buildOverlayData( catalogue: ProfileCatalogue | undefined, snapshot: AgentRunRegistrySnapshot, ): AgentsOverlayData { const activeRuns: AgentsOverlayRun[] = snapshot.activeRuns.map((run) => ({ key: `active:${run.taskId}`, active: true, taskId: run.taskId, profileId: run.profileId, status: run.status, modelId: run.modelId, thinkingLevel: run.thinkingLevel, turns: run.turns, toolUses: run.toolUses, usage: run.usage, recentActivity: run.recentActivity, humanTruncated: false, modelTruncated: false, taskMetadata: run.taskMetadata, ...(snapshot.activeSharedContext === undefined ? {} : { sharedContext: snapshot.activeSharedContext }), conversationView: run.conversationView, })); const lastRuns = snapshot.lastBatch?.tasks.map((task, index) => completedRun( task, index, snapshot.lastBatch?.conversationViews[task.taskId], snapshot.lastBatch?.sharedContext, ), ) ?? []; const diagnostics = boundedDiagnostics(catalogue?.diagnostics ?? []); return { runs: [...activeRuns, ...lastRuns], profiles: catalogue?.profiles .filter( (profile) => catalogue.profileIds.includes(profile.id) || "code" in resolveProfileDefinition(profile), ) .map((profile) => profileView(profile, catalogue.sourceDirectories)) ?? [], diagnostics: diagnostics.visible, omittedDiagnosticCount: diagnostics.omitted, omittedProfileCount: catalogue?.omittedProfileCount ?? 0, }; } function completedRun( task: BatchTaskResult, index: number, conversationView: AgentsOverlayRun["conversationView"], sharedContext?: string, ): AgentsOverlayRun { return { key: `last:${index}:${task.taskId}`, active: false, taskId: task.taskId, profileId: task.profileId, status: task.status, failureCode: task.failureCode, modelId: task.modelId, thinkingLevel: task.thinkingLevel, turns: task.turns, toolUses: task.toolUses, usage: task.usage, humanTruncated: task.humanTruncated, modelTruncated: task.modelTruncated, ...(task.finalTextFull === undefined ? {} : { finalText: task.finalTextFull }), taskMetadata: task.taskMetadata ?? conversationView?.taskMetadata, ...(sharedContext === undefined ? {} : { sharedContext }), conversationView, }; } function profileView( entry: ProfileCatalogue["profiles"][number], sourceDirectories: ProfileSourceDirectories, ): AgentsOverlayProfile { const profile = resolveProfileDefinition(entry, sourceDirectories); if ("code" in profile) { return { id: entry.id, description: entry.description, unavailable: profile.message, }; } return { id: profile.id, description: profile.manifest.description, source: profile.source, directory: profile.directory, model: profile.manifest.model ?? "inherit", thinking: profile.manifest.thinking ?? "inherit", ...(profile.manifest.timeoutMinutes === undefined ? {} : { timeoutMinutes: profile.manifest.timeoutMinutes }), tools: profile.manifest.tools, systemPrompt: profile.manifest.systemPrompt, instructionScopes: profile.manifest.instructionScopes, ...(profile.fieldSources === undefined ? {} : { fieldSources: profile.fieldSources }), }; } function boundedDiagnostics(diagnostics: readonly ProfileDiagnostic[]): { visible: readonly ProfileDiagnostic[]; omitted: number; } { const ordered = [...diagnostics].sort((left, right) => { if (left.code === right.code) return 0; if (left.code === "catalogue-overflow") return -1; if (right.code === "catalogue-overflow") return 1; return 0; }); return { visible: ordered.slice(0, MAX_OVERLAY_DIAGNOSTICS), omitted: Math.max(0, ordered.length - MAX_OVERLAY_DIAGNOSTICS), }; }