import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; import { dirname, join, basename } from "node:path"; import { fileURLToPath } from "node:url"; import { writeFileSync, existsSync } from "node:fs"; const baseDir = dirname(fileURLToPath(import.meta.url)); const distDir = join(baseDir, "..", "dist"); const viewerPath = join(distDir, "viewer.js"); const shellPath = join(distDir, "shell.html"); // Resolve glimpseui from node_modules — dynamic import to avoid jiti issues const glimpsePath = join(baseDir, "..", "node_modules", "glimpseui", "src", "glimpse.mjs"); let win: any = null; let openFn: any = null; let ready = false; let readyResolve: (() => void) | null = null; /** * Write shell.html to dist/ — a tiny HTML file with a loading spinner * that loads viewer.js via `; writeFileSync(shellPath, html); } /** Wire up window events. */ function wireWindow(w: any) { w.loadFile(shellPath); w.on("message", (data: any) => { if (data?.type === "viewer-ready") { ready = true; if (readyResolve) { readyResolve(); readyResolve = null; } } }); w.on("closed", () => { win = null; ready = false; readyResolve = null; setTimeout(() => prewarm(), 100); }); } /** Prewarm: hidden window with viewer.js already parsed. */ function prewarm() { if (!openFn || !existsSync(shellPath) || win) return; // Pass null — we'll loadFile in the ready handler win = openFn(null, { width: 1120, height: 760, title: "Diffs", hidden: true, }); ready = false; wireWindow(win); } /** Wait for viewer.js to finish loading (with timeout). */ function waitForReady(timeoutMs = 15000): Promise { if (ready) return Promise.resolve(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { readyResolve = null; reject(new Error("Timed out waiting for viewer to load")); }, timeoutMs); readyResolve = () => { clearTimeout(timer); resolve(); }; }); } export default function (pi: ExtensionAPI) { // Write shell.html, load glimpse, then prewarm (async () => { try { if (!existsSync(viewerPath)) return; writeShellHTML(); const glimpse = await import(glimpsePath); openFn = glimpse.open; prewarm(); } catch { // Silently fail — prewarm is best-effort } })(); pi.registerCommand("diffs", { description: "Show git diffs in a native window", handler: async (_args, ctx) => { if (!existsSync(viewerPath)) { ctx.ui.notify( "Viewer not built. Run 'npm run build' in the pi-extension-diffs package directory.", "error" ); return; } if (!openFn) { try { const glimpse = await import(glimpsePath); openFn = glimpse.open; } catch (e: any) { ctx.ui.notify(`Failed to load Glimpse: ${e.message}`, "error"); return; } } // Ensure shell.html exists if (!existsSync(shellPath)) writeShellHTML(); const gitCheck = await pi.exec("git", [ "rev-parse", "--is-inside-work-tree", ]); if (gitCheck.code !== 0) { ctx.ui.notify("Not in a git repository", "error"); return; } const [stagedResult, unstagedResult, untrackedResult, branchResult] = await Promise.all([ pi.exec("git", ["diff", "--cached"]), pi.exec("git", ["diff"]), pi.exec("git", ["ls-files", "--others", "--exclude-standard"]), pi.exec("git", ["branch", "--show-current"]), ]); const staged = stagedResult.stdout || ""; const unstaged = unstagedResult.stdout || ""; const untrackedPaths = untrackedResult.stdout .split("\n") .filter((p) => p.trim()); const branch = branchResult.stdout.trim(); // Gather commits (up to 5) const isMain = branch === "main" || branch === "master"; let commitLogResult; if (isMain) { commitLogResult = await pi.exec("git", [ "log", "HEAD", "--max-count=5", '--format=%h|%s|%ar', ]); } else { commitLogResult = await pi.exec("git", [ "log", "main..HEAD", "--max-count=5", '--format=%h|%s|%ar', ]); if (commitLogResult.code !== 0) { commitLogResult = await pi.exec("git", [ "log", "master..HEAD", "--max-count=5", '--format=%h|%s|%ar', ]); } if (commitLogResult.code !== 0) { commitLogResult = await pi.exec("git", [ "log", "HEAD", "--max-count=5", '--format=%h|%s|%ar', ]); } } const commitLines = (commitLogResult.stdout || "") .split("\n") .filter((l) => l.trim()); const parsedCommits = commitLines .map((line) => { const parts = line.split("|"); const hash = parts[0] ?? ""; const time = parts[parts.length - 1] ?? ""; const message = parts.slice(1, parts.length - 1).join("|"); return { hash, message, time }; }) .filter((c) => c.hash); const commitDiffs = await Promise.all( parsedCommits.map((c) => pi.exec("git", ["show", c.hash, "--format=", "--patch"]) ) ); const commits = parsedCommits.map((c, i) => ({ ...c, diff: commitDiffs[i].stdout || "", })); if (!staged && !unstaged && untrackedPaths.length === 0 && commits.length === 0) { ctx.ui.notify("No changes", "info"); return; } const untracked: { path: string; content: string }[] = []; for (const filePath of untrackedPaths) { try { const result = await pi.exec("cat", [filePath]); if (result.code === 0) { untracked.push({ path: filePath, content: result.stdout }); } } catch {} } const repoName = basename(ctx.cwd); const data = { staged, unstaged, untracked, repoName, branch, commits }; const dataJSON = JSON.stringify(data); // Open window if needed if (!win) { win = openFn(null, { width: 1120, height: 760, title: `Diffs — ${repoName}`, }); ready = false; wireWindow(win); } try { await waitForReady(); } catch (e: any) { ctx.ui.notify(`Diffs viewer failed: ${e.message}`, "error"); win = null; ready = false; return; } win.send(`window.updateDiffs(${dataJSON})`); win.show({ title: `Diffs — ${repoName}` }); }, }); }