@quandev104/pi-style 0.1.5 → 0.1.7

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.
@@ -0,0 +1,368 @@
1
+ // Turn tool summary registry (ADR 0007).
2
+ //
3
+ // When a turn completes, its finalized tool blocks collapse into a single
4
+ // summary line (`➔ Read 2 files, ran 4 shell commands · 3.1s`) rendered by the
5
+ // turn's leader (its first non-error tool call); every other non-error tool
6
+ // item of the turn renders zero lines. Error results stay visible, interrupted
7
+ // turns never collapse, and Pi's global tool-output toggle (Ctrl+O) expands
8
+ // everything again (`options.expanded` is read, never written).
9
+ //
10
+ // Design notes:
11
+ // - The registry is populated from **session content**, never from runtime
12
+ // event flags: the live path registers the final assistant message +
13
+ // toolResults at `turn_end`; the restore path rebuilds the registry from
14
+ // `sessionManager.getEntries()` at session start / `session_tree`, so
15
+ // scroll-back and session resume render identically.
16
+ // - A turn is "ended" only when every tool call of its message has a matching
17
+ // tool result AND (live) turn_end fired / (restore) the message is
18
+ // finalized (`stopReason`) or a later user/assistant message exists.
19
+ // - Elapsed per member is frozen from the renderer wall-clock state
20
+ // (STARTED_AT/ENDED_AT) at the first post-turn result pass; the summary
21
+ // totals the members' frozen elapsed. No render-time I/O.
22
+ // - No new Pi-core patch identity: the dispatcher (boxed/index.ts) decides
23
+ // collapse before the per-tool renderers run, so every certified renderer
24
+ // surface stays untouched when the turn is not collapsed.
25
+
26
+ import type { Component } from "@earendil-works/pi-tui";
27
+ import type { BoxTheme } from "../../../shared/box.js";
28
+ import { safeTruncateToWidth } from "../../../shared/render-budget.js";
29
+ import { pluralForm } from "./output-tree.js";
30
+
31
+ export interface TurnMemberInfo {
32
+ readonly toolCallId: string;
33
+ readonly toolName: string;
34
+ /** Whether a tool result was registered for this call (run completeness). */
35
+ readonly hasResult: boolean;
36
+ isError: boolean;
37
+ /** Frozen wall-clock elapsed (ms), recorded from the renderer context state. */
38
+ elapsedMs?: number;
39
+ }
40
+
41
+ export interface TurnState {
42
+ /** First non-error member; renders the summary line. Empty when every member errored. */
43
+ leaderId: string;
44
+ ended: boolean;
45
+ members: readonly TurnMemberInfo[];
46
+ }
47
+
48
+ interface TurnEntry {
49
+ readonly turn: TurnState;
50
+ readonly member: TurnMemberInfo;
51
+ }
52
+
53
+ const memberByCallId = new Map<string, TurnEntry>();
54
+
55
+ /** Per-member component invalidate callbacks captured during render passes. */
56
+ const invalidateByCallId = new Map<string, () => void>();
57
+
58
+ /** Reset all turn state (session start/shutdown). */
59
+ export function resetTurnRegistry(): void {
60
+ memberByCallId.clear();
61
+ invalidateByCallId.clear();
62
+ }
63
+
64
+ interface ToolCallLike {
65
+ readonly type?: unknown;
66
+ readonly id?: unknown;
67
+ readonly name?: unknown;
68
+ }
69
+
70
+ function toolCallsOf(message: unknown): ToolCallLike[] {
71
+ if (!message || typeof message !== "object") return [];
72
+ const content = (message as { readonly content?: unknown }).content;
73
+ if (!Array.isArray(content)) return [];
74
+ const calls: ToolCallLike[] = [];
75
+ for (const item of content) {
76
+ if (!item || typeof item !== "object") continue;
77
+ const candidate = item as ToolCallLike;
78
+ if (candidate.type === "toolCall") calls.push(candidate);
79
+ }
80
+ return calls;
81
+ }
82
+
83
+ function registerTurn(
84
+ calls: readonly ToolCallLike[],
85
+ isErrorById: ReadonlyMap<string, boolean>,
86
+ ended: boolean,
87
+ ): TurnState | undefined {
88
+ if (calls.length === 0) return undefined;
89
+ const complete = calls.every((call) => typeof call.id === "string" && isErrorById.has(call.id));
90
+ const members: TurnMemberInfo[] = calls.map((call) => {
91
+ const toolCallId = String(call.id ?? "");
92
+ return {
93
+ toolCallId,
94
+ toolName: typeof call.name === "string" ? call.name : "tool",
95
+ hasResult: isErrorById.has(toolCallId),
96
+ isError: isErrorById.get(toolCallId) === true,
97
+ };
98
+ });
99
+ const leader = members.find((member) => !member.isError);
100
+ const turn: TurnState = {
101
+ leaderId: leader?.toolCallId ?? "",
102
+ ended: ended && complete,
103
+ members: Object.freeze(members),
104
+ };
105
+ for (const member of members) memberByCallId.set(member.toolCallId, { turn, member });
106
+ return turn;
107
+ }
108
+
109
+ export interface TurnResultLike {
110
+ readonly toolCallId: string;
111
+ readonly isError?: boolean;
112
+ }
113
+
114
+ /**
115
+ * One summary group = one agent run (user request → `agent_end`). Pi emits
116
+ * `turn_end` per assistant message, so tool batches of the same request are
117
+ * appended to the same run and collapse into ONE summary line at `agent_end`.
118
+ */
119
+ let currentRun: TurnState | undefined;
120
+
121
+ /** Live path: start a fresh run group (`agent_start`). */
122
+ export function beginAgentRun(): void {
123
+ currentRun = undefined;
124
+ }
125
+
126
+ /**
127
+ * Live path: append the finalized assistant message's tool calls and results
128
+ * to the current run (`turn_end` event). The run stays expanded until
129
+ * `finishAgentRun`; a batch interrupted mid-tool never collapses.
130
+ */
131
+ export function registerTurnFromMessage(message: unknown, toolResults: readonly TurnResultLike[]): void {
132
+ const calls = toolCallsOf(message);
133
+ if (calls.length === 0) return;
134
+ const isErrorById = new Map<string, boolean>();
135
+ for (const result of toolResults) {
136
+ if (typeof result?.toolCallId !== "string") continue;
137
+ isErrorById.set(result.toolCallId, result.isError === true);
138
+ }
139
+ const newMembers: TurnMemberInfo[] = calls.map((call) => {
140
+ const toolCallId = String(call.id ?? "");
141
+ return {
142
+ toolCallId,
143
+ toolName: typeof call.name === "string" ? call.name : "tool",
144
+ hasResult: isErrorById.has(toolCallId),
145
+ isError: isErrorById.get(toolCallId) === true,
146
+ };
147
+ });
148
+ const leader = newMembers.find((member) => !member.isError);
149
+ if (!currentRun) {
150
+ currentRun = {
151
+ leaderId: leader?.toolCallId ?? "",
152
+ ended: false,
153
+ members: Object.freeze(newMembers),
154
+ };
155
+ } else {
156
+ if (currentRun.leaderId === "" && leader) currentRun.leaderId = leader.toolCallId;
157
+ currentRun.members = Object.freeze([...currentRun.members, ...newMembers]);
158
+ }
159
+ for (const member of newMembers) memberByCallId.set(member.toolCallId, { turn: currentRun, member });
160
+ }
161
+
162
+ /**
163
+ * Live path: finalize the current run (`agent_end`). Returns the run so the
164
+ * caller can invalidate its blocks; undefined when the run had no tool calls
165
+ * or is interrupted (a call without a result stays expanded).
166
+ */
167
+ export function finishAgentRun(): TurnState | undefined {
168
+ const run = currentRun;
169
+ currentRun = undefined;
170
+ if (!run) return undefined;
171
+ // A member without a result means the run was interrupted before every call
172
+ // settled; such a run never collapses.
173
+ if (run.members.every((member) => member.hasResult)) run.ended = true;
174
+ return run.ended ? run : undefined;
175
+ }
176
+
177
+ interface TurnEntryLike {
178
+ readonly type?: unknown;
179
+ readonly message?: {
180
+ readonly role?: unknown;
181
+ readonly content?: unknown;
182
+ readonly stopReason?: unknown;
183
+ readonly toolCallId?: unknown;
184
+ readonly isError?: unknown;
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Restore path: rebuild the registry from session entries (session start /
190
+ * `session_tree`). Consecutive assistant messages between user messages form
191
+ * one run; a run is ended when every tool call has a result AND a later user
192
+ * message exists (historical) or its last assistant message is finalized
193
+ * (`stopReason`). The currently streaming run stays expanded.
194
+ */
195
+ export function rebuildTurnRegistryFromEntries(entries: readonly TurnEntryLike[] | undefined): void {
196
+ memberByCallId.clear();
197
+ if (!Array.isArray(entries)) return;
198
+ const isErrorById = new Map<string, boolean>();
199
+ const resultById = new Set<string>();
200
+ const runs: Array<{
201
+ calls: ToolCallLike[];
202
+ lastStopReason: string | undefined;
203
+ followedByUser: boolean;
204
+ }> = [];
205
+ let current: (typeof runs)[number] | undefined;
206
+ const closeRun = () => {
207
+ if (current && current.calls.length > 0) runs.push(current);
208
+ current = undefined;
209
+ };
210
+ entries.forEach((entry) => {
211
+ if (entry?.type !== "message") return;
212
+ const message = entry.message;
213
+ if (message?.role === "toolResult" && typeof message.toolCallId === "string") {
214
+ resultById.add(message.toolCallId);
215
+ isErrorById.set(message.toolCallId, message.isError === true);
216
+ } else if (message?.role === "assistant") {
217
+ if (!current) current = { calls: [], lastStopReason: undefined, followedByUser: false };
218
+ const calls = toolCallsOf(message);
219
+ current.calls.push(...calls);
220
+ if (typeof message.stopReason === "string" && message.stopReason !== "")
221
+ current.lastStopReason = message.stopReason;
222
+ } else if (message?.role === "user") {
223
+ if (current) {
224
+ current.followedByUser = true;
225
+ closeRun();
226
+ } else {
227
+ // A user message with no preceding open run is a plain boundary.
228
+ closeRun();
229
+ }
230
+ }
231
+ });
232
+ closeRun();
233
+ for (const run of runs) {
234
+ const complete = run.calls.every((call) => typeof call.id === "string" && resultById.has(call.id));
235
+ const ended = complete && (run.followedByUser || run.lastStopReason !== undefined);
236
+ registerTurn(run.calls, isErrorById, ended);
237
+ }
238
+ }
239
+
240
+ /** Registry lookup for the render dispatcher. */
241
+ export function getTurnEntry(toolCallId: string): TurnEntry | undefined {
242
+ return memberByCallId.get(toolCallId);
243
+ }
244
+
245
+ /**
246
+ * Capture a member's component invalidate callback during a render pass. Pi
247
+ * only re-invokes the tool renderer selectors from updateDisplay(); calling
248
+ * the captured callback after turn_end rebuilds the block with the collapsed
249
+ * summary. Idempotent per toolCallId (latest component wins).
250
+ */
251
+ export function noteTurnMemberRender(toolCallId: string, invalidate: () => void): void {
252
+ if (typeof invalidate !== "function") return;
253
+ invalidateByCallId.set(toolCallId, invalidate);
254
+ }
255
+
256
+ /**
257
+ * Force the just-finished turn's tool blocks to re-render (updateDisplay).
258
+ * Components that never rendered (headless/print) have no captured callback.
259
+ */
260
+ export function invalidateTurnMembers(turn: TurnState): void {
261
+ for (const member of turn.members) {
262
+ const invalidate = invalidateByCallId.get(member.toolCallId);
263
+ if (!invalidate) continue;
264
+ try {
265
+ invalidate();
266
+ } catch {
267
+ // A detached component must not break the turn-end path.
268
+ invalidateByCallId.delete(member.toolCallId);
269
+ }
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Freeze a member's wall-clock elapsed into the registry (idempotent; the
275
+ * value is frozen by the renderer state once the terminal result rendered).
276
+ */
277
+ export function noteTurnMemberElapsed(toolCallId: string, elapsedMs: number | undefined): void {
278
+ if (elapsedMs === undefined) return;
279
+ const entry = memberByCallId.get(toolCallId);
280
+ if (!entry || entry.member.elapsedMs !== undefined) return;
281
+ entry.member.elapsedMs = elapsedMs;
282
+ }
283
+
284
+ /** Per-tool summary phrasing: `Read 2 files` / `ran 4 shell commands`. */
285
+ const TURN_SUMMARY_STYLE: Readonly<Record<string, { readonly verb: string; readonly unit: string }>> = Object.freeze({
286
+ read: { verb: "Read", unit: "file" },
287
+ bash: { verb: "ran", unit: "shell command" },
288
+ ls: { verb: "Listed", unit: "path" },
289
+ find: { verb: "Found", unit: "file" },
290
+ grep: { verb: "Grepped", unit: "pattern" },
291
+ edit: { verb: "Edited", unit: "file" },
292
+ write: { verb: "Wrote", unit: "file" },
293
+ quick_edit: { verb: "Edited", unit: "file" },
294
+ substitute_edit: { verb: "Edited", unit: "file" },
295
+ target_edit: { verb: "Edited", unit: "file" },
296
+ });
297
+
298
+ export interface TurnSummaryParts {
299
+ /** `Read 2 files`, `ran 4 shell commands`, ... in first-use order. */
300
+ readonly parts: readonly string[];
301
+ readonly failedCount: number;
302
+ /** Sum of members' frozen elapsed; undefined when nothing was recorded. */
303
+ readonly elapsedMs: number | undefined;
304
+ }
305
+
306
+ /** Aggregate a turn's non-error members into summary parts (pure). */
307
+ export function turnSummaryParts(turn: TurnState): TurnSummaryParts {
308
+ const counts = new Map<string, number>();
309
+ const order: string[] = [];
310
+ let failedCount = 0;
311
+ let elapsedMs: number | undefined;
312
+ for (const member of turn.members) {
313
+ if (member.isError) {
314
+ failedCount++;
315
+ continue;
316
+ }
317
+ if (member.elapsedMs !== undefined) elapsedMs = (elapsedMs ?? 0) + member.elapsedMs;
318
+ const existing = counts.get(member.toolName);
319
+ if (existing === undefined) {
320
+ counts.set(member.toolName, 1);
321
+ order.push(member.toolName);
322
+ } else counts.set(member.toolName, existing + 1);
323
+ }
324
+ const parts = order.map((toolName) => {
325
+ const count = counts.get(toolName) ?? 0;
326
+ const style = TURN_SUMMARY_STYLE[toolName] ?? { verb: "ran", unit: `${toolName} call` };
327
+ return `${style.verb} ${count} ${pluralForm(style.unit, count)}`;
328
+ });
329
+ return { parts, failedCount, elapsedMs };
330
+ }
331
+
332
+ function formatTurnSummaryLine(theme: BoxTheme, turn: TurnState): string {
333
+ const summary = turnSummaryParts(turn);
334
+ // The summary is deliberately quiet: the whole line renders dim so completed
335
+ // tool work recedes behind the assistant's answer. Only the failed marker
336
+ // stays error-colored (errors must remain visible).
337
+ const parts = summary.parts.join(", ");
338
+ let line = `${theme.fg("dim", `➔ ${parts}`)}`;
339
+ if (summary.failedCount > 0)
340
+ line += theme.fg("error", ` · ${summary.failedCount} ${pluralForm("failed", summary.failedCount)}`);
341
+ if (summary.elapsedMs !== undefined) line += theme.fg("dim", ` · ${(summary.elapsedMs / 1000).toFixed(2)}s`);
342
+ return line;
343
+ }
344
+
345
+ /** Leader call component: renders the live turn summary line on every pass. */
346
+ export function renderTurnSummaryCall(theme: BoxTheme, turn: TurnState): Component {
347
+ return {
348
+ invalidate() {},
349
+ render(width: number): string[] {
350
+ return [safeTruncateToWidth(formatTurnSummaryLine(theme, turn), Math.max(1, width), "…")];
351
+ },
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Empty result component for the turn-summary leader. The summary lives in the
357
+ * call component; the result adds nothing. Deliberately NOT the shared
358
+ * EMPTY_BATCH_COMPONENT singleton, so the decoration's hideBatchMember
359
+ * (identity-compared) never hides the leader.
360
+ */
361
+ export function emptyTurnResult(): Component {
362
+ return {
363
+ invalidate() {},
364
+ render() {
365
+ return [];
366
+ },
367
+ };
368
+ }
@@ -113,6 +113,19 @@ const DEFAULT_SNAPSHOT: ToolDecorationSnapshot = Object.freeze({
113
113
  });
114
114
  let ownerGeneration = 0;
115
115
  const piStyleWrappers = new WeakSet<RenderFunction>();
116
+
117
+ /**
118
+ * The real Tui instance captured from decorated tool components (`instance.ui`),
119
+ * used to request a repaint after the turn registry flips a turn to collapsed
120
+ * (pi only re-paints after its own events; the renderer selectors re-run on
121
+ * updateDisplay, but the screen refresh still needs a requestRender).
122
+ */
123
+ let capturedToolUi: { requestRender?: (force?: boolean) => void } | undefined;
124
+
125
+ /** Request a screen repaint through the captured tool Tui (no-op headless). */
126
+ export function requestToolPresentationRender(): void {
127
+ capturedToolUi?.requestRender?.();
128
+ }
116
129
  let toolTestHooks: { defineProperty?: typeof Reflect.defineProperty; deleteProperty?: typeof Reflect.deleteProperty } =
117
130
  {};
118
131
  export function __setToolDecorationTestHooks(hooks: typeof toolTestHooks): () => void {
@@ -392,12 +405,14 @@ export function createToolDecorationOwner(snapshot: Partial<ToolDecorationSnapsh
392
405
  } else if (outcome === "later-owner") state.active.delete(record);
393
406
  else state.failed++;
394
407
  }
408
+ capturedToolUi = undefined;
395
409
  const archive = state.active.size === 0 ? finalize(state) : undefined;
396
410
  return { restored: state.restored, failed: state.failed, diagnostics: new Map(state.diagnostics), archive };
397
411
  };
398
412
  return Object.freeze({
399
413
  decorateToolRendererSelection(subtype: RendererSubtype, original: unknown, instance: object, args: unknown[]) {
400
414
  if (typeof original !== "function") return undefined;
415
+ capturedToolUi = (instance as { ui?: { requestRender?: (force?: boolean) => void } }).ui ?? capturedToolUi;
401
416
  const renderer = Reflect.apply(original, instance, args);
402
417
  if (state.snapshot.style === "compact-box") {
403
418
  const toolName = (instance as { toolName?: unknown }).toolName;
@@ -6,6 +6,7 @@ import {
6
6
  detectPiVersion,
7
7
  disposePiCompatibilityProbe,
8
8
  probePiCompatibility,
9
+ SUPPORTED_VERSION_RANGE,
9
10
  } from "./compatibility-probe.js";
10
11
 
11
12
  export interface CompatibilityCoordinator {
@@ -60,8 +61,10 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
60
61
  report?.unsupported.filter((item) =>
61
62
  feature === "messages" ? item.subtype.includes("message") : item.subtype.includes("tool"),
62
63
  ) ?? [];
63
- const failed = records.some((item) => /identity|failed|unsupported shape/i.test(item.reason));
64
- const fallback = records.some((item) => /fallback|authorization|disabled/i.test(item.reason));
64
+ // Identity drift degrades only the affected surface to native (graceful);
65
+ // a feature is only "failed" when an install/shape error occurred.
66
+ const failed = records.some((item) => /failed|rejected|rolled back|shape is not/i.test(item.reason));
67
+ const fallback = records.some((item) => /fallback|authorization|disabled|identity/i.test(item.reason));
65
68
  const authorized = Boolean(authorization?.core && surfaceAuthorized);
66
69
  const installedRecord = report?.recordSnapshots.some(
67
70
  (item) => item.feature === feature && !item.disposed && (subtype === undefined || item.subtype === subtype),
@@ -69,7 +72,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
69
72
  return {
70
73
  configured,
71
74
  authorized,
72
- installed: Boolean(installedRecord && !failed && authorized && configured),
75
+ installed: Boolean(installedRecord && authorized && configured),
73
76
  conflicted: records.some((item) => item.reason.includes("owner")),
74
77
  failed,
75
78
  cleanupPending,
@@ -81,12 +84,13 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
81
84
  configured: messagesConfigured || toolsConfigured,
82
85
  authorized: authorization?.core ?? false,
83
86
  installed: report !== undefined,
84
- conflicted: report?.unsupported.some((item) => item.reason.includes("identity")) ?? false,
85
- failed: report?.unsupported.some((item) => item.reason.includes("failed")) ?? false,
87
+ conflicted: report?.unsupported.some((item) => item.reason.includes("owner")) ?? false,
88
+ failed: report?.unsupported.some((item) => /failed|rejected|rolled back/i.test(item.reason)) ?? false,
86
89
  cleanupPending,
87
- nativeFallbacks: report?.unsupported.filter((item) => item.reason.includes("fallback")).length ?? 0,
90
+ nativeFallbacks: report?.unsupported.filter((item) => /fallback|identity/i.test(item.reason)).length ?? 0,
88
91
  piVersion: version.version ?? report?.piVersion ?? "unknown",
89
- versionRange: report?.versionRange ?? ">=0.83.0 <0.84.0",
92
+ versionRange: report?.versionRange ?? SUPPORTED_VERSION_RANGE,
93
+ supportedVersions: report?.supportedVersions ?? [],
90
94
  assistantMessage: surface(
91
95
  "messages",
92
96
  config.enabled && config.messages.enabled && config.messages.assistantPrefix,
@@ -105,11 +109,13 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
105
109
  const productDenied = productGate === "deny";
106
110
  if (cleanupPending && !report) cleanupPending = false;
107
111
  if (cleanupPending || !tui || !config.enabled || !authorization?.core || productDenied) return undefined;
108
- const certifiedHost = detectPiVersion().version === "0.83.0";
112
+ // Certification is per-surface identity (fingerprint) based, never pinned to
113
+ // a Pi version: authorization only needs the session flags + config. Version
114
+ // drift that preserves identities keeps working; changed identities degrade
115
+ // per-surface inside the probe.
109
116
  const assistantEnabled =
110
117
  authorization.assistant &&
111
118
  isTierCAuthorized({
112
- certifiedHost,
113
119
  coreFlag: authorization.core,
114
120
  surfaceFlag: true,
115
121
  surface: "assistantMessage",
@@ -118,7 +124,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
118
124
  const specialBlocksEnabled =
119
125
  authorization.specialBlocks &&
120
126
  isTierCAuthorized({
121
- certifiedHost,
122
127
  coreFlag: authorization.core,
123
128
  surfaceFlag: true,
124
129
  surface: "specialBlocks",
@@ -133,7 +138,6 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
133
138
  config.messages.enabled &&
134
139
  config.messages.hideThinkingLabel &&
135
140
  isTierCAuthorized({
136
- certifiedHost,
137
141
  coreFlag: authorization.core,
138
142
  surfaceFlag: true,
139
143
  surface: "messages",
@@ -142,7 +146,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
142
146
  );
143
147
  const toolsEnabled =
144
148
  authorization.tools &&
145
- isTierCAuthorized({ certifiedHost, coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
149
+ isTierCAuthorized({ coreFlag: authorization.core, surfaceFlag: true, surface: "tools", config });
146
150
  if (!messagesEnabled && !toolsEnabled) return undefined;
147
151
  const detected = detectPiVersion();
148
152
  report = probePiCompatibility(detected.version, {
@@ -164,6 +168,7 @@ export function createCompatibilityCoordinator(dispose = disposePiCompatibilityP
164
168
  assistantPrefix: authorization.ascii ? "[assistant] " : "│ ",
165
169
  assistantEnabled,
166
170
  collapseHiddenThinking: thinkingCollapseEnabled,
171
+ hideInterimText: config.messages.hideInterimText && messagesEnabled,
167
172
  },
168
173
  toolSnapshot: {
169
174
  callMarker: authorization.ascii ? "[tool] " : "[tool] ",