import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; import * as fs from "node:fs"; import * as path from "node:path"; import { tmpdir } from "node:os"; import { createServer, type Server, type ServerResponse } from "node:http"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { extractResearchName } from "../persistence/research-journal-codec.ts"; import { activeResearch } from "../persistence/research-directory.ts"; const TITLE_PLACEHOLDER = "__GOAL_TITLE__"; const LOGO_PLACEHOLDER = "__GOAL_LOGO__"; const CONTENT_TYPES: Record = { ".html": "text/html; charset=utf-8", ".jsonl": "text/plain; charset=utf-8", ".json": "application/json; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".png": "image/png", ".webp": "image/webp", }; export interface DashboardServerController { export(ctx: ExtensionContext, workDir: string): Promise; broadcast(workDir: string): void; stop(): void; } export function createDashboardServer(): DashboardServerController { let dashboardServer: Server | null = null; let dashboardServerPort: number | null = null; let dashboardServerWorkDir: string | null = null; let dashboardServerHtmlPath: string | null = null; const dashboardSseClients = new Set(); let cachedPackageRoot: string | null = null; let cachedLogoDataUrl: string | null = null; function packageRoot(): string { if (cachedPackageRoot) return cachedPackageRoot; const extensionDir = fs.realpathSync(path.dirname(fileURLToPath(import.meta.url))); cachedPackageRoot = path.resolve(extensionDir, "../.."); return cachedPackageRoot; } function templatePath(): string { return path.join(packageRoot(), "assets/template.html"); } function readTemplate(): string { return fs.readFileSync(templatePath(), "utf-8"); } function logoDataUrl(): string { if (cachedLogoDataUrl) return cachedLogoDataUrl; const logoPath = path.join(packageRoot(), "assets/logo.webp"); const bytes = fs.readFileSync(logoPath); cachedLogoDataUrl = `data:image/webp;base64,${bytes.toString("base64")}`; return cachedLogoDataUrl; } function readJsonlContent(workDir: string): string { return fs.readFileSync(activeResearch(workDir).paths.journal, "utf-8").trim(); } function escapeHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } function injectDataIntoTemplate(template: string, title: string): string { const escapedTitle = escapeHtml(title); return template.replace(TITLE_PLACEHOLDER, () => escapedTitle); } function openInBrowser(url: string): void { if (process.platform === "win32") { spawn("cmd", ["/c", "start", "", url], { detached: true, shell: true, stdio: "ignore", }).unref(); return; } const openCmd = process.platform === "darwin" ? "open" : "xdg-open"; spawn(openCmd, [url], { detached: true, stdio: "ignore" }).unref(); } function stop(): void { for (const client of dashboardSseClients) { try { client.end(); } catch { /* ignore */ } } dashboardSseClients.clear(); if (dashboardServer) { try { dashboardServer.close(); } catch { /* ignore */ } } dashboardServer = null; dashboardServerPort = null; dashboardServerWorkDir = null; dashboardServerHtmlPath = null; } function writeDashboardFile(workDir: string): string { const jsonlContent = readJsonlContent(workDir); const researchName = extractResearchName(jsonlContent); const html = injectDataIntoTemplate(readTemplate(), researchName) .replace(LOGO_PLACEHOLDER, logoDataUrl()); const exportDir = fs.mkdtempSync(path.join(tmpdir(), "pi-goal-dashboard-")); const dest = path.join(exportDir, "index.html"); fs.writeFileSync(dest, html); return dest; } function fileContentType(filePath: string): string { const ext = path.extname(filePath).toLowerCase(); return CONTENT_TYPES[ext] ?? "application/octet-stream"; } function resolveServedFile(workDir: string, requestPath: string): string | null { if (requestPath === "/") return dashboardServerHtmlPath; if (requestPath === "/goal.jsonl") return activeResearch(workDir).paths.journal; return null; } function registerSseClient(res: ServerResponse): void { res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", }); res.write("retry: 1000\n\n"); dashboardSseClients.add(res); res.on("close", () => dashboardSseClients.delete(res)); } function broadcast(workDir: string): void { if (!dashboardServer || dashboardServerWorkDir !== workDir) return; for (const res of dashboardSseClients) { try { res.write("event: jsonl-updated\n"); res.write(`data: ${Date.now()}\n\n`); } catch { dashboardSseClients.delete(res); } } } function startStaticServer(workDir: string, dashboardHtmlPath: string): Promise { return new Promise((resolve, reject) => { const resolvedWorkDir = path.resolve(workDir); const resolvedDashboardHtmlPath = path.resolve(dashboardHtmlPath); if (dashboardServer && dashboardServerWorkDir === resolvedWorkDir && dashboardServerPort) { dashboardServerHtmlPath = resolvedDashboardHtmlPath; resolve(dashboardServerPort); return; } stop(); dashboardServerHtmlPath = resolvedDashboardHtmlPath; const server = createServer((req, res) => { const url = new URL(req.url ?? "/", "http://127.0.0.1"); if (url.pathname === "/events") { registerSseClient(res); return; } const filePath = resolveServedFile(resolvedWorkDir, url.pathname); if (!filePath) { res.writeHead(404); res.end(); return; } fs.readFile(filePath, (err, data) => { if (err) { res.writeHead(404); res.end(); return; } res.writeHead(200, { "Content-Type": fileContentType(filePath) }); res.end(data); }); }); server.listen(0, "127.0.0.1", () => { const address = server.address(); if (!address || typeof address === "string") { reject(new Error("Failed to bind dashboard server")); return; } dashboardServer = server; dashboardServerPort = address.port; dashboardServerWorkDir = resolvedWorkDir; resolve(address.port); }); server.on("error", reject); }); } async function exportDashboard(ctx: ExtensionContext, workDir: string): Promise { const jsonlPath = activeResearch(workDir).paths.journal; if (!fs.existsSync(jsonlPath)) { ctx.ui.notify("No goal.jsonl found — run some experiments first", "error"); return; } try { const dashboardHtmlPath = writeDashboardFile(workDir); const port = await startStaticServer(workDir, dashboardHtmlPath); const url = `http://127.0.0.1:${port}`; openInBrowser(url); ctx.ui.notify(`Dashboard at ${url} (live updates)`, "info"); } catch (error) { ctx.ui.notify( `Export failed: ${error instanceof Error ? error.message : String(error)}`, "error" ); } } return { export: exportDashboard, broadcast, stop }; }