pi-claude-code-ui 1.0.86 → 1.0.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/extensions/index.ts +104 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
4
|
> **1.0.69 — package rename (permanent).** Canonical npm name is now [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui). `pi-claude-style-tools` is legacy and will not receive further releases. Install with `pi install npm:pi-claude-code-ui` or `npm i pi-claude-code-ui`.
|
|
5
5
|
|
|
6
|
+
## 1.0.88 — 2026-10-19
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`diffLayout` setting** ([#34](https://github.com/FammasMaz/pi-cc-tools/issues/34)) - `"auto"` (default) keeps the current behavior and uses split view only when it fits without heavy wrapping. `"split"` always shows side-by-side diffs when both code columns fit, and `"unified"` always stacks them. Applies to `edit`, `write` and `apply_patch` previews.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Finished edits keep their diff after resume, expand and resize** (Jerry, [#37](https://github.com/FammasMaz/pi-cc-tools/pull/37), fixes [#35](https://github.com/FammasMaz/pi-cc-tools/issues/35)) - edits are located once before they run and their start lines are saved in the result details, so a resumed session redraws the same diff at the same line numbers instead of only the stats line, and expanding or resizing no longer drops back to line 1. Sessions saved before this change are located from the patch Pi already stores with each edit result.
|
|
15
|
+
|
|
16
|
+
## 1.0.87 — 2026-10-19
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Expanding image reads no longer crashes Pi** — clear the image-source reuse cache together with the hidden image components when a `read` result is collapsed. Re-expanding standalone or grouped image reads now creates valid image components instead of reusing a missing component and throwing `TypeError: Cannot read properties of undefined (reading 'render')`.
|
|
21
|
+
|
|
6
22
|
## 1.0.86 — 2026-10-08
|
|
7
23
|
|
|
8
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Set in `.pi/settings.json` or the active Pi agent settings file (`$PI_CODING_AGE
|
|
|
57
57
|
"liveToolPreview": true,
|
|
58
58
|
"liveToolPreviewLines": 5,
|
|
59
59
|
"diffCollapsedLines": 24,
|
|
60
|
+
"diffLayout": "auto",
|
|
60
61
|
"themeAdaptive": true,
|
|
61
62
|
"diffTheme": "github-dark"
|
|
62
63
|
}
|
|
@@ -152,6 +153,7 @@ Use `/cc-tools` to control tool UI at runtime:
|
|
|
152
153
|
| `liveToolPreview` | `true` | Show a small live output preview while tools are still running |
|
|
153
154
|
| `liveToolPreviewLines` | `5` | Lines shown in the collapsed live preview |
|
|
154
155
|
| `diffCollapsedLines` | `24` | Diff lines before collapsing |
|
|
156
|
+
| `diffLayout` | `"auto"` | `"auto"` uses split view only when it fits without heavy wrapping; `"split"` always uses split view when both columns fit; `"unified"` always stacks |
|
|
155
157
|
|
|
156
158
|
## Notes
|
|
157
159
|
|
package/extensions/index.ts
CHANGED
|
@@ -124,6 +124,8 @@ interface SettingsFile {
|
|
|
124
124
|
liveToolPreviewLines?: number;
|
|
125
125
|
showTruncationHints?: boolean;
|
|
126
126
|
diffCollapsedLines?: number;
|
|
127
|
+
/** Diff layout: "auto" (default) picks split only when it fits cleanly; "split" and "unified" force one layout. */
|
|
128
|
+
diffLayout?: "auto" | "split" | "unified";
|
|
127
129
|
diffTheme?: string;
|
|
128
130
|
diffColors?: Record<string, string>;
|
|
129
131
|
/**
|
|
@@ -2965,6 +2967,11 @@ function syncLiveToolRenderState(component: any): void {
|
|
|
2965
2967
|
state._liveLineCount = collectNonEmptyLines(raw, 0).total;
|
|
2966
2968
|
} else if (component?.isPartial !== true) {
|
|
2967
2969
|
delete state._liveLineCount;
|
|
2970
|
+
// renderCall never sees the result, so hand a finished edit's details over: its diff is redrawn from them, not from
|
|
2971
|
+
// the file. A call aborted while streaming ends with a bare error instead of a toolResult; it never drew a diff.
|
|
2972
|
+
if (component?.toolName === "edit" && component.result && (!ctxLike.isError || component.result.role === "toolResult")) {
|
|
2973
|
+
state._editResult = component.result.details ?? {};
|
|
2974
|
+
}
|
|
2968
2975
|
}
|
|
2969
2976
|
}
|
|
2970
2977
|
|
|
@@ -3016,6 +3023,8 @@ function removeImageChildren(component: any): void {
|
|
|
3016
3023
|
try { component.removeChild?.(child); } catch { /* noop */ }
|
|
3017
3024
|
}
|
|
3018
3025
|
component.imageComponents = [];
|
|
3026
|
+
// Pi reuses image components by matching imageSources; clear both caches.
|
|
3027
|
+
component.imageSources = [];
|
|
3019
3028
|
component.imageSpacers = [];
|
|
3020
3029
|
}
|
|
3021
3030
|
|
|
@@ -3267,6 +3276,7 @@ function fileExistsForTool(cwd: string, filePath: string): boolean {
|
|
|
3267
3276
|
}
|
|
3268
3277
|
|
|
3269
3278
|
const WRITE_EXISTED_BEFORE = new Map<string, boolean>();
|
|
3279
|
+
const EDIT_LOCALIZED = new Map<string, Promise<LocalizedEditDiff[] | null>>();
|
|
3270
3280
|
|
|
3271
3281
|
interface RtkRewriteRecord {
|
|
3272
3282
|
original: string;
|
|
@@ -4953,12 +4963,20 @@ function maxLineNumber(lines: DiffLine[]): number {
|
|
|
4953
4963
|
return max;
|
|
4954
4964
|
}
|
|
4955
4965
|
|
|
4966
|
+
function diffLayout(): "auto" | "split" | "unified" {
|
|
4967
|
+
return getMode(readSettings().diffLayout, ["auto", "split", "unified"] as const, "auto");
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4956
4970
|
function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
|
|
4957
4971
|
if (!diff.lines.length) return false;
|
|
4958
|
-
|
|
4972
|
+
const layout = diffLayout();
|
|
4973
|
+
if (layout === "unified") return false;
|
|
4959
4974
|
const nw = Math.max(2, String(maxLineNumber(diff.lines)).length);
|
|
4960
4975
|
const half = Math.floor((tw - 1) / 2);
|
|
4961
4976
|
const gw = nw + 5;
|
|
4977
|
+
// Forced split still needs room for both code columns; below that it would overflow the row.
|
|
4978
|
+
if (layout === "split") return half - gw >= 12;
|
|
4979
|
+
if (tw < SPLIT_MIN_WIDTH) return false;
|
|
4962
4980
|
const cw = Math.max(12, half - gw);
|
|
4963
4981
|
if (cw < SPLIT_MIN_CODE_WIDTH) return false;
|
|
4964
4982
|
const vis = diff.lines.slice(0, maxRows);
|
|
@@ -5555,15 +5573,71 @@ function getFirstChangedNewLine(diff: ParsedDiff): number {
|
|
|
5555
5573
|
return 0;
|
|
5556
5574
|
}
|
|
5557
5575
|
|
|
5576
|
+
// Where an edit's diff starts in the old and the new file, plus the text it replaced if that is not oldText verbatim
|
|
5577
|
+
// (a fuzzy match). Saved with the result: once the edit has run, the file no longer holds oldText to find these again.
|
|
5578
|
+
interface SavedEditPreview {
|
|
5579
|
+
oldLine: number;
|
|
5580
|
+
newLine: number;
|
|
5581
|
+
oldText?: string;
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5558
5584
|
interface LocalizedEditDiff {
|
|
5559
5585
|
diff: ParsedDiff;
|
|
5560
5586
|
line: number;
|
|
5587
|
+
saved?: SavedEditPreview;
|
|
5561
5588
|
}
|
|
5562
5589
|
|
|
5563
|
-
|
|
5590
|
+
function restoreLocalizedEditDiffs(saved: unknown, operations: Array<{ oldText: string; newText: string }>): LocalizedEditDiff[] | null {
|
|
5591
|
+
if (!Array.isArray(saved) || saved.length !== operations.length) return null;
|
|
5592
|
+
return operations.map((edit, i) => {
|
|
5593
|
+
const { oldLine, newLine, oldText } = saved[i] as SavedEditPreview;
|
|
5594
|
+
const diff = offsetParsedDiff(parseDiff(oldText ?? normalizeToLf(edit.oldText), normalizeToLf(edit.newText)), oldLine - 1, newLine - 1);
|
|
5595
|
+
return { diff, line: getFirstChangedNewLine(diff) };
|
|
5596
|
+
});
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
// Results saved before SavedEditPreview existed: the patch Pi saves with every edit result still holds the old lines
|
|
5600
|
+
// around each change, at their old line numbers. Lines it does not hold can match nothing.
|
|
5601
|
+
function oldContentFromPatch(patch: string, operations: Array<{ oldText: string; newText: string }>): string {
|
|
5602
|
+
const known: string[] = [];
|
|
5603
|
+
const changes: number[] = [];
|
|
5604
|
+
for (const hunk of Diff.parsePatch(patch)[0]?.hunks ?? []) {
|
|
5605
|
+
let at = hunk.oldStart - 1;
|
|
5606
|
+
for (const line of hunk.lines) {
|
|
5607
|
+
if (line[0] === "-" || line[0] === "+") changes.push(at);
|
|
5608
|
+
if (line[0] === " " || line[0] === "-") known[at++] = line.slice(1);
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
// The patch keeps 4 lines around a change. An oldText reaching further supplies the missing lines itself, but only if
|
|
5612
|
+
// it fits in exactly one place touching a change: a chance fit elsewhere would put the edit on the wrong line.
|
|
5613
|
+
const fuzzy = known.map(normalizeTextForFuzzyMatch);
|
|
5614
|
+
for (const { oldText } of operations) {
|
|
5615
|
+
const own = normalizeToLf(oldText).split("\n");
|
|
5616
|
+
const want = normalizeTextForFuzzyMatch(normalizeToLf(oldText)).split("\n");
|
|
5617
|
+
const last = want.length - 1;
|
|
5618
|
+
const starts = new Set<number>();
|
|
5619
|
+
for (const change of changes) {
|
|
5620
|
+
for (let at = Math.max(0, change - want.length); at <= change; at++) starts.add(at);
|
|
5621
|
+
}
|
|
5622
|
+
const fits = [...starts].filter((at) => {
|
|
5623
|
+
let overlaps = false;
|
|
5624
|
+
return want.every((line, i) => {
|
|
5625
|
+
const have = fuzzy[at + i];
|
|
5626
|
+
if (have === undefined) return true;
|
|
5627
|
+
overlaps = true;
|
|
5628
|
+
return i === 0 ? have.endsWith(line) : i === last ? have.startsWith(line) : have === line;
|
|
5629
|
+
}) && overlaps;
|
|
5630
|
+
});
|
|
5631
|
+
// An empty first or last piece is only the line break before or after oldText, not a line of its own.
|
|
5632
|
+
if (fits.length === 1) own.forEach((line, i) => { if (line || (i > 0 && i < last)) known[fits[0] + i] ??= line; });
|
|
5633
|
+
}
|
|
5634
|
+
return `${Array.from(known, (line) => line ?? "\u0000").join("\n")}\n`;
|
|
5635
|
+
}
|
|
5636
|
+
|
|
5637
|
+
async function computeLocalizedEditDiffs(filePath: string, operations: Array<{ oldText: string; newText: string }>, cwd: string, savedPatch?: string): Promise<LocalizedEditDiff[] | null> {
|
|
5564
5638
|
if (!filePath || operations.length === 0) return null;
|
|
5565
5639
|
try {
|
|
5566
|
-
const rawContent = await readFileAsync(resolve(cwd, filePath), "utf8");
|
|
5640
|
+
const rawContent = savedPatch === undefined ? await readFileAsync(resolve(cwd, filePath), "utf8") : oldContentFromPatch(savedPatch, operations);
|
|
5567
5641
|
const normalizedContent = normalizeToLf(stripBomText(rawContent));
|
|
5568
5642
|
const normalizedOps = operations.map((edit) => ({ oldText: normalizeToLf(edit.oldText), newText: normalizeToLf(edit.newText) }));
|
|
5569
5643
|
const baseContent = normalizedOps.some((edit) => findEditMatch(normalizedContent, edit.oldText).usedFuzzyMatch)
|
|
@@ -5588,7 +5662,9 @@ async function computeLocalizedEditDiffs(filePath: string, operations: Array<{ o
|
|
|
5588
5662
|
const oldStartLine = lineNumberAtIndex(baseContent, match.matchIndex);
|
|
5589
5663
|
const newStartLine = oldStartLine + lineDelta;
|
|
5590
5664
|
const diff = offsetParsedDiff(parseDiff(oldChunk, match.newText), oldStartLine - 1, newStartLine - 1);
|
|
5591
|
-
|
|
5665
|
+
const saved: SavedEditPreview = { oldLine: oldStartLine, newLine: newStartLine };
|
|
5666
|
+
if (oldChunk !== normalizedOps[match.editIndex].oldText) saved.oldText = oldChunk;
|
|
5667
|
+
localized[match.editIndex] = { diff, line: getFirstChangedNewLine(diff), saved };
|
|
5592
5668
|
lineDelta += countLineBreaks(match.newText) - countLineBreaks(oldChunk);
|
|
5593
5669
|
}
|
|
5594
5670
|
return localized.every(Boolean) ? (localized as LocalizedEditDiff[]) : null;
|
|
@@ -7684,6 +7760,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7684
7760
|
});
|
|
7685
7761
|
|
|
7686
7762
|
const editTool = createEditTool(cwd);
|
|
7763
|
+
// The live preview and execute() share one look at the file, taken before the edit runs: what the row shows live is
|
|
7764
|
+
// then exactly what is saved with the result, and so what it shows again after a resume.
|
|
7765
|
+
const localizeEditOnce = (toolCallId: string, filePath: string, operations: Array<{ oldText: string; newText: string }>) => {
|
|
7766
|
+
let localized = EDIT_LOCALIZED.get(toolCallId);
|
|
7767
|
+
if (!localized) EDIT_LOCALIZED.set(toolCallId, localized = computeLocalizedEditDiffs(filePath, operations, cwd));
|
|
7768
|
+
return localized;
|
|
7769
|
+
};
|
|
7687
7770
|
pi.registerTool({
|
|
7688
7771
|
name: "edit",
|
|
7689
7772
|
label: "edit",
|
|
@@ -7694,11 +7777,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
7694
7777
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7695
7778
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
7696
7779
|
const operations = getEditOperations(params);
|
|
7697
|
-
const localizedDiffs =
|
|
7780
|
+
const localizedDiffs = await localizeEditOnce(toolCallId, fp, operations);
|
|
7781
|
+
EDIT_LOCALIZED.delete(toolCallId);
|
|
7698
7782
|
const result = await editTool.execute(toolCallId, params, signal, onUpdate);
|
|
7699
7783
|
if (operations.length === 0) return result;
|
|
7700
7784
|
const { diffs, summary, totalLines, totalHunks } = summarizeEditOperations(operations);
|
|
7701
7785
|
const baseDetails = (((result as any).details ?? {}) as Record<string, unknown>);
|
|
7786
|
+
// null: the edits could not be located, and the row showed (and keeps showing) the diff without file line numbers.
|
|
7787
|
+
const preview = localizedDiffs?.map((entry) => entry.saved) ?? null;
|
|
7702
7788
|
if (operations.length === 1) {
|
|
7703
7789
|
const localized = localizedDiffs?.[0];
|
|
7704
7790
|
const editLine = localized?.line ?? (typeof baseDetails.firstChangedLine === "number" ? baseDetails.firstChangedLine : 0);
|
|
@@ -7711,6 +7797,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7711
7797
|
hunks: countDiffHunks(diff),
|
|
7712
7798
|
added: diff?.added ?? 0,
|
|
7713
7799
|
removed: diff?.removed ?? 0,
|
|
7800
|
+
preview,
|
|
7714
7801
|
};
|
|
7715
7802
|
return result;
|
|
7716
7803
|
}
|
|
@@ -7723,6 +7810,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7723
7810
|
hunks: totalHunks,
|
|
7724
7811
|
totalAdded: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
7725
7812
|
totalRemoved: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
7813
|
+
preview,
|
|
7726
7814
|
};
|
|
7727
7815
|
return result;
|
|
7728
7816
|
},
|
|
@@ -7733,7 +7821,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7733
7821
|
const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
|
|
7734
7822
|
syncToolCallStatus(ctx);
|
|
7735
7823
|
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
|
|
7736
|
-
|
|
7824
|
+
// History replay never sets argsComplete; a finished edit is drawn from its result instead.
|
|
7825
|
+
const finished = ctx.state._editResult as Record<string, unknown> | undefined;
|
|
7826
|
+
if (!((ctx.argsComplete || finished) && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
7737
7827
|
const diffWidth = contextDiffWidth(ctx, 3);
|
|
7738
7828
|
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
7739
7829
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
@@ -7742,7 +7832,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7742
7832
|
ctx.state._ptBody = theme.fg("muted", "(rendering…)");
|
|
7743
7833
|
ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
|
|
7744
7834
|
const lg = lang(fp);
|
|
7745
|
-
|
|
7835
|
+
// Located once per row: expanding or resizing must not look in the file again, it no longer holds oldText.
|
|
7836
|
+
ctx.state._editLocalized ??= !finished
|
|
7837
|
+
? localizeEditOnce(ctx.toolCallId, fp, operations)
|
|
7838
|
+
: "preview" in finished
|
|
7839
|
+
? Promise.resolve(restoreLocalizedEditDiffs(finished.preview, operations))
|
|
7840
|
+
: computeLocalizedEditDiffs(fp, operations, cwd, typeof finished.patch === "string" ? finished.patch : "");
|
|
7841
|
+
void (ctx.state._editLocalized as Promise<LocalizedEditDiff[] | null>)
|
|
7746
7842
|
.then((localizedDiffs) => {
|
|
7747
7843
|
if (ctx.state._pk !== key) return;
|
|
7748
7844
|
const diffs = localizedDiffs?.map((entry) => entry.diff) ?? fallbackDiffs;
|
|
@@ -7911,6 +8007,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7911
8007
|
clearAllBashDurationContexts();
|
|
7912
8008
|
clearRtkRewriteState();
|
|
7913
8009
|
WRITE_EXISTED_BEFORE.clear();
|
|
8010
|
+
EDIT_LOCALIZED.clear();
|
|
7914
8011
|
clearHighlightCache();
|
|
7915
8012
|
invalidateThemePaletteCache();
|
|
7916
8013
|
bumpToolBranchVisualEpoch();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-code-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.88",
|
|
4
4
|
"description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|