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.
21 kB · 549 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path";import { visibleWidth } from "@earendil-works/pi-tui";import { afterAll, beforeAll, describe, expect, it } from "vitest";import { renderRichDiff } from "../src/modes/interactive/components/diff.js";import { IPythonCellComponent } from "../src/modes/interactive/components/ipython-cell.js";import { initTheme, preloadCodeHighlighter, theme } from "../src/modes/interactive/theme/theme.js";
function stripAnsi(text: string): string { return text.replace(/\x1b\[[0-9;]*m/g, "");}
function hasBackground(line: string): boolean { return /\x1b\[4[0-9]m|\x1b\[48[;:]/.test(line);}
type ThemeFg = Parameters<typeof theme.fg>[0];type ThemeBg = Parameters<typeof theme.bg>[0];
function fgAnsi(color: ThemeFg): string { return theme.fg(color, "").replace(/\x1b\[39m$/, "");}
function bgAnsi(color: ThemeBg): string { return theme.bg(color, "").replace(/\x1b\[49m$/, "");}
function changedRow(rows: readonly string[], prefix: "+" | "-"): string { const row = rows.find((line) => stripAnsi(line).startsWith(` 1 ${prefix} `)); expect(row).toBeDefined(); return row ?? "";}
function renderCell(state: ConstructorParameters<typeof IPythonCellComponent>[0]): string { return stripAnsi(new IPythonCellComponent(state).render(80).join("\n"));}
describe("IPythonCellComponent diff rendering", () => { beforeAll(() => { initTheme("dark"); });
it("renders a diff with absolute line numbers and suppresses the Edited confirmation", () => { const out = renderCell({ code: 'await edit(path="sample.py", old_str="gamma", new_str="GAMMA")', details: { status: "ok", durationMs: 12, result: "'Edited sample.py'", diffs: [{ path: "sample.py", oldStr: "alpha\ngamma\ndelta", newStr: "alpha\nGAMMA\ndelta", startLine: 10 }], }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, });
expect(out).toContain("sample.py"); expect(out).not.toMatch(/edit sample\.py/); expect(out).toMatch(/\+1\s+-1/); expect(out).toMatch(/11 - .*gamma/); expect(out).toMatch(/11 \+ .*GAMMA/); expect(out).toMatch(/10 .*alpha/); expect(out.split("\n").some((line) => /^\s*'?Edited sample\.py'?\s*$/.test(line.trim()))).toBe(false); expect(out).toContain('await edit(path="sample.py", old_str="gamma", new_str="GAMMA")'); });
it("shows diffs on collapsed cells when edit diffs are expanded", () => { const state = { code: 'await edit(path="sample.py", old_str="gamma", new_str="GAMMA")\nprint("edit-done-marker")', details: { status: "ok", durationMs: 12, result: "'Edited sample.py'", stdout: "unrelated stdout line", diffs: [{ path: "sample.py", oldStr: "alpha\ngamma\ndelta", newStr: "alpha\nGAMMA\ndelta", startLine: 10 }], }, executionStarted: true, argsComplete: true, };
const collapsedWithDiffs = renderCell({ ...state, expanded: false, editDiffsExpanded: true }); expect(collapsedWithDiffs).toMatch(/11 - .*gamma/); expect(collapsedWithDiffs).toMatch(/11 \+ .*GAMMA/); expect(collapsedWithDiffs).not.toContain('print("edit-done-marker")'); expect(collapsedWithDiffs).not.toContain("unrelated stdout line");
const collapsed = renderCell({ ...state, expanded: false, editDiffsExpanded: false }); expect(collapsed).not.toMatch(/11 - .*gamma/); expect(collapsed).not.toMatch(/11 \+ .*GAMMA/);
const expanded = renderCell({ ...state, expanded: true, editDiffsExpanded: false }); expect(expanded).not.toMatch(/11 - .*gamma/); expect(expanded).toContain('print("edit-done-marker")'); });
it("renders diff rows as full-width colored blocks", () => { const width = 72; const lines = new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: "sample.py", oldStr: "alpha\ngamma\ndelta", newStr: "alpha\nGAMMA\ndelta", startLine: 1 }], }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(width); const diffRows = lines.filter((line) => /alpha|gamma|GAMMA/.test(stripAnsi(line))); expect(diffRows.length).toBeGreaterThan(0); expect(diffRows.every((line) => visibleWidth(line) === width)).toBe(true); expect(diffRows.some(hasBackground)).toBe(true); });
it("always shows the summary line and indents diff rows to its text column", () => { const state = { code: "await edit(...)", details: { status: "ok", diffs: [{ path: "a.ts", oldStr: "x", newStr: "X", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: false, };
const hidden = renderCell({ ...state, editDiffsExpanded: false }).split("\n"); const hiddenSummary = hidden.find((l) => l.includes("╰─ a.ts")); expect(hiddenSummary).toMatch(/^ {4}╰─ a\.ts \+1 -1 · .*to expand\)$/);
const notLatest = renderCell({ ...state, editDiffsExpanded: false, showExpandHint: false }).split("\n"); expect(notLatest.find((l) => l.includes("╰─ a.ts"))).toMatch(/to expand\)$/); expect(hidden.some((l) => /1 - .*x/.test(l))).toBe(false);
const shown = renderCell({ ...state, editDiffsExpanded: true }).split("\n"); const summary = shown.find((l) => l.includes("╰─ a.ts")); expect(summary).toMatch(/^ {4}╰─ a\.ts \+1 -1 · .*to collapse\)$/); const textColumn = (summary ?? "").indexOf("a.ts"); const removed = shown.find((l) => /1 - .*x/.test(l)); const added = shown.find((l) => /1 \+ .*X/.test(l)); expect(removed).toBeDefined(); expect(added).toBeDefined(); for (const row of [removed ?? "", added ?? ""]) { expect(row.startsWith(" ".repeat(textColumn))).toBe(true); } expect(shown.filter((l) => !/\d+ [-+ ] /.test(l) && !l.includes("⋮")).length).toBe(hidden.length); });
it("renders an edit path relative to the session cwd, or absolute when outside it", () => { const cwd = "/home/me/project"; const inside = renderCell({ code: "await edit(...)", cwd, details: { status: "ok", diffs: [{ path: `${cwd}/src/app.ts`, oldStr: "x", newStr: "X", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }); expect(inside).toContain("src/app.ts"); expect(inside).not.toContain(`${cwd}/src/app.ts`);
const outside = renderCell({ code: "await edit(...)", cwd, details: { status: "ok", diffs: [{ path: "/etc/hosts", oldStr: "a", newStr: "b", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }); expect(outside).toContain("/etc/hosts"); });
it("formats the summary path like the built-in edit tool, resolving symlinked cwds", () => { const root = mkdtempSync(join(tmpdir(), "ipython-cell-diff-symlink-")); try { const realCwd = join(root, "real"); const linkedCwd = join(root, "linked"); mkdirSync(realCwd); writeFileSync(join(realCwd, "same.ts"), "x"); symlinkSync(realCwd, linkedCwd, "dir"); const out = renderCell({ code: "await edit(...)", cwd: linkedCwd, details: { status: "ok", diffs: [{ path: join(realpathSync(realCwd), "same.ts"), oldStr: "x", newStr: "X", startLine: 1 }], }, executionStarted: true, argsComplete: true, expanded: false, editDiffsExpanded: false, }); expect(out).toContain("╰─ same.ts +1 -1"); } finally { rmSync(root, { recursive: true, force: true }); } });
it("wraps a long diff line across rows without truncating or overflowing the width", () => { const width = 92; const longLine = `const x = ${Array.from({ length: 30 }, (_, i) => `arg${i}`).join(", ")};`; const lines = new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: "a.ts", oldStr: "const x = 1;", newStr: longLine, startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(width);
expect(lines.every((line) => visibleWidth(line) <= width)).toBe(true); const joined = lines.map(stripAnsi).join(""); expect(joined).toContain("arg0"); expect(joined).toContain("arg29"); expect(lines.filter((line) => /arg\d/.test(stripAnsi(line))).length).toBeGreaterThan(1); });
it("truncates a long summary path so it never overflows the width, keeping the counts", () => { const width = 40; const longPath = `src/${"very-long-directory-name/".repeat(8)}file.ts`; const lines = new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: longPath, oldStr: "x", newStr: "X", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(width); expect(lines.every((line) => visibleWidth(line) <= width)).toBe(true); const summary = lines.map(stripAnsi).find((line) => line.includes("…")); expect(summary).toBeDefined(); expect(summary).toMatch(/\+1 -1 · /); });
it("advertises the collapse key once per cell when diffs are expanded", () => { const lines = new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [ { path: "a.ts", oldStr: "x", newStr: "X", startLine: 1 }, { path: "b.ts", oldStr: "y", newStr: "Y", startLine: 1 }, ], }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(120); const plain = lines.map(stripAnsi); const hinted = plain.filter((line) => line.includes("to collapse") && /[+]\d+ -\d+/.test(line)); expect(hinted).toHaveLength(1); expect(hinted[0]).toContain("b.ts"); });
it("never overflows a narrow pane when expanded diffs render", () => { const width = 24; const lines = new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: "src/some/dir/file.ts", oldStr: "x", newStr: "X", startLine: 1 }], }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(width); expect(lines.every((line) => visibleWidth(line) <= width)).toBe(true); });
it("renders a large diff without spreading the row array (no RangeError)", () => { const n = 5000; const oldStr = Array.from({ length: n }, (_, i) => `line ${i}`).join("\n"); const newStr = Array.from({ length: n }, (_, i) => `LINE ${i}`).join("\n"); expect(() => new IPythonCellComponent({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: "big.txt", oldStr, newStr, startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).render(80), ).not.toThrow(); });
it("uses a blank hanging gutter for wrapped diff rows", () => { const longLine = `const x = ${Array.from({ length: 20 }, (_, i) => `arg${i}`).join(", ")};`; const out = renderCell({ code: "await edit(...) ", details: { status: "ok", diffs: [{ path: "a.ts", oldStr: "const x = 1;", newStr: longLine, startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).split("\n"); const addedRows = out.filter((line) => /arg\d/.test(line)); expect(addedRows.length).toBeGreaterThan(1); expect(addedRows[0]).toMatch(/\d+ \+ /); expect(addedRows.slice(1).every((line) => !line.includes("+"))).toBe(true); });
it("renders expanded source before the always-visible diff", () => { const out = renderCell({ code: "await edit(...)", details: { status: "ok", durationMs: 4, diffs: [{ path: "a.ts", oldStr: "x", newStr: "X", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }).split("\n"); expect(out[0]).toContain("to collapse"); expect(out[1].trim()).toBe(""); expect(out[2]).toContain("await edit(...)"); expect(out.findIndex((line) => line.includes("a.ts"))).toBeGreaterThan(2); });
it("keeps the summary line but hides diff rows when edit diffs are collapsed", () => { const collapsed = renderCell({ code: "await edit(...)", details: { status: "ok", diffs: [{ path: "big.py", oldStr: "old", newStr: "NEW", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: false, }); expect(collapsed).toContain("╰─ big.py +1 -1"); expect(collapsed).toContain("to expand"); expect(collapsed).not.toContain("old"); expect(collapsed).not.toContain("NEW"); });
it("hides edit source when collapsed and shows it when globally expanded", () => { const state = { code: 'hidden_side_effect = "only in full source"\nawait edit(path="a.py", old_str="old", new_str="new")', details: { status: "ok", diffs: [{ path: "a.py", oldStr: "old", newStr: "new", startLine: 1 }] }, executionStarted: true, argsComplete: true, }; const collapsed = renderCell({ ...state, expanded: false }); const expanded = renderCell({ ...state, expanded: true, editDiffsExpanded: true });
expect(collapsed).not.toContain("hidden_side_effect"); expect(collapsed).toContain("a.py"); expect(expanded).toContain('hidden_side_effect = "only in full source"'); expect(expanded).toContain("a.py"); const expandedLines = expanded.split("\n"); expect(expandedLines.findIndex((line) => line.includes("hidden_side_effect ="))).toBeLessThan( expandedLines.findIndex((line) => /╰─ a\.py \+1 -1/.test(line)), ); });
it("keeps non-edit cells collapsed to a single summary line", () => { const collapsed = renderCell({ code: "print('hello')", details: { status: "ok", durationMs: 3, stdout: "hello\nworld\nmore\noutput\nlines" }, executionStarted: true, argsComplete: true, expanded: false, }); expect(collapsed.split("\n")).toHaveLength(1); expect(collapsed).toContain("to expand"); expect(collapsed).not.toContain("world"); });
it("coalesces multiple edits to one file into a single block with hunk separators", () => { const out = renderCell({ code: "await edit(...); await edit(...); await edit(...)", expanded: true, editDiffsExpanded: true, details: { status: "ok", diffs: [ { path: "app.py", oldStr: "a = 1", newStr: "a = 2", startLine: 1 }, { path: "app.py", oldStr: "b = 1", newStr: "b = 2", startLine: 50 }, { path: "app.py", oldStr: "c = 1", newStr: "c = 2", startLine: 90 }, ], }, executionStarted: true, argsComplete: true, }); expect(out.split("\n").filter((l) => l.includes("app.py")).length).toBe(1); expect(out).toMatch(/app\.py\s+\+3\s+-3/); expect((out.match(/⋮/g) ?? []).length).toBe(2); });
it("keeps the top line stable when expanded — only the hint flips, no header line, no layout shift", () => { const state = { code: "print(55)", content: [{ type: "text", text: "55" }], details: { status: "ok", durationMs: 780_000 }, executionStarted: true, argsComplete: true, }; const collapsed = new IPythonCellComponent({ ...state, expanded: false }).render(80); const expanded = new IPythonCellComponent({ ...state, expanded: true }).render(80);
expect(stripAnsi(collapsed[0])).toMatch(/^ ✓ python · .* · ↑ 1 ↓ 1 lines · 780\.0s · \(.*to expand\)$/); expect(stripAnsi(expanded[0])).toMatch(/^ ✓ python · .* · ↑ 1 ↓ 1 lines · 780\.0s · \(.*to collapse\)$/); const upToHint = (line: string) => stripAnsi(line).replace(/· \([^·]*to (expand|collapse)\)$/, ""); expect(upToHint(expanded[0])).toBe(upToHint(collapsed[0]));
const stripped = expanded.map(stripAnsi); expect(stripped.filter((l) => /python · done/.test(l)).length).toBe(0);
expect(stripped.join("\n")).toContain("print(55)"); expect(stripped.join("\n")).toContain("55"); expect(expanded.some(hasBackground)).toBe(false); });
it("does not show a 'no output' line under an edit-only diff", () => { const out = renderCell({ code: 'await edit(path="a.ts", old_str="x", new_str="X")', details: { status: "ok", diffs: [{ path: "a.ts", oldStr: "x", newStr: "X", startLine: 1 }] }, executionStarted: true, argsComplete: true, expanded: true, editDiffsExpanded: true, }); expect(out).toContain("a.ts"); expect(out).not.toContain("no output"); });
it("never emits a line wider than the viewport, even in a very narrow pane", () => { for (const width of [1, 2, 3, 5, 8]) { const lines = new IPythonCellComponent({ code: "import numpy as np\nresult = np.linspace(0, 100, 50)", content: [{ type: "text", text: "a fairly long line of output that must wrap" }], details: { status: "ok", durationMs: 12, stdout: "a fairly long line of output that must wrap" }, executionStarted: true, argsComplete: true, expanded: true, }).render(width); expect( lines.every((line) => visibleWidth(line) <= width), `width=${width}`, ).toBe(true); } });});
describe("renderRichDiff syntax highlighting", () => { let previousColorTerm: string | undefined; let previousTerm: string | undefined; let previousWtSession: string | undefined;
beforeAll(async () => { previousColorTerm = process.env.COLORTERM; previousTerm = process.env.TERM; previousWtSession = process.env.WT_SESSION; await preloadCodeHighlighter(); });
afterAll(() => { if (previousColorTerm === undefined) { delete process.env.COLORTERM; } else { process.env.COLORTERM = previousColorTerm; } if (previousTerm === undefined) { delete process.env.TERM; } else { process.env.TERM = previousTerm; } if (previousWtSession === undefined) { delete process.env.WT_SESSION; } else { process.env.WT_SESSION = previousWtSession; } initTheme("dark"); });
function useTruecolor(themeName: "dark" | "light" = "dark"): void { process.env.COLORTERM = "truecolor"; process.env.TERM = "xterm-256color"; initTheme(themeName); }
function use256Color(): void { delete process.env.COLORTERM; delete process.env.WT_SESSION; process.env.TERM = "dumb"; initTheme("dark"); }
it("keeps syntax token foregrounds on truecolor added and removed backgrounds", () => { useTruecolor(); const rows = renderRichDiff("-1 const answer: number = 1;\n+1 const answer: number = 2;", 72, { language: "typescript", }); const removed = changedRow(rows, "-"); const added = changedRow(rows, "+");
expect(removed).toContain(bgAnsi("toolDiffRemovedBg")); expect(added).toContain(bgAnsi("toolDiffAddedBg")); for (const row of [removed, added]) { expect(row).toContain(fgAnsi("syntaxKeyword")); expect(row).toContain(fgAnsi("syntaxType")); expect(row).toContain(fgAnsi("syntaxNumber")); expect(row).not.toContain(fgAnsi("toolDiffText")); } });
it("uses the code-block foreground when the language is unknown", () => { useTruecolor(); const rows = renderRichDiff("-1 old value\n+1 new value", 48, { language: "not-a-language" });
expect(changedRow(rows, "-")).toContain(fgAnsi("mdCodeBlock")); expect(changedRow(rows, "+")).toContain(fgAnsi("mdCodeBlock")); });
it("keeps the flat red and green fallback in 256-color mode", () => { use256Color(); const rows = renderRichDiff("-1 const answer: number = 1;\n+1 const answer: number = 2;", 72, { language: "typescript", }); const removed = changedRow(rows, "-"); const added = changedRow(rows, "+");
expect(removed).toContain(bgAnsi("toolPanelBg")); expect(removed).toContain(fgAnsi("toolDiffRemoved")); expect(added).toContain(bgAnsi("toolPanelBg")); expect(added).toContain(fgAnsi("toolDiffAdded")); expect(removed).not.toContain(fgAnsi("syntaxKeyword")); expect(added).not.toContain(fgAnsi("syntaxKeyword")); });
it("preserves themed syntax and the diff background across wrapped rows", () => { const source = `const values: number[] = [${Array.from({ length: 20 }, (_, index) => index).join(", ")}];`; for (const themeName of ["dark", "light"] as const) { useTruecolor(themeName); const keyword = fgAnsi("syntaxKeyword"); const background = bgAnsi("toolDiffAddedBg"); const rows = renderRichDiff(`+1 ${source}`, 28, { language: "typescript" });
expect(rows.length).toBeGreaterThan(1); expect(rows.map(stripAnsi).join("")).toContain("19"); for (const row of rows) { expect(visibleWidth(row)).toBe(28); expect(row).toContain(background); expect(row).not.toContain("\x1b[0m"); expect(row.match(/\x1b\[49m/g)).toHaveLength(1); expect(row.endsWith("\x1b[49m")).toBe(true); } expect(rows.join("")).toContain(keyword); } });});