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 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
@@ -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 ? humanizeToolName(sameName) : "Multiple Tools";
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]) => `${humanizeToolName(name)}${count > 1 ? `×${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 = humanizeToolName(getToolName(tool));
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
- proto[method] = function patchedToolMutation(...args: any[]) {
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
- function summarizeMcpToolCall(args: any, theme: Theme): string {
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("toolOutput", line || " "),
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" : "toolOutput", line || " "),
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;
@@ -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
- if (_themeColorsCacheTheme === theme) return;
117
- _themeColorsCacheTheme = theme;
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 is identity-cached on (theme, verbKey, statusKey) so
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.88",
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",