pi-claude-code-ui 1.0.87 → 1.0.89
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 +23 -0
- package/README.md +2 -0
- package/extensions/index.ts +234 -19
- package/extensions/spinner.ts +4 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,29 @@
|
|
|
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.89 — 2026-10-19
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- **Direct MCP tools show what they did** - `mcp__<server>__<tool>` rows showed only `MCP status` until expanded. They now show `server:tool` and the main argument (for example `MCP subagents:Agent Debug NaN`), and grouped headers list the tool names (`Agent, Ctx Reduce`) instead of `Mcp Subagents Agent`.
|
|
11
|
+
- **Running tool rows no longer draw their header twice** - the first pending blink invalidated the row while Pi was still building it, which left the call line in the row twice until the next redraw (until the result arrived on expanded rows). Affected every tool, not only MCP.
|
|
12
|
+
- **Theme switches repaint everything** - after `/theme` or a light/dark switch, diff previews kept the old diff palette, rows inside tool groups kept the old colors, and the spinner verb/status kept the old theme until restart. All three now follow the new theme.
|
|
13
|
+
- **MCP output uses the same dim color as other tools** - MCP running previews and expanded output were drawn in `toolOutput`, brighter than read, bash, grep and other tools.
|
|
14
|
+
|
|
15
|
+
### Performance
|
|
16
|
+
|
|
17
|
+
- **Each diff preview renders once** - since 1.0.80 edit, write and apply_patch rows started their Shiki render at a guessed width and rendered again once the real row width was known. On resume every diff row paid twice. Rows now wait for their width: half the diff renders, and the first frame of a 60-row tool history dropped from about 37 ms to 8 ms in the benchmark.
|
|
18
|
+
|
|
19
|
+
## 1.0.88 — 2026-10-19
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **`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.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **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.
|
|
28
|
+
|
|
6
29
|
## 1.0.87 — 2026-10-19
|
|
7
30
|
|
|
8
31
|
### 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
|
@@ -89,6 +89,8 @@ const PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-container-render");
|
|
|
89
89
|
const TOOL_RENDER_CACHE = Symbol.for("pi-claude-style-tools:tool-render-cache");
|
|
90
90
|
const COMPONENT_PARENT = Symbol.for("pi-claude-style-tools:component-parent");
|
|
91
91
|
const PARENT_TRACKING_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-parent-tracking");
|
|
92
|
+
const TOOL_UPDATE_DISPLAY_ACTIVE = Symbol("pi-claude-style-tools:update-display-active");
|
|
93
|
+
const TOOL_UPDATE_DISPLAY_RERUN = Symbol("pi-claude-style-tools:update-display-rerun");
|
|
92
94
|
const TOOL_CACHE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-cache-invalidation");
|
|
93
95
|
const TOOL_MOUSE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-fullscreen-mouse");
|
|
94
96
|
const FULLSCREEN_HOVER_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-fullscreen-hover");
|
|
@@ -124,6 +126,8 @@ interface SettingsFile {
|
|
|
124
126
|
liveToolPreviewLines?: number;
|
|
125
127
|
showTruncationHints?: boolean;
|
|
126
128
|
diffCollapsedLines?: number;
|
|
129
|
+
/** Diff layout: "auto" (default) picks split only when it fits cleanly; "split" and "unified" force one layout. */
|
|
130
|
+
diffLayout?: "auto" | "split" | "unified";
|
|
127
131
|
diffTheme?: string;
|
|
128
132
|
diffColors?: Record<string, string>;
|
|
129
133
|
/**
|
|
@@ -613,7 +617,7 @@ function getGroupedToolName(tools: any[]): string | undefined {
|
|
|
613
617
|
|
|
614
618
|
function getToolGroupLabel(tools: any[]): string {
|
|
615
619
|
const sameName = getGroupedToolName(tools);
|
|
616
|
-
return sameName ?
|
|
620
|
+
return sameName ? toolDisplayName(sameName) : "Multiple Tools";
|
|
617
621
|
}
|
|
618
622
|
|
|
619
623
|
function getToolGroupOverallStatus(tools: any[]): ToolStatus {
|
|
@@ -681,7 +685,7 @@ function formatToolNameList(tools: any[]): string {
|
|
|
681
685
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
682
686
|
}
|
|
683
687
|
return [...counts.entries()]
|
|
684
|
-
.map(([name, count]) => `${
|
|
688
|
+
.map(([name, count]) => `${toolDisplayName(name)}${count > 1 ? `×${count}` : ""}`)
|
|
685
689
|
.join(", ");
|
|
686
690
|
}
|
|
687
691
|
|
|
@@ -761,6 +765,7 @@ function getToolArgSummary(tool: any): string {
|
|
|
761
765
|
if (name === "grep") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
|
|
762
766
|
if (name === "find") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
|
|
763
767
|
if (name === "ls") return shortPath(process.cwd(), args.path ?? ".");
|
|
768
|
+
if (parseNamespacedMcpToolName(name)) return summarizeDirectMcpArgs(args);
|
|
764
769
|
return summarizeText(getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt") || name, 72);
|
|
765
770
|
}
|
|
766
771
|
|
|
@@ -771,7 +776,7 @@ function getToolCallLine(tool: any): string {
|
|
|
771
776
|
return line.replaceAll(WRAP_MARK, "").replaceAll(CLIP_MARK, "");
|
|
772
777
|
}
|
|
773
778
|
const summary = getToolArgSummary(tool);
|
|
774
|
-
const label =
|
|
779
|
+
const label = toolDisplayName(getToolName(tool));
|
|
775
780
|
return `${label}${summary ? ` ${summary}` : ""}`;
|
|
776
781
|
}
|
|
777
782
|
|
|
@@ -915,6 +920,7 @@ class ToolGroupComponent extends Container {
|
|
|
915
920
|
private cachedStatusKey?: string;
|
|
916
921
|
private cachedToolCount?: number;
|
|
917
922
|
private cachedLines?: string[];
|
|
923
|
+
private paletteFingerprint?: string;
|
|
918
924
|
|
|
919
925
|
private clearRenderCache(): void {
|
|
920
926
|
this.dirty = true;
|
|
@@ -984,6 +990,15 @@ class ToolGroupComponent extends Container {
|
|
|
984
990
|
// only; children keep their own ToolText/TOOL_RENDER_CACHE entries and
|
|
985
991
|
// recompute only when their content actually changes.
|
|
986
992
|
this.clearRenderCache();
|
|
993
|
+
// Exception: a theme switch (Pi calls ui.invalidate()) changes colors the
|
|
994
|
+
// child renderers baked into their text, so those must re-run once.
|
|
995
|
+
// (Child invalidation re-enters here via clearToolRenderCache; the palette is
|
|
996
|
+
// recorded first so that re-entry is a no-op.)
|
|
997
|
+
const palette = currentThemePaletteFingerprint();
|
|
998
|
+
if (palette === undefined || palette === this.paletteFingerprint) return;
|
|
999
|
+
const changed = this.paletteFingerprint !== undefined;
|
|
1000
|
+
this.paletteFingerprint = palette;
|
|
1001
|
+
if (changed) for (const tool of this.tools) tool?.invalidate?.();
|
|
987
1002
|
}
|
|
988
1003
|
|
|
989
1004
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
@@ -1009,6 +1024,8 @@ class ToolGroupComponent extends Container {
|
|
|
1009
1024
|
}
|
|
1010
1025
|
const safeWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
1011
1026
|
// Fast path: settled groups with a valid memo skip ALL child walks.
|
|
1027
|
+
// Remember the palette this group was drawn with (see invalidate()).
|
|
1028
|
+
if (this.paletteFingerprint === undefined) this.paletteFingerprint = currentThemePaletteFingerprint();
|
|
1012
1029
|
// Child mutations mark dirty via clearToolRenderCache → invalidate().
|
|
1013
1030
|
if (
|
|
1014
1031
|
!this.dirty
|
|
@@ -2965,6 +2982,11 @@ function syncLiveToolRenderState(component: any): void {
|
|
|
2965
2982
|
state._liveLineCount = collectNonEmptyLines(raw, 0).total;
|
|
2966
2983
|
} else if (component?.isPartial !== true) {
|
|
2967
2984
|
delete state._liveLineCount;
|
|
2985
|
+
// renderCall never sees the result, so hand a finished edit's details over: its diff is redrawn from them, not from
|
|
2986
|
+
// the file. A call aborted while streaming ends with a bare error instead of a toolResult; it never drew a diff.
|
|
2987
|
+
if (component?.toolName === "edit" && component.result && (!ctxLike.isError || component.result.role === "toolResult")) {
|
|
2988
|
+
state._editResult = component.result.details ?? {};
|
|
2989
|
+
}
|
|
2968
2990
|
}
|
|
2969
2991
|
}
|
|
2970
2992
|
|
|
@@ -2987,7 +3009,7 @@ function patchToolRenderCacheInvalidation(): void {
|
|
|
2987
3009
|
for (const method of methods) {
|
|
2988
3010
|
const original = proto[method];
|
|
2989
3011
|
if (typeof original !== "function") continue;
|
|
2990
|
-
|
|
3012
|
+
const run = function patchedToolMutation(this: any, ...args: any[]) {
|
|
2991
3013
|
clearToolRenderCache(this);
|
|
2992
3014
|
if (method === "updateDisplay" || method === "updateResult" || method === "invalidate") {
|
|
2993
3015
|
syncLiveToolRenderState(this);
|
|
@@ -2996,6 +3018,28 @@ function patchToolRenderCacheInvalidation(): void {
|
|
|
2996
3018
|
clearToolRenderCache(this);
|
|
2997
3019
|
return result;
|
|
2998
3020
|
};
|
|
3021
|
+
proto[method] = method !== "updateDisplay" ? run : function patchedToolUpdateDisplay(this: any, ...args: any[]) {
|
|
3022
|
+
// A renderer can invalidate its own row while updateDisplay is building it
|
|
3023
|
+
// (the first pending blink registration does). Pi's updateDisplay clears the
|
|
3024
|
+
// content box and re-adds regions, so a nested pass leaves the call/result
|
|
3025
|
+
// regions twice in the box until the next redraw. Defer it to one rerun.
|
|
3026
|
+
if (this[TOOL_UPDATE_DISPLAY_ACTIVE]) {
|
|
3027
|
+
this[TOOL_UPDATE_DISPLAY_RERUN] = true;
|
|
3028
|
+
return undefined;
|
|
3029
|
+
}
|
|
3030
|
+
this[TOOL_UPDATE_DISPLAY_ACTIVE] = true;
|
|
3031
|
+
try {
|
|
3032
|
+
let result = run.apply(this, args);
|
|
3033
|
+
if (this[TOOL_UPDATE_DISPLAY_RERUN]) {
|
|
3034
|
+
this[TOOL_UPDATE_DISPLAY_RERUN] = false;
|
|
3035
|
+
result = run.apply(this, args);
|
|
3036
|
+
}
|
|
3037
|
+
return result;
|
|
3038
|
+
} finally {
|
|
3039
|
+
this[TOOL_UPDATE_DISPLAY_ACTIVE] = false;
|
|
3040
|
+
this[TOOL_UPDATE_DISPLAY_RERUN] = false;
|
|
3041
|
+
}
|
|
3042
|
+
};
|
|
2999
3043
|
}
|
|
3000
3044
|
|
|
3001
3045
|
proto[TOOL_CACHE_PATCH_FLAG] = true;
|
|
@@ -3269,6 +3313,7 @@ function fileExistsForTool(cwd: string, filePath: string): boolean {
|
|
|
3269
3313
|
}
|
|
3270
3314
|
|
|
3271
3315
|
const WRITE_EXISTED_BEFORE = new Map<string, boolean>();
|
|
3316
|
+
const EDIT_LOCALIZED = new Map<string, Promise<LocalizedEditDiff[] | null>>();
|
|
3272
3317
|
|
|
3273
3318
|
interface RtkRewriteRecord {
|
|
3274
3319
|
original: string;
|
|
@@ -3780,6 +3825,9 @@ class ToolText extends Text {
|
|
|
3780
3825
|
private toolCachedLines?: string[];
|
|
3781
3826
|
private observedWidth?: number;
|
|
3782
3827
|
private pendingObservedWidth?: number;
|
|
3828
|
+
getObservedWidth(): number | undefined {
|
|
3829
|
+
return this.observedWidth;
|
|
3830
|
+
}
|
|
3783
3831
|
private widthObserver?: (width: number) => void;
|
|
3784
3832
|
private widthObserverScheduled = false;
|
|
3785
3833
|
|
|
@@ -4145,6 +4193,13 @@ function themeBgRgb(theme: any, key: string): Rgb | null {
|
|
|
4145
4193
|
let _themePaletteCacheTheme: unknown = null;
|
|
4146
4194
|
let _themePaletteCacheName: string | null = null;
|
|
4147
4195
|
let _themePaletteCacheFingerprint: string | null = null;
|
|
4196
|
+
// Pi passes the same global theme Proxy everywhere, so keeping the last one lets
|
|
4197
|
+
// non-render code (group invalidation) read the current palette.
|
|
4198
|
+
let _lastSeenTheme: unknown = null;
|
|
4199
|
+
|
|
4200
|
+
function currentThemePaletteFingerprint(): string | undefined {
|
|
4201
|
+
return _lastSeenTheme ? themePaletteFingerprint(_lastSeenTheme) : undefined;
|
|
4202
|
+
}
|
|
4148
4203
|
|
|
4149
4204
|
/** Resolved-color fingerprint so palette re-derives when the active theme file changes under the same name/object. */
|
|
4150
4205
|
function themePaletteFingerprint(theme: any): string {
|
|
@@ -4559,6 +4614,7 @@ function resetThemePalette(): void {
|
|
|
4559
4614
|
|
|
4560
4615
|
function applyThemePaletteIfNeeded(theme: any): void {
|
|
4561
4616
|
if (!theme) return;
|
|
4617
|
+
_lastSeenTheme = theme;
|
|
4562
4618
|
if (!themeAdaptiveEnabled()) {
|
|
4563
4619
|
applyToolBranchColor(theme);
|
|
4564
4620
|
syncOutlineChromeFromBranch(theme);
|
|
@@ -4749,10 +4805,26 @@ function branchDiffWidth(componentWidth?: number, chromeWidth = 2): number {
|
|
|
4749
4805
|
return Math.max(20, Math.min(width - chromeWidth, MAX_TERM_WIDTH));
|
|
4750
4806
|
}
|
|
4751
4807
|
|
|
4808
|
+
// Diff preview cache keys also carry _toolBranchVisualEpoch, which applyThemePaletteIfNeeded
|
|
4809
|
+
// bumps when the theme palette changes: otherwise rows rendered before a /theme or light/dark
|
|
4810
|
+
// switch kept the old diff backgrounds and stat colors while new rows used the new ones.
|
|
4752
4811
|
function contextDiffWidth(ctx: any, chromeWidth = 2): number {
|
|
4753
4812
|
return branchDiffWidth(ctx.state?._diffComponentWidth, chromeWidth);
|
|
4754
4813
|
}
|
|
4755
4814
|
|
|
4815
|
+
// Diff previews are laid out for the row's real width, which is only known after the
|
|
4816
|
+
// row's text component has rendered once. Starting the async Shiki render before that
|
|
4817
|
+
// used the termW() guess and rendered every diff a second time once the real width
|
|
4818
|
+
// arrived (on resume: every edit/write row, twice). Wait for the width instead; the
|
|
4819
|
+
// width observer invalidates the row as soon as it is known.
|
|
4820
|
+
function diffWidthReady(ctx: any): boolean {
|
|
4821
|
+
if (typeof ctx.state?._diffComponentWidth === "number") return true;
|
|
4822
|
+
const seen = ctx.lastComponent instanceof ToolText ? ctx.lastComponent.getObservedWidth() : undefined;
|
|
4823
|
+
if (seen === undefined || !ctx.state) return false;
|
|
4824
|
+
ctx.state._diffComponentWidth = seen;
|
|
4825
|
+
return true;
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4756
4828
|
function adaptiveWrapRows(tw?: number): number {
|
|
4757
4829
|
const width = tw ?? termW();
|
|
4758
4830
|
if (width >= 180) return MAX_WRAP_ROWS_WIDE;
|
|
@@ -4955,12 +5027,20 @@ function maxLineNumber(lines: DiffLine[]): number {
|
|
|
4955
5027
|
return max;
|
|
4956
5028
|
}
|
|
4957
5029
|
|
|
5030
|
+
function diffLayout(): "auto" | "split" | "unified" {
|
|
5031
|
+
return getMode(readSettings().diffLayout, ["auto", "split", "unified"] as const, "auto");
|
|
5032
|
+
}
|
|
5033
|
+
|
|
4958
5034
|
function shouldUseSplit(diff: ParsedDiff, tw: number, maxRows = MAX_PREVIEW_LINES): boolean {
|
|
4959
5035
|
if (!diff.lines.length) return false;
|
|
4960
|
-
|
|
5036
|
+
const layout = diffLayout();
|
|
5037
|
+
if (layout === "unified") return false;
|
|
4961
5038
|
const nw = Math.max(2, String(maxLineNumber(diff.lines)).length);
|
|
4962
5039
|
const half = Math.floor((tw - 1) / 2);
|
|
4963
5040
|
const gw = nw + 5;
|
|
5041
|
+
// Forced split still needs room for both code columns; below that it would overflow the row.
|
|
5042
|
+
if (layout === "split") return half - gw >= 12;
|
|
5043
|
+
if (tw < SPLIT_MIN_WIDTH) return false;
|
|
4964
5044
|
const cw = Math.max(12, half - gw);
|
|
4965
5045
|
if (cw < SPLIT_MIN_CODE_WIDTH) return false;
|
|
4966
5046
|
const vis = diff.lines.slice(0, maxRows);
|
|
@@ -5557,15 +5637,71 @@ function getFirstChangedNewLine(diff: ParsedDiff): number {
|
|
|
5557
5637
|
return 0;
|
|
5558
5638
|
}
|
|
5559
5639
|
|
|
5640
|
+
// Where an edit's diff starts in the old and the new file, plus the text it replaced if that is not oldText verbatim
|
|
5641
|
+
// (a fuzzy match). Saved with the result: once the edit has run, the file no longer holds oldText to find these again.
|
|
5642
|
+
interface SavedEditPreview {
|
|
5643
|
+
oldLine: number;
|
|
5644
|
+
newLine: number;
|
|
5645
|
+
oldText?: string;
|
|
5646
|
+
}
|
|
5647
|
+
|
|
5560
5648
|
interface LocalizedEditDiff {
|
|
5561
5649
|
diff: ParsedDiff;
|
|
5562
5650
|
line: number;
|
|
5651
|
+
saved?: SavedEditPreview;
|
|
5563
5652
|
}
|
|
5564
5653
|
|
|
5565
|
-
|
|
5654
|
+
function restoreLocalizedEditDiffs(saved: unknown, operations: Array<{ oldText: string; newText: string }>): LocalizedEditDiff[] | null {
|
|
5655
|
+
if (!Array.isArray(saved) || saved.length !== operations.length) return null;
|
|
5656
|
+
return operations.map((edit, i) => {
|
|
5657
|
+
const { oldLine, newLine, oldText } = saved[i] as SavedEditPreview;
|
|
5658
|
+
const diff = offsetParsedDiff(parseDiff(oldText ?? normalizeToLf(edit.oldText), normalizeToLf(edit.newText)), oldLine - 1, newLine - 1);
|
|
5659
|
+
return { diff, line: getFirstChangedNewLine(diff) };
|
|
5660
|
+
});
|
|
5661
|
+
}
|
|
5662
|
+
|
|
5663
|
+
// Results saved before SavedEditPreview existed: the patch Pi saves with every edit result still holds the old lines
|
|
5664
|
+
// around each change, at their old line numbers. Lines it does not hold can match nothing.
|
|
5665
|
+
function oldContentFromPatch(patch: string, operations: Array<{ oldText: string; newText: string }>): string {
|
|
5666
|
+
const known: string[] = [];
|
|
5667
|
+
const changes: number[] = [];
|
|
5668
|
+
for (const hunk of Diff.parsePatch(patch)[0]?.hunks ?? []) {
|
|
5669
|
+
let at = hunk.oldStart - 1;
|
|
5670
|
+
for (const line of hunk.lines) {
|
|
5671
|
+
if (line[0] === "-" || line[0] === "+") changes.push(at);
|
|
5672
|
+
if (line[0] === " " || line[0] === "-") known[at++] = line.slice(1);
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
// The patch keeps 4 lines around a change. An oldText reaching further supplies the missing lines itself, but only if
|
|
5676
|
+
// it fits in exactly one place touching a change: a chance fit elsewhere would put the edit on the wrong line.
|
|
5677
|
+
const fuzzy = known.map(normalizeTextForFuzzyMatch);
|
|
5678
|
+
for (const { oldText } of operations) {
|
|
5679
|
+
const own = normalizeToLf(oldText).split("\n");
|
|
5680
|
+
const want = normalizeTextForFuzzyMatch(normalizeToLf(oldText)).split("\n");
|
|
5681
|
+
const last = want.length - 1;
|
|
5682
|
+
const starts = new Set<number>();
|
|
5683
|
+
for (const change of changes) {
|
|
5684
|
+
for (let at = Math.max(0, change - want.length); at <= change; at++) starts.add(at);
|
|
5685
|
+
}
|
|
5686
|
+
const fits = [...starts].filter((at) => {
|
|
5687
|
+
let overlaps = false;
|
|
5688
|
+
return want.every((line, i) => {
|
|
5689
|
+
const have = fuzzy[at + i];
|
|
5690
|
+
if (have === undefined) return true;
|
|
5691
|
+
overlaps = true;
|
|
5692
|
+
return i === 0 ? have.endsWith(line) : i === last ? have.startsWith(line) : have === line;
|
|
5693
|
+
}) && overlaps;
|
|
5694
|
+
});
|
|
5695
|
+
// An empty first or last piece is only the line break before or after oldText, not a line of its own.
|
|
5696
|
+
if (fits.length === 1) own.forEach((line, i) => { if (line || (i > 0 && i < last)) known[fits[0] + i] ??= line; });
|
|
5697
|
+
}
|
|
5698
|
+
return `${Array.from(known, (line) => line ?? "\u0000").join("\n")}\n`;
|
|
5699
|
+
}
|
|
5700
|
+
|
|
5701
|
+
async function computeLocalizedEditDiffs(filePath: string, operations: Array<{ oldText: string; newText: string }>, cwd: string, savedPatch?: string): Promise<LocalizedEditDiff[] | null> {
|
|
5566
5702
|
if (!filePath || operations.length === 0) return null;
|
|
5567
5703
|
try {
|
|
5568
|
-
const rawContent = await readFileAsync(resolve(cwd, filePath), "utf8");
|
|
5704
|
+
const rawContent = savedPatch === undefined ? await readFileAsync(resolve(cwd, filePath), "utf8") : oldContentFromPatch(savedPatch, operations);
|
|
5569
5705
|
const normalizedContent = normalizeToLf(stripBomText(rawContent));
|
|
5570
5706
|
const normalizedOps = operations.map((edit) => ({ oldText: normalizeToLf(edit.oldText), newText: normalizeToLf(edit.newText) }));
|
|
5571
5707
|
const baseContent = normalizedOps.some((edit) => findEditMatch(normalizedContent, edit.oldText).usedFuzzyMatch)
|
|
@@ -5590,7 +5726,9 @@ async function computeLocalizedEditDiffs(filePath: string, operations: Array<{ o
|
|
|
5590
5726
|
const oldStartLine = lineNumberAtIndex(baseContent, match.matchIndex);
|
|
5591
5727
|
const newStartLine = oldStartLine + lineDelta;
|
|
5592
5728
|
const diff = offsetParsedDiff(parseDiff(oldChunk, match.newText), oldStartLine - 1, newStartLine - 1);
|
|
5593
|
-
|
|
5729
|
+
const saved: SavedEditPreview = { oldLine: oldStartLine, newLine: newStartLine };
|
|
5730
|
+
if (oldChunk !== normalizedOps[match.editIndex].oldText) saved.oldText = oldChunk;
|
|
5731
|
+
localized[match.editIndex] = { diff, line: getFirstChangedNewLine(diff), saved };
|
|
5594
5732
|
lineDelta += countLineBreaks(match.newText) - countLineBreaks(oldChunk);
|
|
5595
5733
|
}
|
|
5596
5734
|
return localized.every(Boolean) ? (localized as LocalizedEditDiff[]) : null;
|
|
@@ -5939,6 +6077,18 @@ function isMcpToolName(name: string): boolean {
|
|
|
5939
6077
|
return name === "mcp" || /^mcp[_:-]/i.test(name) || /[_:-]mcp[_:-]/i.test(name);
|
|
5940
6078
|
}
|
|
5941
6079
|
|
|
6080
|
+
// Direct MCP tools are registered as `mcp__<server>__<tool>`; their args are the
|
|
6081
|
+
// tool's own args, not the gateway's `{ tool, server, ... }`.
|
|
6082
|
+
function parseNamespacedMcpToolName(name: string): { server: string; tool: string } | undefined {
|
|
6083
|
+
const match = /^mcp__(.+?)__(.+)$/.exec(name);
|
|
6084
|
+
return match ? { server: match[1], tool: match[2] } : undefined;
|
|
6085
|
+
}
|
|
6086
|
+
|
|
6087
|
+
function toolDisplayName(name: string): string {
|
|
6088
|
+
const direct = parseNamespacedMcpToolName(name);
|
|
6089
|
+
return humanizeToolName(direct ? direct.tool : name);
|
|
6090
|
+
}
|
|
6091
|
+
|
|
5942
6092
|
function shouldUseGenericToolRenderer(name: unknown): boolean {
|
|
5943
6093
|
return typeof name === "string" && name.length > 0 && !CORE_TOOL_OVERRIDES.has(name);
|
|
5944
6094
|
}
|
|
@@ -6445,8 +6595,11 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
6445
6595
|
}
|
|
6446
6596
|
ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);
|
|
6447
6597
|
|
|
6598
|
+
if (!diffWidthReady(ctx)) {
|
|
6599
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, `${hdr}\n${withBranch(theme.fg("muted", "(rendering…)"), theme, false, true)}`);
|
|
6600
|
+
}
|
|
6448
6601
|
const diffWidth = contextDiffWidth(ctx);
|
|
6449
|
-
const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
6602
|
+
const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
6450
6603
|
if (ctx.state._applyPatchPreviewKey !== key) {
|
|
6451
6604
|
ctx.state._applyPatchPreviewKey = key;
|
|
6452
6605
|
ctx.state._applyPatchPreviewBody = theme.fg("muted", "(rendering…)");
|
|
@@ -6529,7 +6682,37 @@ function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, c
|
|
|
6529
6682
|
return makeText(ctx.lastComponent, withBranch(`${theme.fg("success", "Applied")} ${meta.changeCount} files ${summary}${meta.totalLines ? ` ${theme.fg("muted", `(${meta.totalLines} diff lines)`)}` : ""}`, theme));
|
|
6530
6683
|
}
|
|
6531
6684
|
|
|
6532
|
-
|
|
6685
|
+
const DIRECT_MCP_SUMMARY_KEYS = [
|
|
6686
|
+
"description", "subject", "title", "query", "path", "file_path", "url", "command", "name",
|
|
6687
|
+
"agent_id", "agentId", "taskId", "task_id", "threadId", "drop", "target", "message", "prompt", "task",
|
|
6688
|
+
] as const;
|
|
6689
|
+
|
|
6690
|
+
function summarizeArgValue(value: unknown): string {
|
|
6691
|
+
if (typeof value === "string") return value.trim();
|
|
6692
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
6693
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string" || typeof item === "number")) return value.join(", ");
|
|
6694
|
+
return "";
|
|
6695
|
+
}
|
|
6696
|
+
|
|
6697
|
+
function summarizeDirectMcpArgs(args: any): string {
|
|
6698
|
+
if (!args || typeof args !== "object") return "";
|
|
6699
|
+
for (const key of DIRECT_MCP_SUMMARY_KEYS) {
|
|
6700
|
+
const value = summarizeArgValue(args[key]);
|
|
6701
|
+
if (value) return summarizeText(value, 72);
|
|
6702
|
+
}
|
|
6703
|
+
for (const [key, raw] of Object.entries(args)) {
|
|
6704
|
+
const value = summarizeArgValue(raw);
|
|
6705
|
+
if (value) return summarizeText(`${key}: ${value}`, 72);
|
|
6706
|
+
}
|
|
6707
|
+
return "";
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6710
|
+
function summarizeMcpToolCall(name: string, args: any, theme: Theme): string {
|
|
6711
|
+
const direct = parseNamespacedMcpToolName(name);
|
|
6712
|
+
if (direct) {
|
|
6713
|
+
const detail = summarizeDirectMcpArgs(args);
|
|
6714
|
+
return `${direct.server}:${direct.tool}${detail ? ` ${theme.fg("muted", detail)}` : ""}`;
|
|
6715
|
+
}
|
|
6533
6716
|
const tool = getStringArg(args, "tool");
|
|
6534
6717
|
if (tool) return args?.server ? `${args.server}:${tool}` : tool;
|
|
6535
6718
|
const connect = getStringArg(args, "connect");
|
|
@@ -6540,14 +6723,14 @@ function summarizeMcpToolCall(args: any, theme: Theme): string {
|
|
|
6540
6723
|
}
|
|
6541
6724
|
|
|
6542
6725
|
function summarizeGenericToolCall(name: string, args: any, theme: Theme, sp: (path: string) => string): string {
|
|
6543
|
-
if (isMcpToolName(name)) return summarizeMcpToolCall(args, theme);
|
|
6726
|
+
if (isMcpToolName(name)) return summarizeMcpToolCall(name, args, theme);
|
|
6544
6727
|
return summarizeOpenAiToolCall(name, args, theme, sp);
|
|
6545
6728
|
}
|
|
6546
6729
|
|
|
6547
6730
|
function renderMcpToolResult(result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text {
|
|
6548
6731
|
if (isPartial) {
|
|
6549
6732
|
return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "MCP running..."), expanded, theme, ctx, {
|
|
6550
|
-
styleLine: (line) => theme.fg("
|
|
6733
|
+
styleLine: (line) => theme.fg("dim", line || " "),
|
|
6551
6734
|
}));
|
|
6552
6735
|
}
|
|
6553
6736
|
clearBlinkTimer(ctx);
|
|
@@ -6571,7 +6754,7 @@ function renderMcpToolResult(result: any, expanded: boolean, isPartial: boolean,
|
|
|
6571
6754
|
theme,
|
|
6572
6755
|
previewLimit(),
|
|
6573
6756
|
lines.length,
|
|
6574
|
-
(line) => theme.fg(ctx.isError ? "error" : "
|
|
6757
|
+
(line) => theme.fg(ctx.isError ? "error" : "dim", line || " "),
|
|
6575
6758
|
);
|
|
6576
6759
|
return makeText(ctx.lastComponent, withBranch(`${statusText}\n${preview}`, theme));
|
|
6577
6760
|
}
|
|
@@ -7631,10 +7814,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
7631
7814
|
if (d?._type === "diff") {
|
|
7632
7815
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
7633
7816
|
const hunks = d.diff?.lines?.filter((l: any) => l.type === "sep").length + (d.diff?.lines?.length ? 1 : 0);
|
|
7817
|
+
if (!diffWidthReady(ctx)) {
|
|
7818
|
+
const pendingSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, "");
|
|
7819
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, withFinalBranchBlock(`${pendingSummary}\n${theme.fg("muted", "rendering diff…")}`, theme));
|
|
7820
|
+
}
|
|
7634
7821
|
const diffWidth = contextDiffWidth(ctx);
|
|
7635
7822
|
const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
|
|
7636
7823
|
const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
|
|
7637
|
-
const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}`;
|
|
7824
|
+
const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
7638
7825
|
if (ctx.state._wdk !== key) {
|
|
7639
7826
|
ctx.state._wdk = key;
|
|
7640
7827
|
ctx.state._wdt = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
|
|
@@ -7661,8 +7848,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
7661
7848
|
const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
|
|
7662
7849
|
const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
|
|
7663
7850
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
7851
|
+
if (!diffWidthReady(ctx)) {
|
|
7852
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme));
|
|
7853
|
+
}
|
|
7664
7854
|
const diffWidth = contextDiffWidth(ctx);
|
|
7665
|
-
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
7855
|
+
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
7666
7856
|
if (ctx.state._nfk !== pk) {
|
|
7667
7857
|
ctx.state._nfk = pk;
|
|
7668
7858
|
ctx.state._nft = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
|
|
@@ -7686,6 +7876,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
7686
7876
|
});
|
|
7687
7877
|
|
|
7688
7878
|
const editTool = createEditTool(cwd);
|
|
7879
|
+
// The live preview and execute() share one look at the file, taken before the edit runs: what the row shows live is
|
|
7880
|
+
// then exactly what is saved with the result, and so what it shows again after a resume.
|
|
7881
|
+
const localizeEditOnce = (toolCallId: string, filePath: string, operations: Array<{ oldText: string; newText: string }>) => {
|
|
7882
|
+
let localized = EDIT_LOCALIZED.get(toolCallId);
|
|
7883
|
+
if (!localized) EDIT_LOCALIZED.set(toolCallId, localized = computeLocalizedEditDiffs(filePath, operations, cwd));
|
|
7884
|
+
return localized;
|
|
7885
|
+
};
|
|
7689
7886
|
pi.registerTool({
|
|
7690
7887
|
name: "edit",
|
|
7691
7888
|
label: "edit",
|
|
@@ -7696,11 +7893,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
7696
7893
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7697
7894
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
7698
7895
|
const operations = getEditOperations(params);
|
|
7699
|
-
const localizedDiffs =
|
|
7896
|
+
const localizedDiffs = await localizeEditOnce(toolCallId, fp, operations);
|
|
7897
|
+
EDIT_LOCALIZED.delete(toolCallId);
|
|
7700
7898
|
const result = await editTool.execute(toolCallId, params, signal, onUpdate);
|
|
7701
7899
|
if (operations.length === 0) return result;
|
|
7702
7900
|
const { diffs, summary, totalLines, totalHunks } = summarizeEditOperations(operations);
|
|
7703
7901
|
const baseDetails = (((result as any).details ?? {}) as Record<string, unknown>);
|
|
7902
|
+
// null: the edits could not be located, and the row showed (and keeps showing) the diff without file line numbers.
|
|
7903
|
+
const preview = localizedDiffs?.map((entry) => entry.saved) ?? null;
|
|
7704
7904
|
if (operations.length === 1) {
|
|
7705
7905
|
const localized = localizedDiffs?.[0];
|
|
7706
7906
|
const editLine = localized?.line ?? (typeof baseDetails.firstChangedLine === "number" ? baseDetails.firstChangedLine : 0);
|
|
@@ -7713,6 +7913,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7713
7913
|
hunks: countDiffHunks(diff),
|
|
7714
7914
|
added: diff?.added ?? 0,
|
|
7715
7915
|
removed: diff?.removed ?? 0,
|
|
7916
|
+
preview,
|
|
7716
7917
|
};
|
|
7717
7918
|
return result;
|
|
7718
7919
|
}
|
|
@@ -7725,6 +7926,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7725
7926
|
hunks: totalHunks,
|
|
7726
7927
|
totalAdded: diffs.reduce((sum, diff) => sum + diff.added, 0),
|
|
7727
7928
|
totalRemoved: diffs.reduce((sum, diff) => sum + diff.removed, 0),
|
|
7929
|
+
preview,
|
|
7728
7930
|
};
|
|
7729
7931
|
return result;
|
|
7730
7932
|
},
|
|
@@ -7735,16 +7937,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
7735
7937
|
const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
|
|
7736
7938
|
syncToolCallStatus(ctx);
|
|
7737
7939
|
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
|
|
7738
|
-
|
|
7940
|
+
// History replay never sets argsComplete; a finished edit is drawn from its result instead.
|
|
7941
|
+
const finished = ctx.state._editResult as Record<string, unknown> | undefined;
|
|
7942
|
+
if (!((ctx.argsComplete || finished) && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
7943
|
+
if (!diffWidthReady(ctx)) {
|
|
7944
|
+
const pending = liveBranchDisplay(ctx.state, theme) ?? indentBranchBlock(withBranch(theme.fg("muted", "(rendering…)"), theme, false, true));
|
|
7945
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, `${hdr}\n${pending}`);
|
|
7946
|
+
}
|
|
7739
7947
|
const diffWidth = contextDiffWidth(ctx, 3);
|
|
7740
|
-
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
7948
|
+
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
7741
7949
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
7742
7950
|
if (ctx.state._pk !== key) {
|
|
7743
7951
|
ctx.state._pk = key;
|
|
7744
7952
|
ctx.state._ptBody = theme.fg("muted", "(rendering…)");
|
|
7745
7953
|
ctx.state._ptDisplay = indentBranchBlock(withBranch(ctx.state._ptBody, theme, false, true));
|
|
7746
7954
|
const lg = lang(fp);
|
|
7747
|
-
|
|
7955
|
+
// Located once per row: expanding or resizing must not look in the file again, it no longer holds oldText.
|
|
7956
|
+
ctx.state._editLocalized ??= !finished
|
|
7957
|
+
? localizeEditOnce(ctx.toolCallId, fp, operations)
|
|
7958
|
+
: "preview" in finished
|
|
7959
|
+
? Promise.resolve(restoreLocalizedEditDiffs(finished.preview, operations))
|
|
7960
|
+
: computeLocalizedEditDiffs(fp, operations, cwd, typeof finished.patch === "string" ? finished.patch : "");
|
|
7961
|
+
void (ctx.state._editLocalized as Promise<LocalizedEditDiff[] | null>)
|
|
7748
7962
|
.then((localizedDiffs) => {
|
|
7749
7963
|
if (ctx.state._pk !== key) return;
|
|
7750
7964
|
const diffs = localizedDiffs?.map((entry) => entry.diff) ?? fallbackDiffs;
|
|
@@ -7913,6 +8127,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7913
8127
|
clearAllBashDurationContexts();
|
|
7914
8128
|
clearRtkRewriteState();
|
|
7915
8129
|
WRITE_EXISTED_BEFORE.clear();
|
|
8130
|
+
EDIT_LOCALIZED.clear();
|
|
7916
8131
|
clearHighlightCache();
|
|
7917
8132
|
invalidateThemePaletteCache();
|
|
7918
8133
|
bumpToolBranchVisualEpoch();
|
package/extensions/spinner.ts
CHANGED
|
@@ -73,7 +73,6 @@ function themeAdaptiveEnabled(): boolean {
|
|
|
73
73
|
const _DEFAULT_CLAUDE_ORANGE = "\x1b[38;2;215;119;87m";
|
|
74
74
|
const _DEFAULT_STATUS_DIM = "\x1b[38;2;153;153;153m";
|
|
75
75
|
|
|
76
|
-
let _themeColorsCacheTheme: unknown = null;
|
|
77
76
|
let _themeColorsLastAdaptive: boolean | null = null;
|
|
78
77
|
let _themeColorsLastVerbKey: string | null = null;
|
|
79
78
|
let _themeColorsLastStatusKey: string | null = null;
|
|
@@ -105,7 +104,6 @@ function applyThemeColors(theme: any): void {
|
|
|
105
104
|
_themeColorsLastAdaptive = adaptive;
|
|
106
105
|
_themeColorsLastVerbKey = verbColor;
|
|
107
106
|
_themeColorsLastStatusKey = statusColor;
|
|
108
|
-
_themeColorsCacheTheme = null;
|
|
109
107
|
if (!adaptive) {
|
|
110
108
|
CLAUDE_ORANGE = _DEFAULT_CLAUDE_ORANGE;
|
|
111
109
|
STATUS_DIM = _DEFAULT_STATUS_DIM;
|
|
@@ -113,9 +111,9 @@ function applyThemeColors(theme: any): void {
|
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
if (!theme || !adaptive) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
// ctx.ui.theme is Pi's global theme Proxy: the same object before and after
|
|
115
|
+
// /theme or a light/dark switch, so an identity cache never refreshed the
|
|
116
|
+
// spinner colors. Resolving is two map lookups per tick; just do it.
|
|
119
117
|
const verb = resolveThemeColor(theme, verbColor, "accent");
|
|
120
118
|
if (verb) CLAUDE_ORANGE = verb;
|
|
121
119
|
const status = resolveThemeColor(theme, statusColor, "muted");
|
|
@@ -792,8 +790,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
792
790
|
}
|
|
793
791
|
// Re-derive colors on every tick so /cc-spinner verb/status changes
|
|
794
792
|
// take effect within ~250 ms without waiting for the next pi event.
|
|
795
|
-
// applyThemeColors
|
|
796
|
-
// this is cheap when nothing changed.
|
|
793
|
+
// applyThemeColors only re-resolves two theme colors, so this is cheap.
|
|
797
794
|
applyThemeColors(activeCtx.ui?.theme);
|
|
798
795
|
const nextMessage = buildWorkingMessage();
|
|
799
796
|
if (!force && nextMessage === lastWorkingMessage) return;
|
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.89",
|
|
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",
|