Repository URL to install this package:
|
Version:
0.4.41 ▾
|
import React, { useCallback, useEffect, useState } from "react";
import { Box, Text, useInput, useStdout } from "ink";
import { api } from "../rpc.js";
import { Panel } from "../components/Panel.js";
import { HelpOverlay } from "../components/HelpOverlay.js";
function clamp(value, max) {
if (max <= 0)
return 0;
return Math.max(0, Math.min(value, max - 1));
}
const HELP_HINTS = [
{ key: "j/k", label: "navigate files" },
{ key: "enter", label: "view file patch" },
{ key: "r", label: "refresh" },
{ key: "?", label: "help" },
{ key: "esc", label: "back" },
];
const BAR_HINTS = [
{ key: "j/k", label: "navigate" },
{ key: "enter", label: "view patch" },
{ key: "r", label: "refresh" },
{ key: "?", label: "help" },
{ key: "esc", label: "back" },
];
export function DiffViewer({ ticketId, ticketTitle, projectId, onBack, onQuit, onHints, onStatus }) {
const [diff, setDiff] = useState(null);
const [selected, setSelected] = useState(0);
const [patch, setPatch] = useState(null);
const [patchScroll, setPatchScroll] = useState(0);
const [status, setStatus] = useState(null);
const [loading, setLoading] = useState(true);
const [showHelp, setShowHelp] = useState(false);
const { stdout } = useStdout();
const visibleRows = (stdout?.rows ?? 24) - 8;
const refresh = useCallback(async () => {
try {
const result = await api.gitDiff(ticketId, projectId);
setDiff(result);
setLoading(false);
}
catch (err) {
setStatus(`Error: ${err instanceof Error ? err.message : String(err)}`);
setLoading(false);
}
}, [ticketId, projectId]);
useEffect(() => { refresh(); }, [refresh]);
useEffect(() => {
if (!status)
return;
const timer = setTimeout(() => setStatus(null), 4000);
return () => clearTimeout(timer);
}, [status]);
// Report status/hints to parent for app-level StatusBar
useEffect(() => { onStatus?.(status ?? null); }, [status]);
useEffect(() => {
onHints?.(patch !== null
? [{ key: "j/k", label: "scroll" }, { key: "esc", label: "back to files" }]
: BAR_HINTS);
}, [patch !== null]);
const loadPatch = useCallback(async (file) => {
try {
const result = await api.gitDiffFile(ticketId, file.path, projectId);
setPatch(result.patch || "(no diff available)");
setPatchScroll(0);
}
catch (err) {
setPatch(`(error: ${err instanceof Error ? err.message : String(err)})`);
}
}, [ticketId, projectId]);
useInput((input, key) => {
if (showHelp)
return;
if (input === "?") {
setShowHelp(true);
return;
}
if (key.escape) {
if (patch !== null) {
setPatch(null);
return;
}
onBack();
return;
}
if (input === "q") {
onQuit();
return;
}
if (input === "r") {
setLoading(true);
refresh();
setStatus("Refreshed");
return;
}
if (patch !== null) {
const patchLines = patch.split("\n");
if (key.upArrow || input === "k") {
setPatchScroll((v) => Math.max(0, v - 1));
return;
}
if (key.downArrow || input === "j") {
setPatchScroll((v) => Math.min(Math.max(0, patchLines.length - visibleRows), v + 1));
return;
}
return;
}
const files = diff?.files ?? [];
if (key.upArrow || input === "k") {
setSelected((v) => clamp(v - 1, files.length));
return;
}
if (key.downArrow || input === "j") {
setSelected((v) => clamp(v + 1, files.length));
return;
}
if (key.return && files[selected]) {
loadPatch(files[selected]);
return;
}
});
if (showHelp) {
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
React.createElement(HelpOverlay, { title: "Git Diff", hints: HELP_HINTS, onClose: () => setShowHelp(false) })));
}
const diffSummary = diff
? `${diff.total_files} files +${diff.total_additions} -${diff.total_deletions}`
: "";
return (React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, patch !== null ? (
// Patch view
React.createElement(Panel, { title: diff?.files[selected]?.path ?? "Patch", focused: true, flexGrow: 1 },
React.createElement(Box, { flexDirection: "column", paddingX: 1 }, patch.split("\n").slice(patchScroll, patchScroll + visibleRows).map((line, i) => {
let color;
if (line.startsWith("+") && !line.startsWith("+++"))
color = "green";
else if (line.startsWith("-") && !line.startsWith("---"))
color = "red";
else if (line.startsWith("@@"))
color = "cyan";
return React.createElement(Text, { key: i, color: color, wrap: "truncate" }, line);
})))) : (
// File list
React.createElement(Panel, { title: `Diff \u2014 ${ticketTitle}`, badge: diffSummary ? React.createElement(Text, { color: "gray" }, diffSummary) : undefined, focused: true, flexGrow: 1 },
React.createElement(Box, { flexDirection: "column", paddingX: 1 }, loading ? (React.createElement(Text, { color: "gray" }, "Loading diff...")) : !diff || diff.files.length === 0 ? (React.createElement(Text, { color: "gray" }, "No changes detected.")) : (diff.files.map((file, i) => {
const isSelected = i === selected;
return (React.createElement(Text, { key: file.path, color: isSelected ? "cyan" : undefined, wrap: "truncate" },
isSelected ? "\u25B8 " : " ",
file.untracked ? React.createElement(Text, { color: "yellow" }, "? ") : file.staged ? React.createElement(Text, { color: "green" }, "S ") : React.createElement(Text, { color: "blue" }, "M "),
file.path,
file.is_binary ? (React.createElement(Text, { color: "gray" }, " (binary)")) : (React.createElement(Text, { color: "gray" },
" ",
React.createElement(Text, { color: "green" },
"+",
file.additions),
" ",
React.createElement(Text, { color: "red" },
"-",
file.deletions)))));
})))))));
}
//# sourceMappingURL=DiffViewer.js.map