Skip to content

Instantly share code, notes, and snippets.

@jprochazk
Created September 11, 2026 11:51
Show Gist options
  • Select an option

  • Save jprochazk/deec51d6928d2db9cc6fc049a8800793 to your computer and use it in GitHub Desktop.

Select an option

Save jprochazk/deec51d6928d2db9cc6fc049a8800793 to your computer and use it in GitHub Desktop.
import { spawn, spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
type RevdiffResult = { status: "complete"; content: string } | { status: "failed"; exitCode: number | null };
function splitArgs(input: string): string[] {
const args: string[] = [];
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
let match: RegExpExecArray | null;
while ((match = re.exec(input)) !== null) {
args.push(match[1] ?? match[2] ?? match[3]);
}
return args;
}
function defaultArgs(cwd: string): string[] {
const status = spawnSync("git", ["status", "--porcelain"], { cwd, encoding: "utf-8" });
if (status.status === 0 && status.stdout.trim() === "") {
return ["HEAD~1", "HEAD"];
}
return ["HEAD", "--untracked"];
}
async function runRevdiff(args: string[], cwd: string): Promise<RevdiffResult> {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "pi-revdiff-"));
const outputPath = path.join(directory, "annotations.md");
try {
const exitCode = await new Promise<number | null>((resolve) => {
const child = spawn("revdiff", [...args, "-o", outputPath], {
cwd,
stdio: "inherit",
env: process.env,
});
child.on("error", () => resolve(null));
child.on("close", (code) => resolve(code));
});
if (exitCode !== 0 && exitCode !== 10) {
return { status: "failed", exitCode };
}
if (!fs.existsSync(outputPath)) {
return { status: "complete", content: "" };
}
return { status: "complete", content: fs.readFileSync(outputPath, "utf-8").replace(/\n$/, "") };
} finally {
try {
fs.rmSync(directory, { recursive: true, force: true });
} catch {}
}
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("revdiff", {
description: "Review changes in revdiff; annotations go into the prompt",
handler: async (args, ctx) => {
if (ctx.mode !== "tui") {
ctx.ui.notify("revdiff requires interactive mode", "error");
return;
}
const result = await ctx.ui.custom<RevdiffResult>((tui, _theme, _kb, done) => {
tui.stop();
process.stdout.write("\x1b[2J\x1b[H");
const revdiffArgs = args.trim() ? splitArgs(args) : defaultArgs(ctx.cwd);
void runRevdiff(revdiffArgs, ctx.cwd).then(
(res) => {
tui.start();
tui.requestRender(true);
done(res);
},
() => {
tui.start();
tui.requestRender(true);
done({ status: "failed", exitCode: null });
},
);
return { render: () => [], invalidate: () => {} };
});
if (result.status === "failed") {
ctx.ui.notify(
result.exitCode === null
? "Failed to launch revdiff (is it installed?)"
: `revdiff exited with status ${result.exitCode}`,
"error",
);
return;
}
if (!result.content.trim()) {
ctx.ui.notify("revdiff produced no annotations", "info");
return;
}
const existing = ctx.ui.getEditorText();
ctx.ui.setEditorText(existing.trim() ? `${existing.replace(/\n$/, "")}\n\n${result.content}` : result.content);
ctx.ui.notify("Loaded revdiff annotations into the editor", "info");
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment