import { readdir } from "node:fs/promises"; import { homedir } from "node:os"; import { execFileSync } from "node:child_process"; import { resolve as resolvePath } from "node:path"; import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent"; import { isLoggedIn as isAlphaLoggedIn } from "@ada/alpha-hub/lib"; import { APP_ROOT, ADA_AGENT_LOGO, ADA_VERSION, } from "./shared.js"; const ANSI_RE = /\x1b\[[0-9;]*m/g; function visibleLength(text: string): number { return text.replace(ANSI_RE, "").length; } function formatHeaderPath(path: string): string { const home = homedir(); return path.startsWith(home) ? `~${path.slice(home.length)}` : path; } function truncateVisible(text: string, maxVisible: number): string { const raw = text.replace(ANSI_RE, ""); if (raw.length <= maxVisible) return text; if (maxVisible <= 3) return ".".repeat(maxVisible); return `${raw.slice(0, maxVisible - 3)}...`; } function padRight(text: string, width: number): string { const gap = Math.max(0, width - visibleLength(text)); return `${text}${" ".repeat(gap)}`; } function getCurrentModelLabel(ctx: ExtensionContext): string { if (ctx.model) return `${ctx.model.provider}/${ctx.model.id}`; const branch = ctx.sessionManager.getBranch(); for (let index = branch.length - 1; index >= 0; index -= 1) { const entry = branch[index]!; if (entry.type === "model_change") return `${(entry as any).provider}/${(entry as any).modelId}`; } return "not set"; } async function buildAgentCatalogSummary(): Promise<{ agents: string[]; chains: string[] }> { const agents: string[] = []; const chains: string[] = []; try { const entries = await readdir(resolvePath(APP_ROOT, ".ada", "agents"), { withFileTypes: true }); for (const entry of entries) { if (!entry.isFile() || !entry.name.endsWith(".md")) continue; if (entry.name.endsWith(".chain.md")) { chains.push(entry.name.replace(/\.chain\.md$/i, "")); } else { agents.push(entry.name.replace(/\.md$/i, "")); } } } catch { return { agents: [], chains: [] }; } agents.sort(); chains.sort(); return { agents, chains }; } async function countSkills(): Promise { try { const entries = await readdir(resolvePath(APP_ROOT, "skills"), { withFileTypes: true }); return entries.filter((e) => e.isDirectory()).length; } catch { return 0; } } function detectDocker(): boolean { try { execFileSync("docker", ["--version"], { timeout: 500, stdio: "ignore" }); return true; } catch { return false; } } type WorkflowInfo = { name: string; description: string }; function getResearchWorkflows(pi: ExtensionAPI): WorkflowInfo[] { return pi.getCommands() .filter((cmd) => cmd.source === "prompt") .map((cmd) => ({ name: `/${cmd.name}`, description: cmd.description ?? "" })) .sort((a, b) => a.name.localeCompare(b.name)); } function shortDescription(desc: string): string { const lower = desc.toLowerCase(); for (const prefix of ["run a ", "run an ", "set up a ", "build a ", "build the ", "turn ", "design the ", "produce a ", "compare ", "simulate ", "inspect ", "write a ", "plan or execute a ", "prepare a "]) { if (lower.startsWith(prefix)) return desc.slice(prefix.length); } return desc; } function briefDesc(desc: string, maxWords = 8): string { const words = shortDescription(desc).split(" "); if (words.length <= maxWords) return words.join(" "); return `${words.slice(0, maxWords).join(" ")}...`; } export function installAdaHeader( pi: ExtensionAPI, ctx: ExtensionContext, cache: { agentSummaryPromise?: Promise<{ agents: string[]; chains: string[] }>; skillCountPromise?: Promise }, ): void | Promise { if (!ctx.hasUI) return; cache.agentSummaryPromise ??= buildAgentCatalogSummary(); cache.skillCountPromise ??= countSkills(); return Promise.all([cache.agentSummaryPromise, cache.skillCountPromise]).then(([agentData, skillCount]) => { const workflows = getResearchWorkflows(pi); const toolCount = pi.getAllTools().length; const agentCount = agentData.agents.length + agentData.chains.length; const hasDocker = detectDocker(); ctx.ui.setHeader((_tui, theme) => ({ render(width: number): string[] { const cardW = Math.min(width - 4, 120); const innerW = cardW - 2; const contentW = innerW - 2; const lines: string[] = []; const push = (line: string) => { lines.push(line); }; const border = (ch: string) => theme.fg("borderMuted", ch); const row = (content: string): string => `${border("│")} ${padRight(content, contentW)} ${border("│")}`; const emptyRow = (): string => `${border("│")}${" ".repeat(innerW)}${border("│")}`; const sep = (): string => `${border("├")}${border("─".repeat(innerW))}${border("┤")}`; const useWideLayout = contentW >= 90; const leftW = useWideLayout ? Math.min(38, Math.floor(contentW * 0.35)) : 0; const divColW = useWideLayout ? 3 : 0; const rightW = useWideLayout ? contentW - leftW - divColW : contentW; const twoCol = (left: string, right: string): string => { if (!useWideLayout) return row(left || right); return row( `${padRight(left, leftW)}${border(" │ ")}${padRight(right, rightW)}`, ); }; const modelLabel = getCurrentModelLabel(ctx); const dirLabel = formatHeaderPath(ctx.cwd); // ── Logo ── push(""); if (cardW >= 70) { for (const logoLine of ADA_AGENT_LOGO) { push(theme.fg("accent", theme.bold(` ${logoLine}`))); } } // ── Tagline ── push(theme.fg("dim", " The AI research agent. Type naturally or use /commands.")); push(""); // ── Version border ── const versionTag = ` v${ADA_VERSION} `; const vGap = Math.max(0, innerW - versionTag.length); const vGapL = Math.floor(vGap / 2); push( border(`╭${"─".repeat(vGapL)}`) + theme.fg("dim", versionTag) + border(`${"─".repeat(vGap - vGapL)}╮`), ); if (useWideLayout) { const cmdNameW = 16; const leftValueW = Math.max(1, leftW - 11); const leftLines: string[] = [""]; // ── Left: Model + Directory ── leftLines.push(`${theme.fg("dim", "model".padEnd(10))} ${theme.fg("text", truncateVisible(modelLabel, leftValueW))}`); leftLines.push(`${theme.fg("dim", "directory".padEnd(10))} ${theme.fg("text", truncateVisible(dirLabel, leftValueW))}`); // ── Left: Stats ── leftLines.push(""); leftLines.push(theme.fg("dim", truncateVisible(`${toolCount} tools · ${agentCount} agents`, leftW))); leftLines.push(theme.fg("dim", truncateVisible(`${workflows.length} workflows · ${skillCount} skills`, leftW))); // ── Left: Services ── leftLines.push(""); leftLines.push(theme.fg("accent", theme.bold("Services"))); const ok = (v: boolean) => v ? theme.fg("success", "✓") : theme.fg("dim", "✗"); // Required leftLines.push(`alphaXiv ${ok(isAlphaLoggedIn())} docker ${ok(hasDocker)}`); // Optional — collect configured ones, show two per line const configured: string[] = []; if (process.env.ZOTERO_API_KEY) configured.push("zotero"); if (process.env.TAVILY_API_KEY) configured.push("tavily"); if (process.env.S2_API_KEY) configured.push("s2"); if (process.env.OPENALEX_API_KEY) configured.push("openalex"); if (process.env.OPENROUTER_API_KEY) configured.push("openrouter"); if (process.env.MODAL_TOKEN_ID) configured.push("modal"); if (process.env.RUNPOD_API_KEY) configured.push("runpod"); if (process.env.NCBI_API_KEY) configured.push("ncbi"); for (let i = 0; i < configured.length; i += 2) { const a = `${configured[i]} ${ok(true)}`; const b = configured[i + 1] ? ` ${configured[i + 1]} ${ok(true)}` : ""; leftLines.push(a + b); } // ── Right: Quick Start (workflows + help) ── const qsW = 16; const wfDescs: Record = { "/audit": "verify paper claims against its codebase", "/autoresearch": "autonomous experiment loop", "/compare": "compare sources on a topic", "/deepresearch": "thorough multi-source investigation", "/draft": "write a paper-style draft", "/litreview": "literature review with citations", "/replicate": "replicate a paper's experiments", "/review": "simulated peer review", "/watch": "recurring research watch", }; const rightLines: string[] = [ "", `${theme.fg("accent", theme.bold("Quick Start"))} ${theme.fg("text", '"just ask anything"')}`, "", ]; const wfDescMaxW = Math.max(5, rightW - qsW); for (const wf of workflows) { if (wf.name === "/jobs" || wf.name === "/log" || wf.name === "/watch") continue; const desc = wfDescs[wf.name] ?? shortDescription(wf.description); rightLines.push( `${theme.fg("accent", wf.name.padEnd(qsW))}${theme.fg("dim", truncateVisible(desc, wfDescMaxW))}`, ); } rightLines.push(""); rightLines.push(`${theme.fg("accent", "/help".padEnd(qsW))}${theme.fg("dim", "all commands")}`); const maxRows = Math.max(leftLines.length, rightLines.length); for (let i = 0; i < maxRows; i++) { push(twoCol(leftLines[i] ?? "", rightLines[i] ?? "")); } } else { // ── Narrow layout ── const narrowValW = Math.max(1, contentW - 11); push(emptyRow()); push(row(`${theme.fg("dim", "model".padEnd(10))} ${theme.fg("text", truncateVisible(modelLabel, narrowValW))}`)); push(row(`${theme.fg("dim", "directory".padEnd(10))} ${theme.fg("text", truncateVisible(dirLabel, narrowValW))}`)); push(emptyRow()); push(row(theme.fg("dim", `${toolCount} tools · ${agentCount} agents · ${skillCount} skills · ${workflows.length} workflows`))); push(emptyRow()); push(sep()); push(row(theme.fg("accent", theme.bold("Quick Start")))); push(row(` ${theme.fg("text", '"ask anything"')}`)); push(row(` ${theme.fg("accent", "/deepresearch")} ${theme.fg("dim", "")}`)); push(row(` ${theme.fg("accent", "/lit")} ${theme.fg("dim", "")}`)); push(row(` ${theme.fg("accent", "/help")} ${theme.fg("dim", "all commands")}`)); push(sep()); push(row(theme.fg("accent", theme.bold("Workflows")))); const narrowDescW = Math.max(1, contentW - 17); for (const wf of workflows) { if (wf.name === "/jobs" || wf.name === "/log" || wf.name === "/watch") continue; push(row(`${theme.fg("accent", wf.name.padEnd(16))} ${theme.fg("dim", truncateVisible(briefDesc(wf.description), narrowDescW))}`)); } } push(border(`╰${"─".repeat(innerW)}╯`)); push(""); return lines; }, invalidate() {}, })); }); }