@quandev104/pi-style 0.2.9 → 0.2.10

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.
@@ -8,6 +8,13 @@
8
8
  // global tool-output toggle (Ctrl+O) expands everything again
9
9
  // (`options.expanded` is read, never written).
10
10
  //
11
+ // The summary also reports the turn's aggregate diff stats (`· Edit +6 -2`,
12
+ // diff colors) computed purely from tool-result data — `details.diff` for
13
+ // edit, the parsed `── diff ──` output section for the quick-edit family
14
+ // (the same sources the box renderers read) — so live, scroll-back, and
15
+ // resume render identically. `write` carries no diff and is skipped; error
16
+ // members keep their visible blocks and never contribute stats.
17
+ //
11
18
  // Mutating tools (edit/write/quick_edit/substitute_edit/target_edit) are
12
19
  // exempt from the summary by default (`tools.collapseMutatingTools: off`):
13
20
  // their blocks are the record of what was done to the user's files, so they
@@ -34,8 +41,11 @@
34
41
  import type { Component } from "@earendil-works/pi-tui";
35
42
  import type { BoxTheme } from "../../../shared/box.js";
36
43
  import { safeTruncateToWidth } from "../../../shared/render-budget.js";
44
+ import { countDiffStats, firstText } from "../../../shared/split-diff.js";
37
45
  import { pluralForm } from "./output-tree.js";
46
+ import { extractQuickEditDiff, getQuickEditToolConfig } from "./quick-edit.js";
38
47
  import { getToolsRenderConfig } from "./session-config.js";
48
+ import { formatDiffStatsPair } from "./shared.js";
39
49
 
40
50
  export interface TurnMemberInfo {
41
51
  readonly toolCallId: string;
@@ -45,6 +55,8 @@ export interface TurnMemberInfo {
45
55
  isError: boolean;
46
56
  /** Frozen wall-clock elapsed (ms), recorded from the renderer context state. */
47
57
  elapsedMs?: number;
58
+ /** Frozen diff line stats recorded from the tool result (edit family). */
59
+ diffStats?: { additions: number; removals: number } | undefined;
48
60
  }
49
61
 
50
62
  export interface TurnState {
@@ -114,22 +126,72 @@ function toolCallsOf(message: unknown): ToolCallLike[] {
114
126
  return calls;
115
127
  }
116
128
 
117
- function registerTurn(
129
+ /** Tool-result fields the registry consumes (ToolResultMessage subset). */
130
+ export interface TurnResultLike {
131
+ readonly toolCallId: string;
132
+ readonly isError?: boolean;
133
+ readonly content?: readonly unknown[] | undefined;
134
+ readonly details?: unknown;
135
+ }
136
+
137
+ /** Result facts per tool call id: presence, error flag, and raw payload. */
138
+ interface RawMemberResult {
139
+ readonly isError: boolean;
140
+ readonly content?: readonly unknown[] | undefined;
141
+ readonly details?: unknown;
142
+ }
143
+
144
+ /**
145
+ * Extract a mutating member's diff line stats from its tool result — the
146
+ * same sources the box renderers read: `details.diff` for `edit`, the
147
+ * parsed `── diff ──` output section for the quick-edit family. `write`
148
+ * carries no diff and yields undefined. Pure: no render-time work.
149
+ */
150
+ function diffStatsFromResult(
151
+ toolName: string,
152
+ result: RawMemberResult | undefined,
153
+ ): { additions: number; removals: number } | undefined {
154
+ if (!result) return undefined;
155
+ if (isMutatingTool(toolName) && toolName !== "write") {
156
+ const diff = (result.details as { diff?: unknown } | undefined)?.diff;
157
+ if (typeof diff === "string" && diff.length > 0) return countDiffStats(diff);
158
+ }
159
+ if (getQuickEditToolConfig(toolName)) {
160
+ const text = Array.isArray(result.content)
161
+ ? firstText(result.content as Array<{ type: string; text?: string }>)
162
+ : "";
163
+ const diff = text ? extractQuickEditDiff(text) : undefined;
164
+ if (diff) return countDiffStats(diff);
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ function buildMembers(
118
170
  calls: readonly ToolCallLike[],
119
- isErrorById: ReadonlyMap<string, boolean>,
120
- ended: boolean,
121
- ): TurnState | undefined {
122
- if (calls.length === 0) return undefined;
123
- const complete = calls.every((call) => typeof call.id === "string" && isErrorById.has(call.id));
124
- const members: TurnMemberInfo[] = calls.map((call) => {
171
+ resultsById: ReadonlyMap<string, RawMemberResult>,
172
+ ): TurnMemberInfo[] {
173
+ return calls.map((call) => {
125
174
  const toolCallId = String(call.id ?? "");
175
+ const toolName = typeof call.name === "string" ? call.name : "tool";
176
+ const result = resultsById.get(toolCallId);
126
177
  return {
127
178
  toolCallId,
128
- toolName: typeof call.name === "string" ? call.name : "tool",
129
- hasResult: isErrorById.has(toolCallId),
130
- isError: isErrorById.get(toolCallId) === true,
179
+ toolName,
180
+ hasResult: result !== undefined,
181
+ isError: result?.isError === true,
182
+ diffStats: diffStatsFromResult(toolName, result),
131
183
  };
132
184
  });
185
+ }
186
+
187
+ function registerTurn(
188
+ calls: readonly ToolCallLike[],
189
+ resultsById: ReadonlyMap<string, RawMemberResult>,
190
+ ended: boolean,
191
+ ): TurnState | undefined {
192
+ if (calls.length === 0) return undefined;
193
+ const complete = calls.every((call) => typeof call.id === "string" && resultsById.has(String(call.id ?? "")));
194
+ const members: TurnMemberInfo[] = buildMembers(calls, resultsById);
133
195
  const leader = members.find((member) => !member.isError && (!isMutatingTool(member.toolName) || mutatingCollapses()));
134
196
  const turn: TurnState = {
135
197
  leaderId: leader?.toolCallId ?? "",
@@ -140,11 +202,6 @@ function registerTurn(
140
202
  return turn;
141
203
  }
142
204
 
143
- export interface TurnResultLike {
144
- readonly toolCallId: string;
145
- readonly isError?: boolean;
146
- }
147
-
148
205
  /**
149
206
  * One summary group = one agent run (user request → `agent_end`). Pi emits
150
207
  * `turn_end` per assistant message, so tool batches of the same request are
@@ -165,20 +222,16 @@ export function beginAgentRun(): void {
165
222
  export function registerTurnFromMessage(message: unknown, toolResults: readonly TurnResultLike[]): void {
166
223
  const calls = toolCallsOf(message);
167
224
  if (calls.length === 0) return;
168
- const isErrorById = new Map<string, boolean>();
225
+ const resultsById = new Map<string, RawMemberResult>();
169
226
  for (const result of toolResults) {
170
227
  if (typeof result?.toolCallId !== "string") continue;
171
- isErrorById.set(result.toolCallId, result.isError === true);
228
+ resultsById.set(result.toolCallId, {
229
+ isError: result.isError === true,
230
+ content: result.content,
231
+ details: result.details,
232
+ });
172
233
  }
173
- const newMembers: TurnMemberInfo[] = calls.map((call) => {
174
- const toolCallId = String(call.id ?? "");
175
- return {
176
- toolCallId,
177
- toolName: typeof call.name === "string" ? call.name : "tool",
178
- hasResult: isErrorById.has(toolCallId),
179
- isError: isErrorById.get(toolCallId) === true,
180
- };
181
- });
234
+ const newMembers: TurnMemberInfo[] = buildMembers(calls, resultsById);
182
235
  const leader = newMembers.find(
183
236
  (member) => !member.isError && (!isMutatingTool(member.toolName) || mutatingCollapses()),
184
237
  );
@@ -215,6 +268,7 @@ interface TurnEntryLike {
215
268
  readonly message?: {
216
269
  readonly role?: unknown;
217
270
  readonly content?: unknown;
271
+ readonly details?: unknown;
218
272
  readonly stopReason?: unknown;
219
273
  readonly toolCallId?: unknown;
220
274
  readonly isError?: unknown;
@@ -231,8 +285,7 @@ interface TurnEntryLike {
231
285
  export function rebuildTurnRegistryFromEntries(entries: readonly TurnEntryLike[] | undefined): void {
232
286
  memberByCallId.clear();
233
287
  if (!Array.isArray(entries)) return;
234
- const isErrorById = new Map<string, boolean>();
235
- const resultById = new Set<string>();
288
+ const resultsById = new Map<string, RawMemberResult>();
236
289
  const runs: Array<{
237
290
  calls: ToolCallLike[];
238
291
  lastStopReason: string | undefined;
@@ -247,8 +300,11 @@ export function rebuildTurnRegistryFromEntries(entries: readonly TurnEntryLike[]
247
300
  if (entry?.type !== "message") return;
248
301
  const message = entry.message;
249
302
  if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
250
- resultById.add(message.toolCallId);
251
- isErrorById.set(message.toolCallId, message.isError === true);
303
+ resultsById.set(message.toolCallId, {
304
+ isError: message.isError === true,
305
+ content: Array.isArray(message.content) ? (message.content as readonly unknown[]) : undefined,
306
+ details: message.details,
307
+ });
252
308
  } else if (message?.role === "assistant") {
253
309
  if (!current) current = { calls: [], lastStopReason: undefined, followedByUser: false };
254
310
  const calls = toolCallsOf(message);
@@ -267,9 +323,9 @@ export function rebuildTurnRegistryFromEntries(entries: readonly TurnEntryLike[]
267
323
  });
268
324
  closeRun();
269
325
  for (const run of runs) {
270
- const complete = run.calls.every((call) => typeof call.id === "string" && resultById.has(call.id));
326
+ const complete = run.calls.every((call) => typeof call.id === "string" && resultsById.has(String(call.id ?? "")));
271
327
  const ended = complete && (run.followedByUser || run.lastStopReason !== undefined);
272
- registerTurn(run.calls, isErrorById, ended);
328
+ registerTurn(run.calls, resultsById, ended);
273
329
  }
274
330
  }
275
331
 
@@ -349,25 +405,37 @@ export interface TurnSummaryParts {
349
405
  readonly failedCount: number;
350
406
  /** Sum of members' frozen elapsed; undefined when nothing was recorded. */
351
407
  readonly elapsedMs: number | undefined;
408
+ /** Aggregate diff line stats over non-error edit-family members; undefined
409
+ * when none carried a diff. Collected regardless of the mutating exemption:
410
+ * visible edit blocks are exactly what these stats describe. */
411
+ readonly diffStats: { additions: number; removals: number } | undefined;
352
412
  }
353
413
 
354
414
  /**
355
415
  * Aggregate a turn's collapsed members into summary parts (pure). Mutating
356
- * members are excluded unless `tools.collapseMutatingTools` is on — by default
357
- * their visible blocks are the record; the summary describes only what it
358
- * hides.
416
+ * members are excluded from counts/elapsed unless `tools.collapseMutatingTools`
417
+ * is on — by default their visible blocks are the record; the summary counts
418
+ * only what it hides. Their diff stats aggregate either way.
359
419
  */
360
420
  export function turnSummaryParts(turn: TurnState): TurnSummaryParts {
361
421
  const counts = new Map<string, number>();
362
422
  const order: string[] = [];
363
423
  let failedCount = 0;
364
424
  let elapsedMs: number | undefined;
425
+ let diffAdditions = 0;
426
+ let diffRemovals = 0;
427
+ let diffMembers = 0;
365
428
  const collapseMutating = mutatingCollapses();
366
429
  for (const member of turn.members) {
367
430
  if (member.isError) {
368
431
  failedCount++;
369
432
  continue;
370
433
  }
434
+ if (member.diffStats !== undefined) {
435
+ diffAdditions += member.diffStats.additions;
436
+ diffRemovals += member.diffStats.removals;
437
+ diffMembers++;
438
+ }
371
439
  if (!collapseMutating && isMutatingTool(member.toolName)) continue;
372
440
  if (member.elapsedMs !== undefined) elapsedMs = (elapsedMs ?? 0) + member.elapsedMs;
373
441
  const existing = counts.get(member.toolName);
@@ -383,16 +451,24 @@ export function turnSummaryParts(turn: TurnState): TurnSummaryParts {
383
451
  // neutral phrasing with the invariant tool name: `used 5 TaskCreate`.
384
452
  return style ? `${style.verb} ${count} ${pluralForm(style.unit, count)}` : `used ${count} ${toolName}`;
385
453
  });
386
- return { parts, failedCount, elapsedMs };
454
+ return {
455
+ parts,
456
+ failedCount,
457
+ elapsedMs,
458
+ diffStats: diffMembers > 0 ? { additions: diffAdditions, removals: diffRemovals } : undefined,
459
+ };
387
460
  }
388
461
 
389
462
  function formatTurnSummaryLine(theme: BoxTheme, turn: TurnState): string {
390
463
  const summary = turnSummaryParts(turn);
391
464
  // The summary is deliberately quiet: the whole line renders dim so completed
392
- // tool work recedes behind the assistant's answer. Only the failed marker
393
- // stays error-colored (errors must remain visible).
465
+ // tool work recedes behind the assistant's answer. Only the diff stats
466
+ // (`+N` added / `-M` removed) and the failed marker stay color-coded —
467
+ // changes and errors must remain visible at a glance.
394
468
  const parts = summary.parts.join(", ");
395
469
  let line = `${theme.fg("dim", `➔ ${parts}`)}`;
470
+ if (summary.diffStats !== undefined)
471
+ line += `${theme.fg("dim", " · Edit ")}${formatDiffStatsPair(theme, summary.diffStats.additions, summary.diffStats.removals)}`;
396
472
  if (summary.failedCount > 0)
397
473
  line += theme.fg("error", ` · ${summary.failedCount} ${pluralForm("failure", summary.failedCount)}`);
398
474
  if (summary.elapsedMs !== undefined) line += theme.fg("dim", ` · ${(summary.elapsedMs / 1000).toFixed(2)}s`);
@@ -89,6 +89,7 @@ function renderWritePreviewBox(
89
89
  isError: options.isError,
90
90
  isPending: options.isPending,
91
91
  running: Boolean(options.running),
92
+ tint: true, // the write preview is a framed box — it owns its status tint
92
93
  bodyLines: () => {
93
94
  if (preview.length === 0) return [];
94
95
  const shown = preview.slice(0, budget).map((line) => formatNumberedLine(theme, line));
@@ -15,13 +15,16 @@ function hideBatchMember(instance: object): void {
15
15
  }
16
16
 
17
17
  /**
18
- * Neutralize the native ToolExecutionComponent status background for boxed
18
+ * Neutralize the native ToolExecutionComponent container fill for boxed
19
19
  * rendering: Pi's updateDisplay sets contentBox/selfRenderContainer bgFn to
20
20
  * toolPendingBg/toolErrorBg/toolSuccessBg before invoking the renderers. The
21
- * boxed renderers own their visual boundary (borders + ✓/✗ state marks), so the
22
- * container fill is removed (no background slab).
23
- * Runs on every boxed dispatch; updateDisplay re-applies the bgFn on the next
24
- * pass and this wrapper re-neutralizes it.
21
+ * rendered boxes own their tint (box background): boxed components wrap
22
+ * their own lines in the status fill while boxless surfaces (quiet-tool rows,
23
+ * tree panels, git/gh semantic cards, turn summaries) stay transparent so
24
+ * the container fill is always removed. The native Box padding (1,1) is
25
+ * zeroed as well, or the frame would gain stray blank rows and an indent.
26
+ * Runs on every boxed dispatch; updateDisplay re-applies both on the next
27
+ * pass and this wrapper re-neutralizes them.
25
28
  */
26
29
  function neutralizeToolContainerBackground(instance: object): void {
27
30
  const host = instance as {
@@ -421,7 +424,6 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
421
424
  // fallback renderer, mirroring the generic boxed fallback used for
422
425
  // unknown tool names.
423
426
  if (typeof renderer !== "function") {
424
- neutralizeToolContainerBackground(instance);
425
427
  if (subtype === "tool-call-renderer")
426
428
  return (callArgs: unknown, theme: unknown, context: unknown) => {
427
429
  const component = renderBoxedToolCall(
@@ -430,6 +432,7 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
430
432
  theme as never,
431
433
  context as never,
432
434
  );
435
+ neutralizeToolContainerBackground(instance);
433
436
  // Same batch-member contract as the native-renderer path: a
434
437
  // collapsed turn member (or quiet batch member) returns the
435
438
  // singleton and must be hidden, or Pi leaves a stray native
@@ -445,6 +448,7 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
445
448
  theme as never,
446
449
  context as never,
447
450
  );
451
+ neutralizeToolContainerBackground(instance);
448
452
  if (component === EMPTY_BATCH_COMPONENT) hideBatchMember(instance);
449
453
  return component;
450
454
  };
@@ -168,6 +168,8 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
168
168
  assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
169
169
  assistantEnabled,
170
170
  collapseHiddenThinking: thinkingCollapseEnabled,
171
+ thoughtSummary: thinkingCollapseEnabled && config.messages.thoughtSummary,
172
+ thoughtGlyph: authorization.ascii ? ">" : "◈",
171
173
  },
172
174
  toolSnapshot: {
173
175
  callMarker: authorization.ascii ? "[tool] " : "[tool] ",
@@ -44,6 +44,7 @@ export const SUPPORTED_PI_VERSIONS: readonly string[] = Object.freeze([
44
44
  "0.84.3",
45
45
  "0.84.4",
46
46
  "0.85.0",
47
+ "0.85.1",
47
48
  ]);
48
49
 
49
50
  /** A recorded native identity for one certified surface. */
@@ -97,7 +98,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
97
98
  name: "render",
98
99
  arity: 1,
99
100
  fingerprint: "a9be09a3",
100
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
101
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
101
102
  }),
102
103
  ]),
103
104
  "native-assistant-message:updateContent": Object.freeze([
@@ -132,7 +133,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
132
133
  name: "updateContent",
133
134
  arity: 1,
134
135
  fingerprint: "80e338d2",
135
- versions: Object.freeze(["0.85.0"]),
136
+ versions: Object.freeze(["0.85.0", "0.85.1"]),
136
137
  }),
137
138
  // 0.85.0 bundled: same drift, minified.
138
139
  Object.freeze({
@@ -141,6 +142,16 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
141
142
  fingerprint: "c3d72f2b",
142
143
  versions: Object.freeze(["0.85.0"]),
143
144
  }),
145
+ // 0.85.1 bundled: the rebundled runtime renames the minified `message2`
146
+ // parameter to `message` — the modular dist is unchanged from 0.85.0 (only
147
+ // the GPT-6 Astra model catalog and fullscreen-scroll fixes landed), so the
148
+ // minified method text drifts while behavior stays identical.
149
+ Object.freeze({
150
+ name: "updateContent",
151
+ arity: 1,
152
+ fingerprint: "31632e19",
153
+ versions: Object.freeze(["0.85.1"]),
154
+ }),
144
155
  ]),
145
156
  "native-compaction-message:updateDisplay": Object.freeze([
146
157
  Object.freeze({
@@ -153,7 +164,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
153
164
  name: "updateDisplay",
154
165
  arity: 0,
155
166
  fingerprint: "5118a51d",
156
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
167
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
157
168
  }),
158
169
  ]),
159
170
  "native-branch-message:updateDisplay": Object.freeze([
@@ -167,7 +178,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
167
178
  name: "updateDisplay",
168
179
  arity: 0,
169
180
  fingerprint: "2185274e",
170
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
181
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
171
182
  }),
172
183
  ]),
173
184
  "native-skill-message:updateDisplay": Object.freeze([
@@ -181,7 +192,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
181
192
  name: "updateDisplay",
182
193
  arity: 0,
183
194
  fingerprint: "4051fd65",
184
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
195
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
185
196
  }),
186
197
  ]),
187
198
  "native-custom-message:rebuild": Object.freeze([
@@ -195,7 +206,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
195
206
  name: "rebuild",
196
207
  arity: 0,
197
208
  fingerprint: "b89987cc",
198
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
209
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
199
210
  }),
200
211
  ]),
201
212
  "tool-call-renderer:getCallRenderer": Object.freeze([
@@ -224,7 +235,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
224
235
  name: "getCallRenderer",
225
236
  arity: 0,
226
237
  fingerprint: "73116365",
227
- versions: Object.freeze(["0.85.0"]),
238
+ versions: Object.freeze(["0.85.0", "0.85.1"]),
228
239
  }),
229
240
  ]),
230
241
  "tool-result-renderer:getResultRenderer": Object.freeze([
@@ -253,7 +264,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
253
264
  name: "getResultRenderer",
254
265
  arity: 0,
255
266
  fingerprint: "d613a2a3",
256
- versions: Object.freeze(["0.85.0"]),
267
+ versions: Object.freeze(["0.85.0", "0.85.1"]),
257
268
  }),
258
269
  ]),
259
270
  "native-bash-execution:render": Object.freeze([
@@ -271,7 +282,7 @@ export const KNOWN_NATIVE_IDENTITIES: Readonly<Record<string, readonly KnownNati
271
282
  name: "BashExecutionComponent",
272
283
  arity: 2,
273
284
  fingerprint: "98d22d96",
274
- versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0"]),
285
+ versions: Object.freeze(["0.84.3", "0.84.4", "0.85.0", "0.85.1"]),
275
286
  }),
276
287
  ]),
277
288
  });
@@ -4,6 +4,7 @@ import type { ConfigFilePort } from "../app/config-storage.js";
4
4
  import { createPiStyleApp, type PiStyleApp } from "../app/index.js";
5
5
  import { resolveTheme } from "../domain/theme.js";
6
6
  import { resetPendingImageRegistry } from "../features/messages/image-input.js";
7
+ import { setThoughtLabelTheme } from "../features/messages/index.js";
7
8
  import { setMessagesRenderConfig } from "../features/messages/render-config.js";
8
9
  import { setSpecialBlockTheme } from "../features/messages/special-blocks.js";
9
10
  import { setBashExecutionTheme } from "../features/tools/bash-execution.js";
@@ -94,9 +95,26 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
94
95
  * Hide Pi's "Thinking..." placeholder label: an empty label renders zero
95
96
  * lines, so the thinking block leaves no trace while content stays hidden.
96
97
  * Passing undefined restores the default label.
98
+ *
99
+ * Gated on the certified `updateContent` surface actually being installed:
100
+ * blanking without the patch leaves Pi's native invisible-row gap (worse
101
+ * than the label it replaces), so an unsupported runtime identity keeps the
102
+ * native `Thinking...` label instead.
97
103
  */
104
+ const thinkingCollapseInstalled = (): boolean => {
105
+ const records = compatibility.report?.recordSnapshots ?? [];
106
+ return records.some(
107
+ (record) =>
108
+ record.subtype === "native-assistant-message" &&
109
+ record.method === "updateContent" &&
110
+ record.shape === "installed" &&
111
+ !record.disposed,
112
+ );
113
+ };
98
114
  const applyMessagesConfig = (config: import("../domain/config-types.js").NormalizedPiStyleConfig) => {
99
- sessionUi?.setHiddenThinkingLabel?.(config.messages.hideThinkingLabel ? "" : undefined);
115
+ sessionUi?.setHiddenThinkingLabel?.(
116
+ config.messages.hideThinkingLabel && thinkingCollapseInstalled() ? "" : undefined,
117
+ );
100
118
  // User-prompt image previews (ADR 0008) + clipboard image input (ADR
101
119
  // 0009): the leaves gate their respective sides (preview: stage+render;
102
120
  // clipboard: input transform) and size the preview images.
@@ -211,6 +229,7 @@ export function createPiStyleSessionCoordinator(pi: ExtensionAPI, hooks: Compati
211
229
  applyMessagesConfig(app.config);
212
230
  if (ctx.ui?.theme) setSpecialBlockTheme(ctx.ui.theme as never);
213
231
  if (ctx.ui?.theme) setBashExecutionTheme(ctx.ui.theme as never);
232
+ if (ctx.ui?.theme) setThoughtLabelTheme(ctx.ui.theme as never);
214
233
  const toolDetails = collectToolDetails(pi.getActiveTools?.(), pi.getAllTools?.());
215
234
  app.sessionStart(
216
235
  {