pi-claude-code-ui 1.0.88 → 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 +13 -0
- package/extensions/index.ts +132 -12
- package/extensions/spinner.ts +4 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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
|
+
|
|
6
19
|
## 1.0.88 — 2026-10-19
|
|
7
20
|
|
|
8
21
|
### Added
|
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");
|
|
@@ -615,7 +617,7 @@ function getGroupedToolName(tools: any[]): string | undefined {
|
|
|
615
617
|
|
|
616
618
|
function getToolGroupLabel(tools: any[]): string {
|
|
617
619
|
const sameName = getGroupedToolName(tools);
|
|
618
|
-
return sameName ?
|
|
620
|
+
return sameName ? toolDisplayName(sameName) : "Multiple Tools";
|
|
619
621
|
}
|
|
620
622
|
|
|
621
623
|
function getToolGroupOverallStatus(tools: any[]): ToolStatus {
|
|
@@ -683,7 +685,7 @@ function formatToolNameList(tools: any[]): string {
|
|
|
683
685
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
684
686
|
}
|
|
685
687
|
return [...counts.entries()]
|
|
686
|
-
.map(([name, count]) => `${
|
|
688
|
+
.map(([name, count]) => `${toolDisplayName(name)}${count > 1 ? `×${count}` : ""}`)
|
|
687
689
|
.join(", ");
|
|
688
690
|
}
|
|
689
691
|
|
|
@@ -763,6 +765,7 @@ function getToolArgSummary(tool: any): string {
|
|
|
763
765
|
if (name === "grep") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
|
|
764
766
|
if (name === "find") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
|
|
765
767
|
if (name === "ls") return shortPath(process.cwd(), args.path ?? ".");
|
|
768
|
+
if (parseNamespacedMcpToolName(name)) return summarizeDirectMcpArgs(args);
|
|
766
769
|
return summarizeText(getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt") || name, 72);
|
|
767
770
|
}
|
|
768
771
|
|
|
@@ -773,7 +776,7 @@ function getToolCallLine(tool: any): string {
|
|
|
773
776
|
return line.replaceAll(WRAP_MARK, "").replaceAll(CLIP_MARK, "");
|
|
774
777
|
}
|
|
775
778
|
const summary = getToolArgSummary(tool);
|
|
776
|
-
const label =
|
|
779
|
+
const label = toolDisplayName(getToolName(tool));
|
|
777
780
|
return `${label}${summary ? ` ${summary}` : ""}`;
|
|
778
781
|
}
|
|
779
782
|
|
|
@@ -917,6 +920,7 @@ class ToolGroupComponent extends Container {
|
|
|
917
920
|
private cachedStatusKey?: string;
|
|
918
921
|
private cachedToolCount?: number;
|
|
919
922
|
private cachedLines?: string[];
|
|
923
|
+
private paletteFingerprint?: string;
|
|
920
924
|
|
|
921
925
|
private clearRenderCache(): void {
|
|
922
926
|
this.dirty = true;
|
|
@@ -986,6 +990,15 @@ class ToolGroupComponent extends Container {
|
|
|
986
990
|
// only; children keep their own ToolText/TOOL_RENDER_CACHE entries and
|
|
987
991
|
// recompute only when their content actually changes.
|
|
988
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?.();
|
|
989
1002
|
}
|
|
990
1003
|
|
|
991
1004
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
@@ -1011,6 +1024,8 @@ class ToolGroupComponent extends Container {
|
|
|
1011
1024
|
}
|
|
1012
1025
|
const safeWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
1013
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();
|
|
1014
1029
|
// Child mutations mark dirty via clearToolRenderCache → invalidate().
|
|
1015
1030
|
if (
|
|
1016
1031
|
!this.dirty
|
|
@@ -2994,7 +3009,7 @@ function patchToolRenderCacheInvalidation(): void {
|
|
|
2994
3009
|
for (const method of methods) {
|
|
2995
3010
|
const original = proto[method];
|
|
2996
3011
|
if (typeof original !== "function") continue;
|
|
2997
|
-
|
|
3012
|
+
const run = function patchedToolMutation(this: any, ...args: any[]) {
|
|
2998
3013
|
clearToolRenderCache(this);
|
|
2999
3014
|
if (method === "updateDisplay" || method === "updateResult" || method === "invalidate") {
|
|
3000
3015
|
syncLiveToolRenderState(this);
|
|
@@ -3003,6 +3018,28 @@ function patchToolRenderCacheInvalidation(): void {
|
|
|
3003
3018
|
clearToolRenderCache(this);
|
|
3004
3019
|
return result;
|
|
3005
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
|
+
};
|
|
3006
3043
|
}
|
|
3007
3044
|
|
|
3008
3045
|
proto[TOOL_CACHE_PATCH_FLAG] = true;
|
|
@@ -3788,6 +3825,9 @@ class ToolText extends Text {
|
|
|
3788
3825
|
private toolCachedLines?: string[];
|
|
3789
3826
|
private observedWidth?: number;
|
|
3790
3827
|
private pendingObservedWidth?: number;
|
|
3828
|
+
getObservedWidth(): number | undefined {
|
|
3829
|
+
return this.observedWidth;
|
|
3830
|
+
}
|
|
3791
3831
|
private widthObserver?: (width: number) => void;
|
|
3792
3832
|
private widthObserverScheduled = false;
|
|
3793
3833
|
|
|
@@ -4153,6 +4193,13 @@ function themeBgRgb(theme: any, key: string): Rgb | null {
|
|
|
4153
4193
|
let _themePaletteCacheTheme: unknown = null;
|
|
4154
4194
|
let _themePaletteCacheName: string | null = null;
|
|
4155
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
|
+
}
|
|
4156
4203
|
|
|
4157
4204
|
/** Resolved-color fingerprint so palette re-derives when the active theme file changes under the same name/object. */
|
|
4158
4205
|
function themePaletteFingerprint(theme: any): string {
|
|
@@ -4567,6 +4614,7 @@ function resetThemePalette(): void {
|
|
|
4567
4614
|
|
|
4568
4615
|
function applyThemePaletteIfNeeded(theme: any): void {
|
|
4569
4616
|
if (!theme) return;
|
|
4617
|
+
_lastSeenTheme = theme;
|
|
4570
4618
|
if (!themeAdaptiveEnabled()) {
|
|
4571
4619
|
applyToolBranchColor(theme);
|
|
4572
4620
|
syncOutlineChromeFromBranch(theme);
|
|
@@ -4757,10 +4805,26 @@ function branchDiffWidth(componentWidth?: number, chromeWidth = 2): number {
|
|
|
4757
4805
|
return Math.max(20, Math.min(width - chromeWidth, MAX_TERM_WIDTH));
|
|
4758
4806
|
}
|
|
4759
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.
|
|
4760
4811
|
function contextDiffWidth(ctx: any, chromeWidth = 2): number {
|
|
4761
4812
|
return branchDiffWidth(ctx.state?._diffComponentWidth, chromeWidth);
|
|
4762
4813
|
}
|
|
4763
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
|
+
|
|
4764
4828
|
function adaptiveWrapRows(tw?: number): number {
|
|
4765
4829
|
const width = tw ?? termW();
|
|
4766
4830
|
if (width >= 180) return MAX_WRAP_ROWS_WIDE;
|
|
@@ -6013,6 +6077,18 @@ function isMcpToolName(name: string): boolean {
|
|
|
6013
6077
|
return name === "mcp" || /^mcp[_:-]/i.test(name) || /[_:-]mcp[_:-]/i.test(name);
|
|
6014
6078
|
}
|
|
6015
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
|
+
|
|
6016
6092
|
function shouldUseGenericToolRenderer(name: unknown): boolean {
|
|
6017
6093
|
return typeof name === "string" && name.length > 0 && !CORE_TOOL_OVERRIDES.has(name);
|
|
6018
6094
|
}
|
|
@@ -6519,8 +6595,11 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
6519
6595
|
}
|
|
6520
6596
|
ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);
|
|
6521
6597
|
|
|
6598
|
+
if (!diffWidthReady(ctx)) {
|
|
6599
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, `${hdr}\n${withBranch(theme.fg("muted", "(rendering…)"), theme, false, true)}`);
|
|
6600
|
+
}
|
|
6522
6601
|
const diffWidth = contextDiffWidth(ctx);
|
|
6523
|
-
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}`;
|
|
6524
6603
|
if (ctx.state._applyPatchPreviewKey !== key) {
|
|
6525
6604
|
ctx.state._applyPatchPreviewKey = key;
|
|
6526
6605
|
ctx.state._applyPatchPreviewBody = theme.fg("muted", "(rendering…)");
|
|
@@ -6603,7 +6682,37 @@ function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, c
|
|
|
6603
6682
|
return makeText(ctx.lastComponent, withBranch(`${theme.fg("success", "Applied")} ${meta.changeCount} files ${summary}${meta.totalLines ? ` ${theme.fg("muted", `(${meta.totalLines} diff lines)`)}` : ""}`, theme));
|
|
6604
6683
|
}
|
|
6605
6684
|
|
|
6606
|
-
|
|
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
|
+
}
|
|
6607
6716
|
const tool = getStringArg(args, "tool");
|
|
6608
6717
|
if (tool) return args?.server ? `${args.server}:${tool}` : tool;
|
|
6609
6718
|
const connect = getStringArg(args, "connect");
|
|
@@ -6614,14 +6723,14 @@ function summarizeMcpToolCall(args: any, theme: Theme): string {
|
|
|
6614
6723
|
}
|
|
6615
6724
|
|
|
6616
6725
|
function summarizeGenericToolCall(name: string, args: any, theme: Theme, sp: (path: string) => string): string {
|
|
6617
|
-
if (isMcpToolName(name)) return summarizeMcpToolCall(args, theme);
|
|
6726
|
+
if (isMcpToolName(name)) return summarizeMcpToolCall(name, args, theme);
|
|
6618
6727
|
return summarizeOpenAiToolCall(name, args, theme, sp);
|
|
6619
6728
|
}
|
|
6620
6729
|
|
|
6621
6730
|
function renderMcpToolResult(result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text {
|
|
6622
6731
|
if (isPartial) {
|
|
6623
6732
|
return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "MCP running..."), expanded, theme, ctx, {
|
|
6624
|
-
styleLine: (line) => theme.fg("
|
|
6733
|
+
styleLine: (line) => theme.fg("dim", line || " "),
|
|
6625
6734
|
}));
|
|
6626
6735
|
}
|
|
6627
6736
|
clearBlinkTimer(ctx);
|
|
@@ -6645,7 +6754,7 @@ function renderMcpToolResult(result: any, expanded: boolean, isPartial: boolean,
|
|
|
6645
6754
|
theme,
|
|
6646
6755
|
previewLimit(),
|
|
6647
6756
|
lines.length,
|
|
6648
|
-
(line) => theme.fg(ctx.isError ? "error" : "
|
|
6757
|
+
(line) => theme.fg(ctx.isError ? "error" : "dim", line || " "),
|
|
6649
6758
|
);
|
|
6650
6759
|
return makeText(ctx.lastComponent, withBranch(`${statusText}\n${preview}`, theme));
|
|
6651
6760
|
}
|
|
@@ -7705,10 +7814,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
7705
7814
|
if (d?._type === "diff") {
|
|
7706
7815
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
7707
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
|
+
}
|
|
7708
7821
|
const diffWidth = contextDiffWidth(ctx);
|
|
7709
7822
|
const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
|
|
7710
7823
|
const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
|
|
7711
|
-
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}`;
|
|
7712
7825
|
if (ctx.state._wdk !== key) {
|
|
7713
7826
|
ctx.state._wdk = key;
|
|
7714
7827
|
ctx.state._wdt = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
|
|
@@ -7735,8 +7848,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
7735
7848
|
const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
|
|
7736
7849
|
const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
|
|
7737
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
|
+
}
|
|
7738
7854
|
const diffWidth = contextDiffWidth(ctx);
|
|
7739
|
-
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
7855
|
+
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
7740
7856
|
if (ctx.state._nfk !== pk) {
|
|
7741
7857
|
ctx.state._nfk = pk;
|
|
7742
7858
|
ctx.state._nft = withFinalBranchBlock(`${richSummary}\n${theme.fg("muted", "rendering diff…")}`, theme);
|
|
@@ -7824,8 +7940,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
7824
7940
|
// History replay never sets argsComplete; a finished edit is drawn from its result instead.
|
|
7825
7941
|
const finished = ctx.state._editResult as Record<string, unknown> | undefined;
|
|
7826
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
|
+
}
|
|
7827
7947
|
const diffWidth = contextDiffWidth(ctx, 3);
|
|
7828
|
-
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}`;
|
|
7829
7949
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
7830
7950
|
if (ctx.state._pk !== key) {
|
|
7831
7951
|
ctx.state._pk = key;
|
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",
|