Skip to content

Instantly share code, notes, and snippets.

@badlogic
Last active January 23, 2026 23:34
Show Gist options
  • Select an option

  • Save badlogic/679b221a1749353a5be3f3134c120685 to your computer and use it in GitHub Desktop.

Select an option

Save badlogic/679b221a1749353a5be3f3134c120685 to your computer and use it in GitHub Desktop.
pi extension: /diff command to show git changes and open in VS Code diff view
/**
* Diff Extension
*
* /diff command shows modified/deleted/new files from git status and opens
* the selected file in VS Code's diff view.
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
import { Container, Key, matchesKey, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
interface FileInfo {
status: string;
statusLabel: string;
file: string;
}
export default function (pi: ExtensionAPI) {
pi.registerCommand("diff", {
description: "Show git changes and open in VS Code diff view",
handler: async (_args, ctx) => {
if (!ctx.hasUI) {
ctx.ui.notify("No UI available", "error");
return;
}
// Get changed files from git status
const result = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
if (result.code !== 0) {
ctx.ui.notify(`git status failed: ${result.stderr}`, "error");
return;
}
if (!result.stdout || !result.stdout.trim()) {
ctx.ui.notify("No changes in working tree", "info");
return;
}
// Parse git status output
// Format: XY filename (where XY is two-letter status, then space, then filename)
const lines = result.stdout.split("\n");
const files: FileInfo[] = [];
for (const line of lines) {
if (line.length < 4) continue; // Need at least "XY f"
const status = line.slice(0, 2);
const file = line.slice(2).trimStart();
// Translate status codes to short labels
let statusLabel: string;
if (status.includes("M")) statusLabel = "M";
else if (status.includes("A")) statusLabel = "A";
else if (status.includes("D")) statusLabel = "D";
else if (status.includes("?")) statusLabel = "?";
else if (status.includes("R")) statusLabel = "R";
else if (status.includes("C")) statusLabel = "C";
else statusLabel = status.trim() || "~";
files.push({ status: statusLabel, statusLabel, file });
}
if (files.length === 0) {
ctx.ui.notify("No changes found", "info");
return;
}
// Show file picker with SelectList
const selected = await ctx.ui.custom<FileInfo | null>((tui, theme, _kb, done) => {
const container = new Container();
// Top border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
// Title
container.addChild(new Text(theme.fg("accent", theme.bold(" Select file to diff")), 0, 0));
// Build select items with colored status
const items: SelectItem[] = files.map((f) => {
let statusColor: string;
switch (f.status) {
case "M":
statusColor = theme.fg("warning", f.status);
break;
case "A":
statusColor = theme.fg("success", f.status);
break;
case "D":
statusColor = theme.fg("error", f.status);
break;
case "?":
statusColor = theme.fg("muted", f.status);
break;
default:
statusColor = theme.fg("dim", f.status);
}
return {
value: f,
label: `${statusColor} ${f.file}`,
};
});
const visibleRows = Math.min(files.length, 15);
let currentIndex = 0;
const selectList = new SelectList(items, visibleRows, {
selectedPrefix: (t) => theme.fg("accent", t),
selectedText: (t) => t, // Keep existing colors
description: (t) => theme.fg("muted", t),
scrollInfo: (t) => theme.fg("dim", t),
noMatch: (t) => theme.fg("warning", t),
});
selectList.onSelect = (item) => done(item.value as FileInfo);
selectList.onCancel = () => done(null);
selectList.onSelectionChange = (item) => {
currentIndex = items.indexOf(item);
};
container.addChild(selectList);
// Help text
container.addChild(
new Text(theme.fg("dim", " ↑↓ navigate • ←→ page • enter select • esc cancel"), 0, 0),
);
// Bottom border
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
return {
render: (w) => container.render(w),
invalidate: () => container.invalidate(),
handleInput: (data) => {
// Add paging with left/right
if (matchesKey(data, Key.left)) {
// Page up - clamp to 0
currentIndex = Math.max(0, currentIndex - visibleRows);
selectList.setSelectedIndex(currentIndex);
} else if (matchesKey(data, Key.right)) {
// Page down - clamp to last
currentIndex = Math.min(items.length - 1, currentIndex + visibleRows);
selectList.setSelectedIndex(currentIndex);
} else {
selectList.handleInput(data);
}
tui.requestRender();
},
};
});
if (!selected) return;
// Open in VS Code diff view (this will block until VS Code is closed)
// For untracked files, git difftool won't work, so fall back to just opening the file
if (selected.status === "?") {
await pi.exec("code", ["-g", selected.file], { cwd: ctx.cwd });
} else {
const diffResult = await pi.exec("git", ["difftool", "-y", "--tool=vscode", selected.file], {
cwd: ctx.cwd,
});
// Fallback if difftool fails
if (diffResult.code !== 0) {
await pi.exec("code", ["-g", selected.file], { cwd: ctx.cwd });
}
}
},
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment