mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
Move skill command handling to AgentSession, update docs
- Skill commands (/skill:name) now expanded in AgentSession instead of interactive mode, enabling them in RPC and print modes - Input event can now intercept /skill:name before expansion - Updated extensions.md with clearer input event docs and processing order - Updated rpc.md: hook -> extension terminology, added skill expansion mentions - Added PR attribution to changelog entries for #761
This commit is contained in:
parent
3e5d91f287
commit
b4a05cbcab
7 changed files with 211 additions and 420 deletions
|
|
@ -1,27 +1,13 @@
|
|||
/**
|
||||
* Git Diff Extension
|
||||
*
|
||||
* Shows modified/added/removed files in the git worktree and displays their diffs.
|
||||
*
|
||||
* Usage:
|
||||
* - Press Ctrl+F or type /diff to open the file picker
|
||||
* - Select a file to view its diff
|
||||
* - Use Up/Down or Left/Right to scroll the diff
|
||||
* - Press Escape to close
|
||||
* Git Diff Extension - Ctrl+F or /diff to view git changes
|
||||
*/
|
||||
|
||||
import { appendFileSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { ExtensionAPI, ExtensionContext, Theme } from "@mariozechner/pi-coding-agent";
|
||||
import { DynamicBorder } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
Container,
|
||||
Key,
|
||||
matchesKey,
|
||||
type SelectItem,
|
||||
SelectList,
|
||||
Text,
|
||||
truncateToWidth,
|
||||
visibleWidth,
|
||||
} from "@mariozechner/pi-tui";
|
||||
import { DynamicBorder, generateDiffString, renderDiff } from "@mariozechner/pi-coding-agent";
|
||||
import { Container, Key, matchesKey, type SelectItem, SelectList, Text, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
|
||||
|
||||
interface GitFile {
|
||||
status: "M" | "A" | "D" | "R" | "C" | "U" | "?";
|
||||
|
|
@ -29,383 +15,148 @@ interface GitFile {
|
|||
staged: boolean;
|
||||
}
|
||||
|
||||
type FileStatus = GitFile["status"];
|
||||
|
||||
const STATUS_LABELS: Record<FileStatus, string> = {
|
||||
M: "modified",
|
||||
A: "added",
|
||||
D: "deleted",
|
||||
R: "renamed",
|
||||
C: "copied",
|
||||
U: "unmerged",
|
||||
"?": "untracked",
|
||||
const STATUS_LABELS: Record<GitFile["status"], string> = {
|
||||
M: "modified", A: "added", D: "deleted", R: "renamed", C: "copied", U: "unmerged", "?": "untracked",
|
||||
};
|
||||
const dbg = (msg: string) => appendFileSync("/tmp/git-diff-debug.log", msg + "\n");
|
||||
|
||||
const STATUS_COLORS: Record<FileStatus, "warning" | "success" | "error" | "muted"> = {
|
||||
M: "warning",
|
||||
A: "success",
|
||||
D: "error",
|
||||
R: "warning",
|
||||
C: "warning",
|
||||
U: "error",
|
||||
"?": "muted",
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse git status --porcelain output into file list
|
||||
*/
|
||||
function parseGitStatus(output: string): GitFile[] {
|
||||
dbg(`=== parseGitStatus ===`);
|
||||
dbg(`Raw output: ${JSON.stringify(output)}`);
|
||||
const files: GitFile[] = [];
|
||||
const lines = output.trim().split("\n").filter(Boolean);
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.length < 3) continue;
|
||||
|
||||
const indexStatus = line[0];
|
||||
const workTreeStatus = line[1];
|
||||
const path = line.slice(3);
|
||||
|
||||
// Staged changes (index has status, worktree is space or has same status)
|
||||
if (indexStatus !== " " && indexStatus !== "?") {
|
||||
files.push({
|
||||
status: indexStatus as FileStatus,
|
||||
path,
|
||||
staged: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Unstaged changes (worktree has status different from index)
|
||||
if (workTreeStatus !== " " && workTreeStatus !== "?") {
|
||||
// Don't duplicate if same status in both
|
||||
if (indexStatus === " " || indexStatus !== workTreeStatus) {
|
||||
files.push({
|
||||
status: workTreeStatus as FileStatus,
|
||||
path,
|
||||
staged: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Untracked files
|
||||
if (indexStatus === "?" && workTreeStatus === "?") {
|
||||
files.push({
|
||||
status: "?",
|
||||
path,
|
||||
staged: false,
|
||||
});
|
||||
}
|
||||
for (const line of output.trim().split("\n").filter(Boolean)) {
|
||||
dbg(`Line: ${JSON.stringify(line)} (len=${line.length})`);
|
||||
if (line.length < 4) { dbg(" Skipped: too short"); continue; }
|
||||
const idx = line[0], wt = line[1], path = line.slice(3);
|
||||
dbg(` idx='${idx}' wt='${wt}' path='${path}'`);
|
||||
if (idx !== " " && idx !== "?") { dbg(` -> staged`); files.push({ status: idx as GitFile["status"], path, staged: true }); }
|
||||
if (wt !== " " && wt !== "?" && (idx === " " || idx !== wt)) { dbg(` -> unstaged`); files.push({ status: wt as GitFile["status"], path, staged: false }); }
|
||||
if (idx === "?" && wt === "?") { dbg(` -> untracked`); files.push({ status: "?", path, staged: false }); }
|
||||
}
|
||||
|
||||
dbg(`Files: ${JSON.stringify(files)}`);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a unified diff with colors
|
||||
*/
|
||||
function renderUnifiedDiff(diffText: string, theme: Theme): string[] {
|
||||
const lines = diffText.split("\n");
|
||||
const result: string[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith("+++") || line.startsWith("---")) {
|
||||
// File headers
|
||||
result.push(theme.fg("muted", line));
|
||||
} else if (line.startsWith("@@")) {
|
||||
// Hunk headers
|
||||
result.push(theme.fg("accent", line));
|
||||
} else if (line.startsWith("+")) {
|
||||
result.push(theme.fg("toolDiffAdded", line));
|
||||
} else if (line.startsWith("-")) {
|
||||
result.push(theme.fg("toolDiffRemoved", line));
|
||||
} else if (line.startsWith("diff --git")) {
|
||||
result.push(theme.fg("dim", line));
|
||||
} else if (line.startsWith("index ") || line.startsWith("new file") || line.startsWith("deleted file")) {
|
||||
result.push(theme.fg("dim", line));
|
||||
} else {
|
||||
result.push(theme.fg("toolDiffContext", line));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrollable diff viewer component
|
||||
*/
|
||||
class DiffViewer {
|
||||
private lines: string[] = [];
|
||||
private scrollOffset = 0;
|
||||
private viewportHeight = 20;
|
||||
private filePath: string;
|
||||
|
||||
private lines: string[];
|
||||
private offset = 0;
|
||||
private height = 20;
|
||||
onClose?: () => void;
|
||||
|
||||
constructor(
|
||||
private theme: Theme,
|
||||
filePath: string,
|
||||
diffText: string,
|
||||
) {
|
||||
this.filePath = filePath;
|
||||
this.lines = renderUnifiedDiff(diffText, theme);
|
||||
constructor(private theme: Theme, private path: string, diff: string) {
|
||||
this.lines = renderDiff(diff).split("\n");
|
||||
}
|
||||
|
||||
handleInput(data: string): void {
|
||||
const maxScroll = Math.max(0, this.lines.length - this.viewportHeight + 4);
|
||||
|
||||
if (matchesKey(data, Key.escape)) {
|
||||
this.onClose?.();
|
||||
} else if (matchesKey(data, Key.up) || matchesKey(data, Key.left)) {
|
||||
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
||||
} else if (matchesKey(data, Key.down) || matchesKey(data, Key.right)) {
|
||||
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + 1);
|
||||
} else if (matchesKey(data, Key.pageUp) || matchesKey(data, "shift+up")) {
|
||||
this.scrollOffset = Math.max(0, this.scrollOffset - this.viewportHeight);
|
||||
} else if (matchesKey(data, Key.pageDown) || matchesKey(data, "shift+down")) {
|
||||
this.scrollOffset = Math.min(maxScroll, this.scrollOffset + this.viewportHeight);
|
||||
} else if (matchesKey(data, Key.home) || matchesKey(data, "g")) {
|
||||
this.scrollOffset = 0;
|
||||
} else if (matchesKey(data, Key.end) || matchesKey(data, "shift+g")) {
|
||||
this.scrollOffset = maxScroll;
|
||||
}
|
||||
const max = Math.max(0, this.lines.length - this.height + 4);
|
||||
if (matchesKey(data, Key.escape)) this.onClose?.();
|
||||
else if (matchesKey(data, Key.up) || matchesKey(data, Key.left)) this.offset = Math.max(0, this.offset - 1);
|
||||
else if (matchesKey(data, Key.down) || matchesKey(data, Key.right)) this.offset = Math.min(max, this.offset + 1);
|
||||
else if (matchesKey(data, Key.pageUp)) this.offset = Math.max(0, this.offset - this.height);
|
||||
else if (matchesKey(data, Key.pageDown)) this.offset = Math.min(max, this.offset + this.height);
|
||||
else if (matchesKey(data, "g")) this.offset = 0;
|
||||
else if (matchesKey(data, "shift+g")) this.offset = max;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
const th = this.theme;
|
||||
const result: string[] = [];
|
||||
const innerWidth = width - 2;
|
||||
const th = this.theme, w = width - 2, out: string[] = [];
|
||||
const visible = this.lines.slice(this.offset, this.offset + this.height);
|
||||
const row = (c: string) => th.fg("border", "│") + c + " ".repeat(Math.max(0, w - visibleWidth(c))) + th.fg("border", "│");
|
||||
|
||||
// Calculate viewport (leave room for header, footer, borders)
|
||||
this.viewportHeight = Math.max(5, 20);
|
||||
|
||||
const maxScroll = Math.max(0, this.lines.length - this.viewportHeight + 4);
|
||||
const visibleLines = this.lines.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
|
||||
|
||||
// Helper to create bordered line
|
||||
const row = (content: string) => {
|
||||
const vis = visibleWidth(content);
|
||||
const padding = Math.max(0, innerWidth - vis);
|
||||
return th.fg("border", "│") + content + " ".repeat(padding) + th.fg("border", "│");
|
||||
};
|
||||
|
||||
// Top border
|
||||
result.push(th.fg("border", "╭" + "─".repeat(innerWidth) + "╮"));
|
||||
|
||||
// Header with file path
|
||||
const header = ` ${th.fg("accent", th.bold(truncateToWidth(this.filePath, innerWidth - 2)))}`;
|
||||
result.push(row(header));
|
||||
result.push(row(""));
|
||||
|
||||
// Diff content
|
||||
for (const line of visibleLines) {
|
||||
result.push(row(" " + truncateToWidth(line, innerWidth - 2)));
|
||||
}
|
||||
|
||||
// Pad if fewer lines than viewport
|
||||
const paddingNeeded = this.viewportHeight - visibleLines.length;
|
||||
for (let i = 0; i < paddingNeeded; i++) {
|
||||
result.push(row(""));
|
||||
}
|
||||
|
||||
// Scroll indicator
|
||||
const scrollInfo =
|
||||
this.lines.length > this.viewportHeight
|
||||
? `${this.scrollOffset + 1}-${Math.min(this.scrollOffset + this.viewportHeight, this.lines.length)} of ${this.lines.length}`
|
||||
: `${this.lines.length} lines`;
|
||||
result.push(row(""));
|
||||
result.push(row(` ${th.fg("dim", scrollInfo)}`));
|
||||
|
||||
// Footer with help
|
||||
result.push(row(` ${th.fg("dim", "↑↓/←→ scroll • PgUp/PgDn page • g/G start/end • Esc close")}`));
|
||||
|
||||
// Bottom border
|
||||
result.push(th.fg("border", "╰" + "─".repeat(innerWidth) + "╯"));
|
||||
|
||||
return result;
|
||||
out.push(th.fg("border", "╭" + "─".repeat(w) + "╮"));
|
||||
out.push(row(` ${th.fg("accent", th.bold(truncateToWidth(this.path, w - 2)))}`));
|
||||
out.push(row(""));
|
||||
for (const l of visible) out.push(row(" " + truncateToWidth(l, w - 2)));
|
||||
for (let i = visible.length; i < this.height; i++) out.push(row(""));
|
||||
const info = this.lines.length > this.height ? `${this.offset + 1}-${Math.min(this.offset + this.height, this.lines.length)} of ${this.lines.length}` : `${this.lines.length} lines`;
|
||||
out.push(row(""));
|
||||
out.push(row(` ${th.fg("dim", info)}`));
|
||||
out.push(row(` ${th.fg("dim", "↑↓/←→ scroll • PgUp/PgDn • g/G • Esc")}`));
|
||||
out.push(th.fg("border", "╰" + "─".repeat(w) + "╯"));
|
||||
return out;
|
||||
}
|
||||
|
||||
invalidate(): void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the diff for a file
|
||||
*/
|
||||
async function showFileDiff(file: GitFile, ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
||||
// Get the diff
|
||||
let diffArgs: string[];
|
||||
async function getDiff(file: GitFile, ctx: ExtensionContext, pi: ExtensionAPI): Promise<{ diff: string } | { error: string }> {
|
||||
const abs = join(ctx.cwd, file.path);
|
||||
|
||||
if (file.status === "?") {
|
||||
// Untracked file: show full content as "added"
|
||||
const result = await pi.exec("cat", [file.path], { cwd: ctx.cwd });
|
||||
if (result.code !== 0) {
|
||||
ctx.ui.notify(`Failed to read ${file.path}`, "error");
|
||||
return;
|
||||
}
|
||||
// Create a fake diff showing all lines as added
|
||||
const lines = result.stdout.split("\n");
|
||||
const diffText = [
|
||||
`diff --git a/${file.path} b/${file.path}`,
|
||||
"new file",
|
||||
`--- /dev/null`,
|
||||
`+++ b/${file.path}`,
|
||||
`@@ -0,0 +1,${lines.length} @@`,
|
||||
...lines.map((l) => "+" + l),
|
||||
].join("\n");
|
||||
|
||||
await showDiffViewer(file.path, diffText, ctx);
|
||||
return;
|
||||
try { return { diff: generateDiffString("", await readFile(abs, "utf-8")).diff }; }
|
||||
catch (e) { return { error: `Read failed: ${e}` }; }
|
||||
}
|
||||
|
||||
if (file.status === "D") {
|
||||
const r = await pi.exec("git", ["show", `HEAD:${file.path}`], { cwd: ctx.cwd });
|
||||
return r.code === 0 ? { diff: generateDiffString(r.stdout, "").diff } : { error: `Git show failed: ${r.stderr}` };
|
||||
}
|
||||
|
||||
let old = "", cur = "";
|
||||
if (file.staged) {
|
||||
diffArgs = ["diff", "--cached", "--", file.path];
|
||||
const h = await pi.exec("git", ["show", `HEAD:${file.path}`], { cwd: ctx.cwd });
|
||||
old = h.code === 0 ? h.stdout : "";
|
||||
const i = await pi.exec("git", ["show", `:${file.path}`], { cwd: ctx.cwd });
|
||||
if (i.code !== 0) return { error: `Staged content failed for '${file.path}': ${i.stderr}` };
|
||||
cur = i.stdout;
|
||||
} else {
|
||||
diffArgs = ["diff", "--", file.path];
|
||||
const i = await pi.exec("git", ["show", `:${file.path}`], { cwd: ctx.cwd });
|
||||
old = i.code === 0 ? i.stdout : (await pi.exec("git", ["show", `HEAD:${file.path}`], { cwd: ctx.cwd })).stdout || "";
|
||||
try { cur = await readFile(abs, "utf-8"); }
|
||||
catch (e) { return { error: `Read failed: ${e}` }; }
|
||||
}
|
||||
|
||||
const result = await pi.exec("git", diffArgs, { cwd: ctx.cwd });
|
||||
if (result.code !== 0) {
|
||||
ctx.ui.notify(`Failed to get diff for ${file.path}: ${result.stderr}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.stdout.trim()) {
|
||||
ctx.ui.notify(`No diff available for ${file.path}`, "info");
|
||||
return;
|
||||
}
|
||||
|
||||
await showDiffViewer(file.path, result.stdout, ctx);
|
||||
return { diff: generateDiffString(old, cur).diff };
|
||||
}
|
||||
|
||||
async function showDiffViewer(filePath: string, diffText: string, ctx: ExtensionContext): Promise<void> {
|
||||
await ctx.ui.custom<void>((tui, theme, _kb, done) => {
|
||||
const viewer = new DiffViewer(theme, filePath, diffText);
|
||||
viewer.onClose = () => done();
|
||||
|
||||
return {
|
||||
render: (w) => viewer.render(w),
|
||||
invalidate: () => viewer.invalidate(),
|
||||
handleInput: (data) => {
|
||||
viewer.handleInput(data);
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
async function showViewer(path: string, diff: string, ctx: ExtensionContext): Promise<void> {
|
||||
await ctx.ui.custom<void>((tui, theme, _, done) => {
|
||||
const v = new DiffViewer(theme, path, diff);
|
||||
v.onClose = () => done();
|
||||
return { render: (w) => v.render(w), invalidate: () => v.invalidate(), handleInput: (d) => { v.handleInput(d); tui.requestRender(); } };
|
||||
}, { overlay: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the file picker overlay
|
||||
*/
|
||||
async function showFilePicker(files: GitFile[], ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
||||
// Group files by status
|
||||
const items: SelectItem[] = files.map((file) => {
|
||||
const statusLabel = STATUS_LABELS[file.status];
|
||||
const stagedLabel = file.staged ? " (staged)" : "";
|
||||
return {
|
||||
value: file,
|
||||
label: file.path,
|
||||
description: `${statusLabel}${stagedLabel}`,
|
||||
};
|
||||
});
|
||||
async function showPicker(files: GitFile[], ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
||||
const items: SelectItem[] = files.map((f, i) => ({ value: i, label: f.path, description: `${STATUS_LABELS[f.status]}${f.staged ? " (staged)" : ""}` }));
|
||||
|
||||
const result = await ctx.ui.custom<GitFile | null>((tui, theme, _kb, done) => {
|
||||
const container = new Container();
|
||||
|
||||
// Top border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
// Title
|
||||
const title = new Text(theme.fg("accent", theme.bold("Git Changes")) + theme.fg("dim", ` (${files.length} files)`), 1, 0);
|
||||
container.addChild(title);
|
||||
|
||||
// File list
|
||||
const selectList = new SelectList(items, Math.min(items.length, 15), {
|
||||
selectedPrefix: (t) => theme.fg("accent", t),
|
||||
selectedText: (t) => theme.fg("accent", t),
|
||||
description: (t) => {
|
||||
// Color description based on status keyword
|
||||
if (t.startsWith("modified")) return theme.fg("warning", t);
|
||||
if (t.startsWith("added")) return theme.fg("success", t);
|
||||
if (t.startsWith("deleted")) return theme.fg("error", t);
|
||||
if (t.startsWith("untracked")) return theme.fg("muted", t);
|
||||
if (t.startsWith("renamed") || t.startsWith("copied")) return theme.fg("warning", t);
|
||||
if (t.startsWith("unmerged")) return theme.fg("error", t);
|
||||
return theme.fg("muted", t);
|
||||
},
|
||||
scrollInfo: (t) => theme.fg("dim", t),
|
||||
noMatch: (t) => theme.fg("warning", t),
|
||||
const idx = await ctx.ui.custom<number | null>((tui, theme, _, done) => {
|
||||
const c = new Container();
|
||||
c.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
c.addChild(new Text(`${theme.fg("accent", theme.bold("Git Changes"))} ${theme.fg("dim", `(${files.length})`)}`, 1, 0));
|
||||
const list = new SelectList(items, Math.min(items.length, 15), {
|
||||
selectedPrefix: (t) => theme.fg("accent", t), selectedText: (t) => theme.fg("accent", t),
|
||||
description: (t) => theme.fg(t.startsWith("modified") ? "warning" : t.startsWith("added") ? "success" : t.startsWith("deleted") ? "error" : "muted", t),
|
||||
scrollInfo: (t) => theme.fg("dim", t), noMatch: (t) => theme.fg("warning", t),
|
||||
});
|
||||
|
||||
selectList.onSelect = (item) => done(item.value as GitFile);
|
||||
selectList.onCancel = () => done(null);
|
||||
container.addChild(selectList);
|
||||
|
||||
// Help text
|
||||
container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • type to filter • esc close"), 1, 0));
|
||||
|
||||
// Bottom border
|
||||
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
|
||||
return {
|
||||
render: (w) => container.render(w),
|
||||
invalidate: () => container.invalidate(),
|
||||
handleInput: (data) => {
|
||||
selectList.handleInput(data);
|
||||
tui.requestRender();
|
||||
},
|
||||
};
|
||||
list.onSelect = (item) => done(item.value as number);
|
||||
list.onCancel = () => done(null);
|
||||
c.addChild(list);
|
||||
c.addChild(new Text(theme.fg("dim", "↑↓ nav • enter select • type filter • esc close"), 1, 0));
|
||||
c.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
|
||||
return { render: (w) => c.render(w), invalidate: () => c.invalidate(), handleInput: (d) => { list.handleInput(d); tui.requestRender(); } };
|
||||
}, { overlay: true });
|
||||
|
||||
if (result) {
|
||||
await showFileDiff(result, ctx, pi);
|
||||
// After viewing diff, show file picker again
|
||||
await showFilePicker(files, ctx, pi);
|
||||
if (idx !== null) {
|
||||
const file = files[idx];
|
||||
const r = await getDiff(file, ctx, pi);
|
||||
if ("error" in r) ctx.ui.notify(r.error, "error");
|
||||
else if (!r.diff.trim()) ctx.ui.notify(`No changes in ${file.path}`, "info");
|
||||
else await showViewer(file.path, r.diff, ctx);
|
||||
await showPicker(files, ctx, pi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main handler for showing the diff overlay
|
||||
*/
|
||||
async function showDiffOverlay(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
||||
// Check if we're in a git repo
|
||||
const gitCheck = await pi.exec("git", ["rev-parse", "--git-dir"], { cwd: ctx.cwd });
|
||||
if (gitCheck.code !== 0) {
|
||||
ctx.ui.notify("Not in a git repository", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files
|
||||
const statusResult = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
||||
if (statusResult.code !== 0) {
|
||||
ctx.ui.notify(`Git status failed: ${statusResult.stderr}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!statusResult.stdout.trim()) {
|
||||
ctx.ui.notify("No changes in working tree", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
const files = parseGitStatus(statusResult.stdout);
|
||||
if (files.length === 0) {
|
||||
ctx.ui.notify("No changes in working tree", "info");
|
||||
return;
|
||||
}
|
||||
|
||||
await showFilePicker(files, ctx, pi);
|
||||
async function run(ctx: ExtensionContext, pi: ExtensionAPI): Promise<void> {
|
||||
if ((await pi.exec("git", ["rev-parse", "--git-dir"], { cwd: ctx.cwd })).code !== 0) { ctx.ui.notify("Not a git repo", "error"); return; }
|
||||
const s = await pi.exec("git", ["status", "--porcelain"], { cwd: ctx.cwd });
|
||||
if (s.code !== 0) { ctx.ui.notify(`Git failed: ${s.stderr}`, "error"); return; }
|
||||
if (!s.stdout.trim()) { ctx.ui.notify("No changes", "info"); return; }
|
||||
const files = parseGitStatus(s.stdout);
|
||||
if (!files.length) { ctx.ui.notify("No changes", "info"); return; }
|
||||
await showPicker(files, ctx, pi);
|
||||
}
|
||||
|
||||
export default function gitDiffExtension(pi: ExtensionAPI) {
|
||||
// Register Ctrl+F shortcut
|
||||
pi.registerShortcut(Key.ctrl("f"), {
|
||||
description: "Show git diff overlay",
|
||||
handler: async (ctx) => {
|
||||
await showDiffOverlay(ctx, pi);
|
||||
},
|
||||
});
|
||||
|
||||
// Register /diff command
|
||||
pi.registerCommand("diff", {
|
||||
description: "Show modified files and their diffs",
|
||||
handler: async (_args, ctx) => {
|
||||
await showDiffOverlay(ctx, pi);
|
||||
},
|
||||
});
|
||||
export default function (pi: ExtensionAPI) {
|
||||
pi.registerShortcut(Key.ctrl("f"), { description: "Git diff overlay", handler: (ctx) => run(ctx, pi) });
|
||||
pi.registerCommand("diff", { description: "Show git changes", handler: (_, ctx) => run(ctx, pi) });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue