Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
12 kB · 394 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";import * as Diff from "diff";import { getLanguageFromPath, highlightCode, theme } from "../theme/theme.js";
/** * Parse diff line to extract prefix, line number, and content. * Supports both numbered lines ("+123 content") and standard unified diff lines ("+content"). */function parseDiffLine(line: string): { prefix: string; lineNum: string; content: string } | null { const numberedMatch = line.match(/^([+-\s])(\s*\d+)\s(.*)$/); if (numberedMatch) { return { prefix: numberedMatch[1], lineNum: numberedMatch[2], content: numberedMatch[3] }; }
if (line.startsWith("+++") || line.startsWith("---")) { return null; }
if (line.startsWith("+")) { return { prefix: "+", lineNum: "", content: line.slice(1) }; } if (line.startsWith("-")) { return { prefix: "-", lineNum: "", content: line.slice(1) }; } if (line.startsWith(" ")) { return { prefix: " ", lineNum: "", content: line.slice(1) }; }
return null;}
/** * Replace tabs with spaces for consistent rendering. */function replaceTabs(text: string): string { return text.replace(/\t/g, " ");}
/** * Compute word-level diff and render with inverse on changed parts. * Uses diffWords which groups whitespace with adjacent words for cleaner highlighting. * Strips leading whitespace from inverse to avoid highlighting indentation. */function renderIntraLineDiff(oldContent: string, newContent: string): { removedLine: string; addedLine: string } { const wordDiff = Diff.diffWords(oldContent, newContent);
let removedLine = ""; let addedLine = ""; let isFirstRemoved = true; let isFirstAdded = true;
for (const part of wordDiff) { if (part.removed) { let value = part.value; // Strip leading whitespace from the first removed part if (isFirstRemoved) { const leadingWs = value.match(/^(\s*)/)?.[1] || ""; value = value.slice(leadingWs.length); removedLine += leadingWs; isFirstRemoved = false; } if (value) { removedLine += theme.inverse(value); } } else if (part.added) { let value = part.value; // Strip leading whitespace from the first added part if (isFirstAdded) { const leadingWs = value.match(/^(\s*)/)?.[1] || ""; value = value.slice(leadingWs.length); addedLine += leadingWs; isFirstAdded = false; } if (value) { addedLine += theme.inverse(value); } } else { removedLine += part.value; addedLine += part.value; } }
return { removedLine, addedLine };}
export interface RenderDiffOptions { /** File path (unused, kept for API compatibility) */ filePath?: string;}
/** * Render a diff string with colored lines and intra-line change highlighting. * - Context lines: dim/gray * - Removed lines: red, with inverse on changed tokens * - Added lines: green, with inverse on changed tokens */export function renderDiff(diffText: string, _options: RenderDiffOptions = {}): string { const lines = diffText.split("\n"); const result: string[] = [];
let i = 0; while (i < lines.length) { const line = lines[i]; const parsed = parseDiffLine(line);
if (!parsed) { result.push(theme.fg("toolDiffContext", line)); i++; continue; }
if (parsed.prefix === "-") { // Collect consecutive removed lines const removedLines: { lineNum: string; content: string }[] = []; while (i < lines.length) { const p = parseDiffLine(lines[i]); if (!p || p.prefix !== "-") break; removedLines.push({ lineNum: p.lineNum, content: p.content }); i++; }
// Collect consecutive added lines const addedLines: { lineNum: string; content: string }[] = []; while (i < lines.length) { const p = parseDiffLine(lines[i]); if (!p || p.prefix !== "+") break; addedLines.push({ lineNum: p.lineNum, content: p.content }); i++; }
// Only do intra-line diffing when there's exactly one removed and one added line // (indicating a single line modification). Otherwise, show lines as-is. if (removedLines.length === 1 && addedLines.length === 1) { const removed = removedLines[0]; const added = addedLines[0];
const { removedLine, addedLine } = renderIntraLineDiff( replaceTabs(removed.content), replaceTabs(added.content), );
result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${removedLine}`)); result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${addedLine}`)); } else { // Show all removed lines first, then all added lines for (const removed of removedLines) { result.push(theme.fg("toolDiffRemoved", `-${removed.lineNum} ${replaceTabs(removed.content)}`)); } for (const added of addedLines) { result.push(theme.fg("toolDiffAdded", `+${added.lineNum} ${replaceTabs(added.content)}`)); } } } else if (parsed.prefix === "+") { // Standalone added line result.push(theme.fg("toolDiffAdded", `+${parsed.lineNum} ${replaceTabs(parsed.content)}`)); i++; } else { // Context line result.push(theme.fg("toolDiffContext", ` ${parsed.lineNum} ${replaceTabs(parsed.content)}`)); i++; } }
return result.join("\n");}
type ThemeBg = Parameters<typeof theme.bg>[0];type ThemeColor = Parameters<typeof theme.fg>[0];
// Rewrite full/background resets to foreground-only so a row's background block// survives the syntax-highlighted content.const BG_CLEARING_RESET = /\x1b\[(?:0|49)m/g;
function keepBackground(highlighted: string): string { return highlighted.replace(BG_CLEARING_RESET, "\x1b[39m");}
function highlightContent(content: string, language: string | undefined): string { if (!content) { return ""; } if (language) { return keepBackground(highlightCode(content, language)[0] ?? content); } return theme.fg("mdCodeBlock", content);}
interface DiffLineSpec { bg: ThemeBg; gutter: string; gutterFg: ThemeColor; content: string; language: string | undefined; width: number; /** Flat content color instead of syntax highlighting (256-color fallback). */ contentFg?: ThemeColor; /** Render content with dimmed intensity (faint). */ dim?: boolean;}
function padToWidth(inner: string, width: number): string { if (visibleWidth(inner) > width) { inner = truncateToWidth(inner, width, ""); } // Pad after truncating too: a 2-cell character straddling the cutoff leaves // the result a cell short. const pad = width - visibleWidth(inner); return pad > 0 ? inner + " ".repeat(pad) : inner;}
function dimHighlightedContent(highlighted: string): string { // Scale 24-bit truecolor RGB codes down to 50% luminance (half-brightness) let dimmed = highlighted.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (_match, r, g, b) => { const dr = Math.round(Number.parseInt(r, 10) * 0.5); const dg = Math.round(Number.parseInt(g, 10) * 0.5); const db = Math.round(Number.parseInt(b, 10) * 0.5); return `\x1b[38;2;${dr};${dg};${db}m`; });
// Wrap unstyled text and resets in muted gray so they don't bounce back to bright white const mutedAnsi = theme.fg("muted", ""); const mutedPrefix = mutedAnsi.replace(/\x1b\[39m/g, ""); dimmed = `${mutedPrefix}${dimmed.replace(/\x1b\[39m/g, mutedPrefix)}\x1b[39m`;
return dimmed;}
// One diff line as one-or-more full-width background rows. Content wider than the// row wraps onto continuation rows with a blank gutter, so nothing is truncated.function buildRichDiffLine(spec: DiffLineSpec): string[] { let renderedContent = spec.contentFg ? theme.fg(spec.contentFg, spec.content) : highlightContent(spec.content, spec.language);
if (spec.dim) { renderedContent = dimHighlightedContent(renderedContent); }
const gutterWidth = visibleWidth(spec.gutter); const contentWidth = Math.max(1, spec.width - gutterWidth); const contentRows = wrapTextWithAnsi(renderedContent, contentWidth); if (contentRows.length === 0) { contentRows.push(""); }
const styledGutter = theme.fg(spec.gutterFg, spec.gutter); const styledCont = " ".repeat(gutterWidth); return contentRows.map((row, index) => { const gutter = index === 0 ? styledGutter : styledCont; return theme.bg(spec.bg, padToWidth(`${gutter}${row}\x1b[39m`, spec.width)); });}
export interface RichDiffOptions { /** Language id for syntax highlighting the diff content (e.g. "typescript"). */ language?: string;}
/** A dim `⋮` row separating non-adjacent hunks of one file's diff. */export function renderDiffSeparator(contentWidth: number): string { const width = Math.max(1, contentWidth); const marker = theme.fg("toolDiffContext", " ⋮"); const pad = Math.max(0, width - visibleWidth(marker)); return theme.bg("toolPanelBg", marker + " ".repeat(pad));}
/** Render a unified diff as full-width rows: green/red blocks, syntax-highlighted. */export function renderRichDiff(diffText: string, contentWidth: number, options: RichDiffOptions = {}): string[] { const width = Math.max(1, contentWidth); let currentLanguage = options.language; const rows: string[] = [];
for (const rawLine of diffText.split("\n")) { // File header: diff --git a/... b/... if (rawLine.startsWith("diff --git ")) { const match = rawLine.match(/diff --git a\/(.*?) b\/(.*)/); const fileName = match ? match[2] : rawLine.slice(11); currentLanguage = getLanguageFromPath(fileName) ?? options.language; const title = `─── ${fileName} `; const bar = "─".repeat(Math.max(0, width - visibleWidth(title) - 2)); rows.push( theme.bg( "toolPanelBg", padToWidth(` ${theme.fg("accent", theme.bold(title))}${theme.fg("dim", bar)}`, width), ), ); continue; }
// Git index or mode lines if ( rawLine.startsWith("index ") || rawLine.startsWith("new file mode ") || rawLine.startsWith("deleted file mode ") ) { rows.push(theme.bg("toolPanelBg", padToWidth(` ${theme.fg("dim", rawLine)}`, width))); continue; }
// File paths: --- a/... and +++ b/... if (rawLine.startsWith("--- ") || rawLine.startsWith("+++ ")) { const isAdd = rawLine.startsWith("+++ "); if (isAdd && rawLine.startsWith("+++ b/")) { currentLanguage = getLanguageFromPath(rawLine.slice(6).trim()) ?? currentLanguage; } const color = isAdd ? "toolDiffAdded" : "toolDiffRemoved"; rows.push(theme.bg("toolPanelBg", padToWidth(` ${theme.fg(color, rawLine)}`, width))); continue; }
// Hunk headers: @@ -... +... @@ if (rawLine.startsWith("@@ ")) { rows.push(theme.bg("toolPanelBg", padToWidth(` ${theme.fg("accent", rawLine)}`, width))); continue; }
// Commit metadata headers if ( rawLine.startsWith("commit ") || rawLine.startsWith("Author:") || rawLine.startsWith("AuthorDate:") || rawLine.startsWith("Commit:") || rawLine.startsWith("CommitDate:") ) { rows.push(theme.bg("toolPanelBg", padToWidth(` ${theme.fg("dim", rawLine)}`, width))); continue; }
// Commit message body if (rawLine.startsWith(" ") && currentLanguage === undefined) { rows.push(theme.bg("toolPanelBg", padToWidth(` ${theme.fg("muted", rawLine)}`, width))); continue; }
const parsed = parseDiffLine(rawLine); if (!parsed) { rows.push( ...buildRichDiffLine({ bg: "toolPanelBg", gutterFg: "toolDiffContext", gutter: " ", content: replaceTabs(rawLine), language: undefined, width, }), ); continue; }
const { prefix, lineNum, content } = parsed; const gutter = lineNum ? ` ${lineNum.padStart(4)} ${prefix} ` : ` ${prefix} `; const text = replaceTabs(content);
if (prefix === "+") { rows.push( ...buildRichDiffLine({ bg: "toolPanelBg", gutterFg: "toolDiffAdded", gutter, content: text, language: currentLanguage, width, contentFg: currentLanguage ? undefined : "toolDiffAdded", }), ); } else if (prefix === "-") { rows.push( ...buildRichDiffLine({ bg: "toolPanelBg", gutterFg: "toolDiffRemoved", gutter, content: text, language: currentLanguage, width, contentFg: currentLanguage ? undefined : "dim", dim: true, }), ); } else { rows.push( ...buildRichDiffLine({ bg: "toolPanelBg", gutterFg: "toolDiffContext", gutter, content: text, language: currentLanguage, width, }), ); } }
return rows;}