// Status presentation: status supervision config and status-line aggregate // formatting. Subagent lifecycle state lives in lifecycle.ts — this module // deliberately has no state machine. import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import type { NestedLifecycleSnapshot } from "./nested-lifecycle.ts"; export const DEFAULT_STATUS_LINE_LIMIT = 4; export const MAX_STATUS_NAME_LENGTH = 72; export const DEFAULT_STATUS_CONFIG: StatusConfig = { enabled: true }; const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "../.."); const DEFAULT_STATUS_CONFIG_PATH = join(PACKAGE_ROOT, "config.json"); export interface StatusConfig { enabled: boolean; } export interface CappedStatusLines { visibleLines: string[]; overflow: number; } function invalidStatusConfig(source: string, message: string): never { throw new Error(`Invalid subagent status config in ${source}: ${message}`); } function requireObject(value: unknown, source: string, fieldName: string): Record { if (value == null || typeof value !== "object" || Array.isArray(value)) { invalidStatusConfig(source, `${fieldName} must be an object`); } return value as Record; } function requireBoolean(value: unknown, source: string, fieldName: string): boolean { if (typeof value !== "boolean") { invalidStatusConfig(source, `${fieldName} must be a boolean`); } return value; } function rejectUnsupportedKeys( value: Record, allowedKeys: string[], source: string, fieldName: string, ): void { const unsupportedKeys = Object.keys(value).filter((key) => !allowedKeys.includes(key)); if (unsupportedKeys.length > 0) { invalidStatusConfig(source, `${fieldName} has unsupported key(s): ${unsupportedKeys.join(", ")}`); } } function truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) return text; if (maxLength <= 1) return text.slice(0, maxLength); return `${text.slice(0, maxLength - 1)}…`; } export function normalizeStatusName(name: string): string { const collapsed = name.replace(/\s+/g, " ").trim() || "subagent"; return truncateText(collapsed, MAX_STATUS_NAME_LENGTH); } /** Format the ownership counts that keep a parent status visible while draining. */ export function formatNestedLifecycleDrainDetail( snapshot: NestedLifecycleSnapshot, reservationLabel = "reservation", ): string { const parts: string[] = []; if (snapshot.activeLaunchReservations > 0) { parts.push( `${snapshot.activeLaunchReservations} ${reservationLabel}${snapshot.activeLaunchReservations === 1 ? "" : "s"}`, ); } if (snapshot.activeDescendants > 0) { parts.push( `${snapshot.activeDescendants} descendant${snapshot.activeDescendants === 1 ? "" : "s"}`, ); } if (snapshot.pendingHandoffs > 0) { parts.push( `${snapshot.pendingHandoffs} handoff${snapshot.pendingHandoffs === 1 ? "" : "s"}`, ); } return parts.length > 0 ? ` (${parts.join(", ")})` : ""; } export function parseStatusConfig(rawConfig: unknown, source = "config.json"): StatusConfig { const config = requireObject(rawConfig, source, "root"); const status = requireObject(config.status, source, "status"); rejectUnsupportedKeys(status, ["enabled"], source, "status"); const enabled = requireBoolean(status.enabled, source, "status.enabled"); return { enabled, }; } /** * Load the status config. `config.json` in the package root is optional: * when absent, status display stays enabled by default. */ export function loadStatusConfig( configPath = DEFAULT_STATUS_CONFIG_PATH, ): StatusConfig { let raw: string; try { raw = readFileSync(configPath, "utf8"); } catch (error) { const errno = error as NodeJS.ErrnoException; if (errno.code === "ENOENT") return { ...DEFAULT_STATUS_CONFIG }; throw error; } let parsed: unknown; try { parsed = JSON.parse(raw) as unknown; } catch (error) { const detail = error instanceof Error ? error.message : String(error); throw new Error(`Invalid JSON in subagent config ${configPath}: ${detail}`); } return parseStatusConfig(parsed, configPath); } export function capStatusLines(lines: string[], lineLimit: number): CappedStatusLines { const visibleLines = lines.slice(0, lineLimit); return { visibleLines, overflow: Math.max(0, lines.length - visibleLines.length), }; } export function formatStatusAggregate(lines: string[], lineLimit: number): string { const { visibleLines, overflow } = capStatusLines(lines, lineLimit); const bulletLines = visibleLines.map((line) => `• ${line}`); if (overflow > 0) bulletLines.push(`• +${overflow} more running.`); return `Subagent status:\n${bulletLines.join("\n")}`; }