pi-cc-extensions 0.7.11 → 0.8.0

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,1136 @@
1
+ import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { AssistantMessageComponent, ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
3
+ import { Markdown, Spacer, Text } from "@earendil-works/pi-tui";
4
+ import { homedir } from "node:os";
5
+ import { sanitizeToolResultText } from "./tool-result-sanitize.ts";
6
+
7
+ /**
8
+ * Compact transcript rendering is adapted from pi-compact-transcript v0.6.2
9
+ * (MIT, Alan Hagedorn). This module is internal so claude-code-style.ts remains
10
+ * the package's only extension entry point.
11
+ */
12
+
13
+ export type CompactStyleMode = "on" | "off" | "compact";
14
+
15
+ export type CompactStyleHost = {
16
+ getMode: () => CompactStyleMode;
17
+ getExcludeRenderers: () => readonly string[];
18
+ };
19
+
20
+ export type CompactRendererRoute = "claude" | "native" | "compact";
21
+
22
+ type ToolPrototypePatch = {
23
+ prototype: any;
24
+ active: boolean;
25
+ originalUpdateDisplay: (...args: any[]) => any;
26
+ originalRender: (...args: any[]) => any;
27
+ patchedUpdateDisplay: (...args: any[]) => any;
28
+ patchedRender: (...args: any[]) => any;
29
+ };
30
+
31
+ type AssistantPrototypePatch = {
32
+ prototype: any;
33
+ active: boolean;
34
+ originalUpdateContent: (...args: any[]) => any;
35
+ patchedUpdateContent: (...args: any[]) => any;
36
+ };
37
+
38
+ type CompactInstallation = {
39
+ active: boolean;
40
+ toolPatch?: ToolPrototypePatch;
41
+ assistantPatch?: AssistantPrototypePatch;
42
+ };
43
+
44
+ export function rendererRoute(
45
+ mode: CompactStyleMode,
46
+ toolName: string,
47
+ excludeRenderers: readonly string[] = [],
48
+ ): CompactRendererRoute {
49
+ if (toolName === "Agent" || excludeRenderers.includes(toolName)) return "native";
50
+ if (mode === "compact") return "compact";
51
+ if (mode === "on") return "claude";
52
+ return "native";
53
+ }
54
+
55
+ export function shouldUseCompactRenderer(
56
+ mode: CompactStyleMode,
57
+ toolName: string,
58
+ excludeRenderers: readonly string[] = [],
59
+ ): boolean {
60
+ return rendererRoute(mode, toolName, excludeRenderers) === "compact";
61
+ }
62
+
63
+ type ToolInfo = {
64
+ id: string;
65
+ name: string;
66
+ args: any;
67
+ preview: string;
68
+ hidden?: boolean;
69
+ running?: boolean;
70
+ burstCount?: number;
71
+ startedAt?: number;
72
+ durationMs?: number;
73
+ burstDurationMs?: number;
74
+ result?: string;
75
+ isError?: boolean;
76
+ invalidate?: () => void;
77
+ };
78
+
79
+ type RunStats = {
80
+ startedAt: number;
81
+ toolCount: number;
82
+ readFiles: Set<string>;
83
+ editFiles: Set<string>;
84
+ commandCount: number;
85
+ otherCount: number;
86
+ failedCount: number;
87
+ };
88
+
89
+ export type CompactSummaryData = {
90
+ reads: number;
91
+ edits: number;
92
+ commands: number;
93
+ others: number;
94
+ failed: number;
95
+ durationMs: number;
96
+ };
97
+
98
+ type RuntimeState = {
99
+ getMode: () => CompactStyleMode;
100
+ getExcludeRenderers: () => readonly string[];
101
+ pi?: ExtensionAPI;
102
+ toolsById: Map<string, ToolInfo>;
103
+ currentBurst: ToolInfo[];
104
+ hiddenToolIds: Set<string>;
105
+ runningToolIds: Set<string>;
106
+ agentActive: boolean;
107
+ blinkOn: boolean;
108
+ blinkTimer?: ReturnType<typeof setInterval>;
109
+ runStats: RunStats;
110
+ toolComponents: Set<any>;
111
+ assistantComponents: Set<any>;
112
+ currentTheme?: Theme;
113
+ lastTui?: any;
114
+ thinkingHidden: boolean;
115
+ currentThoughtHeading?: string;
116
+ thoughtAnchorId?: string;
117
+ activeInstallation?: CompactInstallation;
118
+ };
119
+
120
+ const SUMMARY_ENTRY_TYPE = "compact-transcript-summary";
121
+ const STATUS_KEY = "compact-transcript";
122
+ const MIN_PREVIEW_WIDTH = 20;
123
+ const MAX_PREVIEW_WIDTH = 104;
124
+ // Leave room for pi's row gutter/padding so compact lines never wrap.
125
+ const PREVIEW_MARGIN = 6;
126
+ const BLINK_INTERVAL_MS = 400;
127
+ // Status marker is two cells wide ("◆ ").
128
+ const MARKER_WIDTH = 2;
129
+ const STATE_KEY = Symbol.for("pi.ccstyle.compact-style.state");
130
+ const TOOL_PATCH_KEY = Symbol.for("pi.ccstyle.compact-style.tool-patch");
131
+ const ASSISTANT_PATCH_KEY = Symbol.for("pi.ccstyle.compact-style.assistant-patch");
132
+ const TOOL_NATIVE_CHILD_KEY = Symbol.for("pi.ccstyle.compact-style.native-tool-child");
133
+
134
+ function newRunStats(): RunStats {
135
+ return {
136
+ startedAt: Date.now(),
137
+ toolCount: 0,
138
+ readFiles: new Set(),
139
+ editFiles: new Set(),
140
+ commandCount: 0,
141
+ otherCount: 0,
142
+ failedCount: 0,
143
+ };
144
+ }
145
+
146
+ function getState(): RuntimeState {
147
+ const globalWithState = globalThis as typeof globalThis & { [STATE_KEY]?: RuntimeState };
148
+ globalWithState[STATE_KEY] ??= {
149
+ getMode: () => "on",
150
+ getExcludeRenderers: () => [],
151
+ toolsById: new Map(),
152
+ currentBurst: [],
153
+ hiddenToolIds: new Set(),
154
+ runningToolIds: new Set(),
155
+ agentActive: false,
156
+ blinkOn: true,
157
+ runStats: newRunStats(),
158
+ toolComponents: new Set(),
159
+ assistantComponents: new Set(),
160
+ thinkingHidden: true,
161
+ };
162
+ return globalWithState[STATE_KEY]!;
163
+ }
164
+
165
+ const state = getState();
166
+
167
+ function currentMode(): CompactStyleMode {
168
+ try {
169
+ const mode = state.getMode();
170
+ return mode === "on" || mode === "off" || mode === "compact" ? mode : "on";
171
+ } catch {
172
+ return "off";
173
+ }
174
+ }
175
+
176
+ function isCompact(): boolean {
177
+ return currentMode() === "compact";
178
+ }
179
+
180
+ function isNonEmptyString(value: unknown): value is string {
181
+ return typeof value === "string" && value.trim().length > 0;
182
+ }
183
+
184
+ function shortenPath(path: unknown): string {
185
+ if (typeof path !== "string" || !path) return "";
186
+ const clean = sanitizeToolResultText(path);
187
+ const home = homedir();
188
+ return clean.startsWith(home) ? `~${clean.slice(home.length)}` : clean;
189
+ }
190
+
191
+ function oneLine(value: unknown): string {
192
+ return sanitizeToolResultText(String(value ?? ""))
193
+ .replace(/\s+/g, " ")
194
+ .trim();
195
+ }
196
+
197
+ function previewWidth(base = process.stdout.columns || 100): number {
198
+ return Math.max(MIN_PREVIEW_WIDTH, Math.min(MAX_PREVIEW_WIDTH, base - PREVIEW_MARGIN));
199
+ }
200
+
201
+ function limitPlain(text: string, max = previewWidth()): string {
202
+ const clean = oneLine(text);
203
+ if (clean.length <= max) return clean;
204
+ return `${clean.slice(0, Math.max(0, max - 1))}…`;
205
+ }
206
+
207
+ function quote(s: string): string {
208
+ return JSON.stringify(s);
209
+ }
210
+
211
+ function safeJson(value: unknown): string {
212
+ try {
213
+ return JSON.stringify(value ?? {});
214
+ } catch {
215
+ return sanitizeToolResultText(String(value));
216
+ }
217
+ }
218
+
219
+ /** Durations below one second are intentionally omitted. */
220
+ export function formatDuration(ms: number): string {
221
+ if (!Number.isFinite(ms) || ms < 1000) return "";
222
+ const totalSeconds = Math.round(ms / 1000);
223
+ if (totalSeconds < 60) return `${totalSeconds}s`;
224
+ const minutes = Math.floor(totalSeconds / 60);
225
+ const seconds = totalSeconds % 60;
226
+ return seconds ? `${minutes}m${seconds}s` : `${minutes}m`;
227
+ }
228
+
229
+ const PREFERRED_ARG_KEYS = [
230
+ "command",
231
+ "code",
232
+ "query",
233
+ "pattern",
234
+ "path",
235
+ "file_path",
236
+ "filePath",
237
+ "file",
238
+ "url",
239
+ "prompt",
240
+ "text",
241
+ "description",
242
+ "name",
243
+ ];
244
+ const PATH_ARG_KEYS = new Set(["path", "file_path", "filePath", "file"]);
245
+
246
+ export function previewFor(name: string, args: any): string {
247
+ switch (name) {
248
+ case "bash":
249
+ return `$ ${oneLine(args?.command || "...")}`;
250
+ case "read": {
251
+ let out = `read ${shortenPath(args?.path) || "..."}`;
252
+ if (args?.offset !== undefined || args?.limit !== undefined) {
253
+ const start = args.offset ?? 1;
254
+ const end = args.limit !== undefined ? start + args.limit - 1 : "";
255
+ out += `:${start}${end ? `-${end}` : ""}`;
256
+ }
257
+ return out;
258
+ }
259
+ case "write": {
260
+ const lines = typeof args?.content === "string" ? args.content.split("\n").length : 0;
261
+ return `write ${shortenPath(args?.path) || "..."}${lines ? ` (${lines} lines)` : ""}`;
262
+ }
263
+ case "edit": {
264
+ const edits = Array.isArray(args?.edits) ? args.edits.length : 0;
265
+ return `edit ${shortenPath(args?.path) || "..."}${edits > 1 ? ` (${edits} edits)` : ""}`;
266
+ }
267
+ case "grep": {
268
+ const pattern = args?.pattern ? quote(String(args.pattern)) : "...";
269
+ const path = shortenPath(args?.path) || ".";
270
+ return `grep ${pattern} ${path}${args?.glob ? ` (${args.glob})` : ""}`;
271
+ }
272
+ case "find":
273
+ return `find ${args?.pattern ? quote(String(args.pattern)) : "..."} ${shortenPath(args?.path) || "."}`;
274
+ case "ls":
275
+ return `ls ${shortenPath(args?.path) || "."}`;
276
+ default: {
277
+ if (args && typeof args === "object") {
278
+ for (const key of PREFERRED_ARG_KEYS) {
279
+ const value = (args as Record<string, unknown>)[key];
280
+ if (isNonEmptyString(value)) {
281
+ const rendered = PATH_ARG_KEYS.has(key) ? shortenPath(value) : oneLine(value);
282
+ return `${name} ${rendered}`;
283
+ }
284
+ }
285
+ const firstString = Object.values(args).find(isNonEmptyString);
286
+ if (firstString) return `${name} ${oneLine(firstString)}`;
287
+ }
288
+ return `${name} ${safeJson(args ?? {})}`;
289
+ }
290
+ }
291
+ }
292
+
293
+ export function resultPreview(result: any, isPartial = false): string {
294
+ const text = Array.isArray(result?.content)
295
+ ? result.content.find((c: any) => c?.type === "text" && typeof c.text === "string")?.text
296
+ : undefined;
297
+ if (!text) return isPartial ? "running" : "";
298
+ const lines = sanitizeToolResultText(text).trim().split("\n").filter((line) => line.trim().length > 0);
299
+ if (lines.length === 0) return isPartial ? "running" : "";
300
+ if (lines.length === 1) return lines[0];
301
+ return `${lines.length} lines`;
302
+ }
303
+
304
+ function resetToolRun() {
305
+ state.toolsById = new Map();
306
+ state.currentBurst = [];
307
+ state.hiddenToolIds = new Set();
308
+ state.runningToolIds = new Set();
309
+ state.currentThoughtHeading = undefined;
310
+ state.thoughtAnchorId = undefined;
311
+ stopBlinkTimer();
312
+ }
313
+
314
+ function clearSessionState() {
315
+ resetToolRun();
316
+ state.agentActive = false;
317
+ state.runStats = newRunStats();
318
+ state.toolComponents = new Set();
319
+ state.assistantComponents = new Set();
320
+ state.currentTheme = undefined;
321
+ state.lastTui = undefined;
322
+ state.thinkingHidden = true;
323
+ }
324
+
325
+ function captureTheme(ctx: ExtensionContext) {
326
+ state.currentTheme = ctx.ui.theme;
327
+ }
328
+
329
+ function setToolHidden(info: ToolInfo, hidden: boolean) {
330
+ info.hidden = hidden;
331
+ if (hidden) state.hiddenToolIds.add(info.id);
332
+ else state.hiddenToolIds.delete(info.id);
333
+ }
334
+
335
+ function restoreBurstSegment(tools: ToolInfo[], changed: Set<ToolInfo>): void {
336
+ const lastIndex = tools.length - 1;
337
+ for (let index = 0; index < tools.length; index++) {
338
+ const tool = tools[index]!;
339
+ const hidden = index !== lastIndex;
340
+ const burstCount = hidden ? 1 : tools.length;
341
+ if (tool.hidden !== hidden || tool.burstCount !== burstCount) changed.add(tool);
342
+ setToolHidden(tool, hidden);
343
+ tool.burstCount = burstCount;
344
+ }
345
+ }
346
+
347
+ function markToolFailed(info: ToolInfo): void {
348
+ const burst = state.currentBurst;
349
+ const failedIndex = burst.findIndex((tool) => tool.id === info.id);
350
+ const changed = new Set<ToolInfo>();
351
+
352
+ // Publish the boundary before invalidating peers: their updateDisplay calls
353
+ // must not be able to rejoin the failed burst.
354
+ state.currentBurst = [];
355
+ if (failedIndex >= 0) {
356
+ restoreBurstSegment(burst.slice(0, failedIndex), changed);
357
+ restoreBurstSegment(burst.slice(failedIndex + 1), changed);
358
+ }
359
+
360
+ info.isError = true;
361
+ info.burstCount = 1;
362
+ setToolHidden(info, false);
363
+ for (const tool of changed) {
364
+ if (tool !== info) tool.invalidate?.();
365
+ }
366
+ }
367
+
368
+ function applyResult(info: ToolInfo, result: any, isError: boolean, isPartial: boolean) {
369
+ const suffix = resultPreview(result, isPartial);
370
+ if (suffix) info.result = suffix;
371
+ if (isError && (!info.isError || state.currentBurst.includes(info))) markToolFailed(info);
372
+ }
373
+
374
+ function ensureBlinkTimer() {
375
+ if (state.blinkTimer || state.runningToolIds.size === 0) return;
376
+ state.blinkTimer = setInterval(() => {
377
+ if (state.runningToolIds.size === 0) {
378
+ stopBlinkTimer();
379
+ return;
380
+ }
381
+ state.blinkOn = !state.blinkOn;
382
+ for (const id of state.runningToolIds) state.toolsById.get(id)?.invalidate?.();
383
+ }, BLINK_INTERVAL_MS);
384
+ state.blinkTimer.unref?.();
385
+ }
386
+
387
+ function stopBlinkTimer() {
388
+ if (state.blinkTimer) clearInterval(state.blinkTimer);
389
+ state.blinkTimer = undefined;
390
+ state.blinkOn = true;
391
+ }
392
+
393
+ function statusMarker(theme: Theme, opts: { running?: boolean; isError?: boolean; hasResult?: boolean }): string {
394
+ if (opts.isError) return theme.fg("error", "◆ ");
395
+ if (opts.running) return theme.fg("dim", state.blinkOn ? "◆ " : "◇ ");
396
+ if (opts.hasResult) return theme.fg("success", "◆ ");
397
+ return theme.fg("dim", "◆ ");
398
+ }
399
+
400
+ function textSignalHasVisibleContent(assistantMessageEvent: any): boolean {
401
+ const type = assistantMessageEvent?.type;
402
+ if (type === "text_delta") {
403
+ return typeof assistantMessageEvent.delta === "string" && assistantMessageEvent.delta.trim().length > 0;
404
+ }
405
+ if (type === "text_end") {
406
+ return typeof assistantMessageEvent.content === "string" && assistantMessageEvent.content.trim().length > 0;
407
+ }
408
+ return false;
409
+ }
410
+
411
+ function thoughtTickerEnabled(): boolean {
412
+ return isCompact() && state.thinkingHidden;
413
+ }
414
+
415
+ function cleanThoughtHeading(line: string): string {
416
+ let clean = oneLine(line)
417
+ .replace(/^#{1,6}\s+/, "")
418
+ .replace(/^[-*]\s+/, "")
419
+ .trim();
420
+ clean = clean
421
+ .replace(/^\*\*(.+)\*\*$/, "$1")
422
+ .replace(/^__(.+)__$/, "$1")
423
+ .replace(/^`(.+)`$/, "$1");
424
+ return clean.trim();
425
+ }
426
+
427
+ function extractThoughtHeading(text: unknown): string {
428
+ if (typeof text !== "string") return "";
429
+ const firstLine = text.split(/\r?\n/).find((line) => line.trim().length > 0);
430
+ return firstLine ? cleanThoughtHeading(firstLine) : "";
431
+ }
432
+
433
+ function latestThoughtHeading(message: any): string {
434
+ if (!Array.isArray(message?.content)) return "";
435
+ for (let i = message.content.length - 1; i >= 0; i--) {
436
+ const content = message.content[i];
437
+ if (content?.type === "thinking") return extractThoughtHeading(content.thinking);
438
+ }
439
+ return "";
440
+ }
441
+
442
+ function invalidateToolById(id: string | undefined) {
443
+ if (!id) return;
444
+ state.toolsById.get(id)?.invalidate?.();
445
+ }
446
+
447
+ function latestVisibleTool(): ToolInfo | undefined {
448
+ return Array.from(state.toolsById.values())
449
+ .reverse()
450
+ .find((tool) => !tool.hidden);
451
+ }
452
+
453
+ function clearCurrentThought() {
454
+ if (!state.currentThoughtHeading && !state.thoughtAnchorId) return;
455
+ const previousAnchorId = state.thoughtAnchorId;
456
+ state.currentThoughtHeading = undefined;
457
+ state.thoughtAnchorId = undefined;
458
+ invalidateToolById(previousAnchorId);
459
+ }
460
+
461
+ function setCurrentThought(heading: string) {
462
+ const nextHeading = oneLine(heading);
463
+ if (!thoughtTickerEnabled() || !nextHeading) {
464
+ clearCurrentThought();
465
+ return;
466
+ }
467
+
468
+ const previousAnchorId = state.thoughtAnchorId;
469
+ const nextAnchorId = latestVisibleTool()?.id;
470
+ const changed = state.currentThoughtHeading !== nextHeading || previousAnchorId !== nextAnchorId;
471
+ state.currentThoughtHeading = nextHeading;
472
+ state.thoughtAnchorId = nextAnchorId;
473
+ if (!changed) return;
474
+
475
+ invalidateToolById(previousAnchorId);
476
+ if (nextAnchorId !== previousAnchorId) invalidateToolById(nextAnchorId);
477
+ }
478
+
479
+ function updateCurrentThoughtFromMessage(message: any) {
480
+ const heading = latestThoughtHeading(message);
481
+ // Keep the previous heading while a replacement thinking block is empty.
482
+ if (heading) setCurrentThought(heading);
483
+ }
484
+
485
+ function anchorCurrentThoughtTo(info: ToolInfo) {
486
+ if (!thoughtTickerEnabled() || !state.currentThoughtHeading || state.thoughtAnchorId === info.id) return;
487
+ const previousAnchorId = state.thoughtAnchorId;
488
+ state.thoughtAnchorId = info.id;
489
+ invalidateToolById(previousAnchorId);
490
+ info.invalidate?.();
491
+ }
492
+
493
+ function currentThoughtLine(toolCallId: string, theme: Theme): string {
494
+ if (!thoughtTickerEnabled() || state.thoughtAnchorId !== toolCallId || !state.currentThoughtHeading) return "";
495
+ const prefix = " ↳ ";
496
+ const budget = previewWidth((process.stdout.columns || 100) - prefix.length);
497
+ return theme.fg("dim", prefix) + theme.fg("thinkingText", limitPlain(state.currentThoughtHeading, budget));
498
+ }
499
+
500
+ function upsertToolInfo(id: string, name: string, args: any, invalidate?: () => void): ToolInfo {
501
+ let info = state.toolsById.get(id);
502
+ if (!info) {
503
+ info = { id, name, args, preview: previewFor(name, args) };
504
+ state.toolsById.set(id, info);
505
+ }
506
+ info.name = name;
507
+ info.args = args;
508
+ info.preview = previewFor(name, args);
509
+ if (invalidate) info.invalidate = invalidate;
510
+ return info;
511
+ }
512
+
513
+ function recordToolStart(name: string, args: any) {
514
+ const base = name.split(".").pop() ?? name;
515
+ state.runStats.toolCount++;
516
+ if (base === "read") {
517
+ if (isNonEmptyString(args?.path)) state.runStats.readFiles.add(args.path);
518
+ } else if (base === "edit" || base === "write") {
519
+ if (isNonEmptyString(args?.path)) state.runStats.editFiles.add(args.path);
520
+ } else if (base === "bash") {
521
+ state.runStats.commandCount++;
522
+ } else {
523
+ state.runStats.otherCount++;
524
+ }
525
+ }
526
+
527
+ function joinBurst(info: ToolInfo) {
528
+ const previous = state.currentBurst[state.currentBurst.length - 1];
529
+
530
+ if (!isCompact()) {
531
+ state.currentBurst = [];
532
+ return;
533
+ }
534
+
535
+ // Only consecutive uses of the same tool are grouped.
536
+ if (state.currentBurst.length && state.currentBurst[state.currentBurst.length - 1].name !== info.name) {
537
+ state.currentBurst = [];
538
+ }
539
+
540
+ if (!state.currentBurst.some((tool) => tool.id === info.id)) state.currentBurst.push(info);
541
+ for (const tool of state.currentBurst.slice(0, -1)) setToolHidden(tool, true);
542
+ info.burstCount = state.currentBurst.length;
543
+ previous?.invalidate?.();
544
+ }
545
+
546
+ function beginTool(id: string, name: string, args: any) {
547
+ const info = upsertToolInfo(id, name, args);
548
+ setToolHidden(info, false);
549
+ info.burstCount = 1;
550
+ info.running = true;
551
+ info.isError = false;
552
+ info.startedAt = Date.now();
553
+ state.runningToolIds.add(id);
554
+ ensureBlinkTimer();
555
+ recordToolStart(name, args);
556
+ joinBurst(info);
557
+ anchorCurrentThoughtTo(info);
558
+ info.invalidate?.();
559
+ }
560
+
561
+ // Rebuilds do not emit tool_execution_start, so reconstruct burst grouping from
562
+ // the persisted transcript without treating old tools as running.
563
+ function hydrateTool(id: string, name: string, args: any, isError: boolean): ToolInfo {
564
+ const info = upsertToolInfo(id, name, args);
565
+ setToolHidden(info, false);
566
+ info.burstCount = 1;
567
+ if (isError) {
568
+ markToolFailed(info);
569
+ return info;
570
+ }
571
+ joinBurst(info);
572
+ return info;
573
+ }
574
+
575
+ function updateToolResult(toolCallId: string, result: any, isError = false, isPartial = false) {
576
+ if (!isPartial) {
577
+ state.runningToolIds.delete(toolCallId);
578
+ if (state.runningToolIds.size === 0) stopBlinkTimer();
579
+ }
580
+ const info = state.toolsById.get(toolCallId);
581
+ if (!info) return;
582
+ if (!isPartial) {
583
+ info.running = false;
584
+ if (info.startedAt) info.durationMs = Date.now() - info.startedAt;
585
+ if (state.currentBurst.includes(info) && state.currentBurst.length > 1) {
586
+ info.burstDurationMs = state.currentBurst.reduce((total, tool) => total + (tool.durationMs ?? 0), 0);
587
+ }
588
+ if (isError) state.runStats.failedCount++;
589
+ }
590
+ applyResult(info, result, isError, isPartial);
591
+ info.invalidate?.();
592
+ }
593
+
594
+ function compactToolLine(
595
+ toolCallId: string,
596
+ name: string,
597
+ args: any,
598
+ theme: Theme,
599
+ invalidate?: () => void,
600
+ result?: any,
601
+ isError = false,
602
+ isPartial = false,
603
+ ): string {
604
+ if (!state.toolsById.has(toolCallId)) hydrateTool(toolCallId, name, args, isError);
605
+ const info = upsertToolInfo(toolCallId, name, args, invalidate);
606
+ applyResult(info, result, isError, isPartial);
607
+ if (info.hidden) return "";
608
+
609
+ const isBurst = (info.burstCount ?? 1) > 1;
610
+ const durationText = formatDuration((isBurst ? (info.burstDurationMs ?? info.durationMs) : info.durationMs) ?? 0);
611
+ const inner = [info.result ? oneLine(info.result) : "", durationText].filter(Boolean).join(" · ");
612
+ const status = inner ? ` {${inner}}` : info.running ? " {running}" : "";
613
+ const details = `${info.preview}${status}`;
614
+ const marker = statusMarker(theme, {
615
+ running: info.running,
616
+ isError: info.isError,
617
+ hasResult: result != null || !!info.result,
618
+ });
619
+ if (!isBurst) return marker + theme.fg("muted", limitPlain(details));
620
+
621
+ const prefix = `${info.burstCount}× `;
622
+ const budget = previewWidth((process.stdout.columns || 100) - prefix.length - MARKER_WIDTH);
623
+ return marker + theme.fg("muted", prefix + limitPlain(details, budget));
624
+ }
625
+
626
+ function shouldCompactComponent(component: any, mode: CompactStyleMode): boolean {
627
+ const name = String(component?.toolName || component?.toolDefinition?.name || "");
628
+ let exclusions: readonly string[] = [];
629
+ try {
630
+ exclusions = state.getExcludeRenderers();
631
+ } catch {
632
+ // A failed/unloaded host must fall back to the native renderer.
633
+ return false;
634
+ }
635
+ return shouldUseCompactRenderer(mode, name, exclusions);
636
+ }
637
+
638
+ function nativeToolShellChild(component: any): any {
639
+ const extensionDefinition = component.toolDefinition;
640
+ const builtInDefinition = component.builtInToolDefinition;
641
+ if (!extensionDefinition && !builtInDefinition) return component.contentText;
642
+
643
+ let shell: "default" | "self";
644
+ if (!builtInDefinition) {
645
+ shell = extensionDefinition?.renderShell ?? "default";
646
+ } else if (!extensionDefinition) {
647
+ shell = builtInDefinition.renderShell ?? "default";
648
+ } else {
649
+ shell = extensionDefinition.renderShell ?? builtInDefinition.renderShell ?? "default";
650
+ }
651
+ return shell === "self" ? component.selfRenderContainer : component.contentBox;
652
+ }
653
+
654
+ function toolShellChildren(component: any): any[] {
655
+ return [component.contentText, component.contentBox, component.selfRenderContainer].filter(Boolean);
656
+ }
657
+
658
+ function syncToolShellChild(component: any, target: any, preferredIndex?: number): void {
659
+ if (!target || !Array.isArray(component.children)) return;
660
+ const candidates = new Set(toolShellChildren(component));
661
+ const shellIndexes = component.children
662
+ .map((child: any, index: number) => candidates.has(child) ? index : -1)
663
+ .filter((index: number) => index >= 0);
664
+ const targetIndex = preferredIndex !== undefined && shellIndexes.includes(preferredIndex)
665
+ ? preferredIndex
666
+ : shellIndexes[0];
667
+ if (targetIndex === undefined) {
668
+ if (preferredIndex !== undefined && preferredIndex >= 0 && preferredIndex <= component.children.length) {
669
+ component.children.splice(preferredIndex, 0, target);
670
+ } else if (typeof component.addChild === "function") {
671
+ component.addChild(target);
672
+ }
673
+ return;
674
+ }
675
+
676
+ component.children[targetIndex] = target;
677
+ for (const index of shellIndexes.sort((left: number, right: number) => right - left)) {
678
+ if (index !== targetIndex) component.children.splice(index, 1);
679
+ }
680
+ }
681
+
682
+ function rememberNativeToolShell(component: any): void {
683
+ if (component[TOOL_NATIVE_CHILD_KEY]) return;
684
+ const child = nativeToolShellChild(component);
685
+ const candidates = new Set(toolShellChildren(component));
686
+ const index = Array.isArray(component.children)
687
+ ? component.children.findIndex((entry: any) => candidates.has(entry))
688
+ : -1;
689
+ component[TOOL_NATIVE_CHILD_KEY] = { child, index };
690
+ }
691
+
692
+ function restoreNativeToolShell(component: any): void {
693
+ const saved = component[TOOL_NATIVE_CHILD_KEY] as { child: any; index: number } | undefined;
694
+ const child = saved?.child ?? nativeToolShellChild(component);
695
+ syncToolShellChild(component, child, saved?.index);
696
+ delete component[TOOL_NATIVE_CHILD_KEY];
697
+ }
698
+
699
+ function patchToolExecutionComponent(installation: CompactInstallation): ToolPrototypePatch | undefined {
700
+ const prototype = ToolExecutionComponent.prototype as any;
701
+ if (typeof prototype.updateDisplay !== "function" || typeof prototype.render !== "function") return undefined;
702
+
703
+ const patch: ToolPrototypePatch = {
704
+ prototype,
705
+ active: true,
706
+ originalUpdateDisplay: prototype.updateDisplay,
707
+ originalRender: prototype.render,
708
+ patchedUpdateDisplay: undefined as any,
709
+ patchedRender: undefined as any,
710
+ };
711
+
712
+ patch.patchedUpdateDisplay = function patchedUpdateDisplay(this: any) {
713
+ if (!patch.active || !installation.active || state.activeInstallation !== installation) {
714
+ this.__compactStyleForceSelf = false;
715
+ this.__compactStyleHidden = false;
716
+ restoreNativeToolShell(this);
717
+ return patch.originalUpdateDisplay.call(this);
718
+ }
719
+
720
+ state.toolComponents.add(this);
721
+ state.lastTui = this.ui;
722
+ const mode = currentMode();
723
+ const useCompact = shouldCompactComponent(this, mode);
724
+ if (!useCompact || this.expanded) {
725
+ this.__compactStyleForceSelf = false;
726
+ this.__compactStyleHidden = false;
727
+ if (mode === "off" || mode === "compact" || this[TOOL_NATIVE_CHILD_KEY]) {
728
+ restoreNativeToolShell(this);
729
+ }
730
+ return patch.originalUpdateDisplay.call(this);
731
+ }
732
+ if (!this.toolCallId || !this.toolName || !this.selfRenderContainer || typeof this.selfRenderContainer.clear !== "function") {
733
+ this.__compactStyleForceSelf = false;
734
+ this.__compactStyleHidden = false;
735
+ restoreNativeToolShell(this);
736
+ return patch.originalUpdateDisplay.call(this);
737
+ }
738
+
739
+ const theme = state.currentTheme;
740
+ if (!theme) {
741
+ this.__compactStyleForceSelf = false;
742
+ this.__compactStyleHidden = false;
743
+ restoreNativeToolShell(this);
744
+ return patch.originalUpdateDisplay.call(this);
745
+ }
746
+
747
+ const invalidate = () => {
748
+ this.invalidate();
749
+ this.ui?.requestRender?.();
750
+ };
751
+ if (!state.toolsById.has(this.toolCallId)) {
752
+ hydrateTool(this.toolCallId, this.toolName, this.args, this.result?.isError ?? false);
753
+ }
754
+ const info = upsertToolInfo(this.toolCallId, this.toolName, this.args, invalidate);
755
+ applyResult(info, this.result, this.result?.isError ?? false, this.isPartial);
756
+
757
+ this.__compactStyleForceSelf = true;
758
+ this.__compactStyleHidden = false;
759
+ rememberNativeToolShell(this);
760
+ syncToolShellChild(this, this.selfRenderContainer);
761
+ this.selfRenderContainer.clear();
762
+ for (const image of this.imageComponents ?? []) this.removeChild?.(image);
763
+ for (const spacer of this.imageSpacers ?? []) this.removeChild?.(spacer);
764
+ this.imageComponents = [];
765
+ this.imageSpacers = [];
766
+
767
+ const line = compactToolLine(
768
+ this.toolCallId,
769
+ this.toolName,
770
+ this.args,
771
+ theme,
772
+ invalidate,
773
+ this.result,
774
+ this.result?.isError ?? false,
775
+ this.isPartial,
776
+ );
777
+ if (!line) {
778
+ this.__compactStyleHidden = true;
779
+ return;
780
+ }
781
+
782
+ this.selfRenderContainer.addChild(new Text(line, 0, 0));
783
+ const thoughtLine = currentThoughtLine(this.toolCallId, theme);
784
+ if (thoughtLine) this.selfRenderContainer.addChild(new Text(thoughtLine, 0, 0));
785
+ };
786
+
787
+ patch.patchedRender = function patchedRender(this: any, width: number) {
788
+ if (!patch.active || !installation.active || state.activeInstallation !== installation) {
789
+ return patch.originalRender.call(this, width);
790
+ }
791
+ if (this.hideComponent || this.__compactStyleHidden) return [];
792
+ if (this.__compactStyleForceSelf) return this.selfRenderContainer.render(width);
793
+ return patch.originalRender.call(this, width);
794
+ };
795
+
796
+ prototype.updateDisplay = patch.patchedUpdateDisplay;
797
+ prototype.render = patch.patchedRender;
798
+ prototype[TOOL_PATCH_KEY] = patch;
799
+ return patch;
800
+ }
801
+
802
+ function appendNativeStopReason(component: any, message: any, theme: Theme): void {
803
+ const hasToolCalls = message.content.some((content: any) => content.type === "toolCall");
804
+ component.hasToolCalls = hasToolCalls;
805
+ if (message.stopReason === "length") {
806
+ component.contentContainer.addChild(new Spacer(1));
807
+ component.contentContainer.addChild(new Text(
808
+ theme.fg("error", "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete."),
809
+ component.outputPad,
810
+ 0,
811
+ ));
812
+ } else if (!hasToolCalls) {
813
+ if (message.stopReason === "aborted") {
814
+ const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
815
+ ? message.errorMessage
816
+ : "Operation aborted";
817
+ component.contentContainer.addChild(new Spacer(1));
818
+ component.contentContainer.addChild(new Text(theme.fg("error", abortMessage), component.outputPad, 0));
819
+ } else if (message.stopReason === "error") {
820
+ const errorMessage = message.errorMessage || "Unknown error";
821
+ component.contentContainer.addChild(new Spacer(1));
822
+ component.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), component.outputPad, 0));
823
+ }
824
+ }
825
+ }
826
+
827
+ function patchAssistantMessageComponent(installation: CompactInstallation): AssistantPrototypePatch | undefined {
828
+ const prototype = AssistantMessageComponent.prototype as any;
829
+ if (typeof prototype.updateContent !== "function") return undefined;
830
+
831
+ const patch: AssistantPrototypePatch = {
832
+ prototype,
833
+ active: true,
834
+ originalUpdateContent: prototype.updateContent,
835
+ patchedUpdateContent: undefined as any,
836
+ };
837
+
838
+ patch.patchedUpdateContent = function patchedUpdateContent(this: any, message: any) {
839
+ if (!patch.active || !installation.active || state.activeInstallation !== installation) {
840
+ return patch.originalUpdateContent.call(this, message);
841
+ }
842
+
843
+ state.assistantComponents.add(this);
844
+ state.thinkingHidden = !!this.hideThinkingBlock;
845
+ const mode = currentMode();
846
+ if (mode !== "compact" || !this.hideThinkingBlock || !Array.isArray(message?.content)) {
847
+ if (mode !== "compact") clearCurrentThought();
848
+ return patch.originalUpdateContent.call(this, message);
849
+ }
850
+ const theme = state.currentTheme;
851
+ if (!theme || !this.contentContainer || typeof this.contentContainer.clear !== "function") {
852
+ return patch.originalUpdateContent.call(this, message);
853
+ }
854
+
855
+ // Native children are not tagged as text vs. thinking, so removing only the
856
+ // thinking children after native rendering is not safe. Mirror pi 0.80.10's
857
+ // text and stop-reason paths exactly, while intentionally omitting thinking.
858
+ this.lastMessage = message;
859
+ this.contentContainer.clear();
860
+ const texts = message.content.filter((content: any) => content.type === "text" && content.text?.trim());
861
+ const hasToolCalls = message.content.some((content: any) => content.type === "toolCall");
862
+ const hasTerminalNotice = message.stopReason === "length"
863
+ || (!hasToolCalls && (message.stopReason === "aborted" || message.stopReason === "error"));
864
+ if (texts.length > 0) {
865
+ this.contentContainer.addChild(new Spacer(1));
866
+ for (const content of texts) {
867
+ this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme));
868
+ }
869
+ }
870
+ if (texts.length > 0 || hasTerminalNotice) {
871
+ clearCurrentThought();
872
+ state.currentBurst = [];
873
+ }
874
+ appendNativeStopReason(this, message, theme);
875
+ };
876
+
877
+ prototype.updateContent = patch.patchedUpdateContent;
878
+ prototype[ASSISTANT_PATCH_KEY] = patch;
879
+ return patch;
880
+ }
881
+
882
+ function patchRenderers(installation: CompactInstallation): void {
883
+ installation.toolPatch = patchToolExecutionComponent(installation);
884
+ installation.assistantPatch = patchAssistantMessageComponent(installation);
885
+ }
886
+
887
+ function restorePrototypePatches(installation: CompactInstallation): void {
888
+ const toolPatch = installation.toolPatch;
889
+ if (toolPatch) {
890
+ toolPatch.active = false;
891
+ if (toolPatch.prototype.updateDisplay === toolPatch.patchedUpdateDisplay) {
892
+ toolPatch.prototype.updateDisplay = toolPatch.originalUpdateDisplay;
893
+ }
894
+ if (toolPatch.prototype.render === toolPatch.patchedRender) {
895
+ toolPatch.prototype.render = toolPatch.originalRender;
896
+ }
897
+ if (toolPatch.prototype[TOOL_PATCH_KEY] === toolPatch) delete toolPatch.prototype[TOOL_PATCH_KEY];
898
+ }
899
+
900
+ const assistantPatch = installation.assistantPatch;
901
+ if (assistantPatch) {
902
+ assistantPatch.active = false;
903
+ if (assistantPatch.prototype.updateContent === assistantPatch.patchedUpdateContent) {
904
+ assistantPatch.prototype.updateContent = assistantPatch.originalUpdateContent;
905
+ }
906
+ if (assistantPatch.prototype[ASSISTANT_PATCH_KEY] === assistantPatch) {
907
+ delete assistantPatch.prototype[ASSISTANT_PATCH_KEY];
908
+ }
909
+ }
910
+ }
911
+
912
+ /** Re-render all rows touched by compact mode, including persisted summaries. */
913
+ export function refreshCompactTranscript(): void {
914
+ for (const component of state.toolComponents) {
915
+ try {
916
+ component.invalidate?.();
917
+ } catch {
918
+ state.toolComponents.delete(component);
919
+ }
920
+ }
921
+ for (const component of state.assistantComponents) {
922
+ try {
923
+ if (typeof component.invalidate === "function") component.invalidate();
924
+ else if (component.lastMessage) component.updateContent?.(component.lastMessage);
925
+ } catch {
926
+ state.assistantComponents.delete(component);
927
+ }
928
+ }
929
+ try {
930
+ state.lastTui?.requestRender?.(true);
931
+ } catch {
932
+ state.lastTui = undefined;
933
+ }
934
+ }
935
+
936
+ export function normalizeSummary(input: unknown): CompactSummaryData {
937
+ const source = (input && typeof input === "object" ? input : {}) as Record<string, unknown>;
938
+ const num = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0);
939
+ return {
940
+ reads: num(source.reads),
941
+ edits: num(source.edits),
942
+ commands: num(source.commands),
943
+ others: num(source.others),
944
+ failed: num(source.failed),
945
+ durationMs: num(source.durationMs),
946
+ };
947
+ }
948
+
949
+ export function summaryLine(input: CompactSummaryData | unknown): string {
950
+ const data = normalizeSummary(input);
951
+ const plural = (count: number) => (count === 1 ? "" : "s");
952
+ const parts: string[] = [];
953
+ if (data.reads) parts.push(`read ${data.reads} file${plural(data.reads)}`);
954
+ if (data.edits) parts.push(`edited ${data.edits} file${plural(data.edits)}`);
955
+ if (data.commands) parts.push(`ran ${data.commands} command${plural(data.commands)}`);
956
+ if (data.others) parts.push(`${data.others} other tool${plural(data.others)}`);
957
+ if (data.failed) parts.push(`${data.failed} failed`);
958
+ if (parts.length === 0) return "";
959
+ const text = parts.join(", ");
960
+ const capitalized = text[0].toUpperCase() + text.slice(1);
961
+ const duration = formatDuration(data.durationMs);
962
+ return duration ? `${capitalized} · ${duration}` : capitalized;
963
+ }
964
+
965
+ /**
966
+ * Entry renderer component whose output follows the current mode on every
967
+ * render. The host keeps this component mounted, so a normal requestRender is
968
+ * enough to hide or reveal persisted summaries without patching private entry
969
+ * components.
970
+ */
971
+ export class DynamicSummaryComponent {
972
+ private readonly data: unknown;
973
+ private readonly theme: Theme;
974
+
975
+ constructor(data: unknown, theme: Theme) {
976
+ this.data = data;
977
+ this.theme = theme;
978
+ }
979
+
980
+ invalidate(): void {}
981
+
982
+ render(width: number): string[] {
983
+ const line = isCompact() ? summaryLine(this.data) : "";
984
+ if (!line) return [];
985
+ return new Text(this.theme.fg("muted", line), 0, 0).render(width);
986
+ }
987
+ }
988
+
989
+ function appendRunSummary() {
990
+ const stats = state.runStats;
991
+ // A single tool row is already the summary.
992
+ if (!isCompact() || stats.toolCount < 2 || !state.pi?.appendEntry) return;
993
+ const data: CompactSummaryData = {
994
+ reads: stats.readFiles.size,
995
+ edits: stats.editFiles.size,
996
+ commands: stats.commandCount,
997
+ others: stats.otherCount,
998
+ failed: stats.failedCount,
999
+ durationMs: Date.now() - stats.startedAt,
1000
+ };
1001
+ state.pi.appendEntry(SUMMARY_ENTRY_TYPE, data);
1002
+ }
1003
+
1004
+ export type CompactStyleHooks = {
1005
+ onSessionStart(event: unknown, ctx: ExtensionContext): void;
1006
+ onSessionShutdown(event: unknown, ctx: ExtensionContext): void;
1007
+ onAgentStart(event: unknown, ctx: ExtensionContext): void;
1008
+ onAgentEnd(event: unknown, ctx: ExtensionContext): void;
1009
+ onTurnStart(event: unknown, ctx: ExtensionContext): void;
1010
+ onMessageUpdate(event: any, ctx: ExtensionContext): void;
1011
+ onToolExecutionStart(event: any, ctx: ExtensionContext): void;
1012
+ onToolExecutionUpdate(event: any, ctx: ExtensionContext): void;
1013
+ onToolExecutionEnd(event: any, ctx: ExtensionContext): void;
1014
+ refresh(): void;
1015
+ };
1016
+
1017
+ function teardownCompactInstallation(installation: CompactInstallation): void {
1018
+ if (!installation.active) return;
1019
+ installation.active = false;
1020
+ if (installation.toolPatch) installation.toolPatch.active = false;
1021
+ if (installation.assistantPatch) installation.assistantPatch.active = false;
1022
+
1023
+ if (state.activeInstallation === installation) {
1024
+ // Make every retained wrapper a native pass-through before rebuilding rows.
1025
+ // This also severs callbacks into an unloaded extension module.
1026
+ state.getMode = () => "off";
1027
+ state.getExcludeRenderers = () => [];
1028
+ state.pi = undefined;
1029
+ refreshCompactTranscript();
1030
+ }
1031
+
1032
+ restorePrototypePatches(installation);
1033
+ if (state.activeInstallation === installation) {
1034
+ clearSessionState();
1035
+ state.activeInstallation = undefined;
1036
+ }
1037
+ }
1038
+
1039
+ /** Install compact renderers without registering another Pi package entry. */
1040
+ export function installCompactStyle(pi: ExtensionAPI, host: CompactStyleHost): CompactStyleHooks {
1041
+ if (state.activeInstallation) {
1042
+ teardownCompactInstallation(state.activeInstallation);
1043
+ } else {
1044
+ // Recover state left by an older/failed installation before taking ownership.
1045
+ state.getMode = () => "off";
1046
+ state.getExcludeRenderers = () => [];
1047
+ refreshCompactTranscript();
1048
+ clearSessionState();
1049
+ }
1050
+
1051
+ const installation: CompactInstallation = { active: true };
1052
+ state.activeInstallation = installation;
1053
+ state.getMode = host.getMode;
1054
+ state.getExcludeRenderers = host.getExcludeRenderers;
1055
+ state.pi = pi;
1056
+
1057
+ try {
1058
+ patchRenderers(installation);
1059
+ if (typeof (pi as any).registerEntryRenderer === "function") {
1060
+ pi.registerEntryRenderer(SUMMARY_ENTRY_TYPE, (entry: any, _options: any, theme: Theme) =>
1061
+ new DynamicSummaryComponent(entry.data, theme),
1062
+ );
1063
+ }
1064
+ } catch (error) {
1065
+ teardownCompactInstallation(installation);
1066
+ throw error;
1067
+ }
1068
+
1069
+ const isOwner = () => installation.active && state.activeInstallation === installation;
1070
+ return {
1071
+ onSessionStart: (_event, ctx) => {
1072
+ if (!isOwner()) return;
1073
+ captureTheme(ctx);
1074
+ state.agentActive = false;
1075
+ state.runStats = newRunStats();
1076
+ // /reload and /resume rebuild transcript components before this event.
1077
+ // Keep those new references and repaint now that the new theme is known.
1078
+ refreshCompactTranscript();
1079
+ ctx.ui.setStatus(STATUS_KEY, undefined);
1080
+ },
1081
+ onSessionShutdown: (_event, ctx) => {
1082
+ if (isOwner()) teardownCompactInstallation(installation);
1083
+ ctx.ui.setStatus(STATUS_KEY, undefined);
1084
+ },
1085
+ onAgentStart: (_event, ctx) => {
1086
+ if (!isOwner()) return;
1087
+ state.agentActive = true;
1088
+ captureTheme(ctx);
1089
+ resetToolRun();
1090
+ state.runStats = newRunStats();
1091
+ },
1092
+ onAgentEnd: (_event, _ctx) => {
1093
+ if (!isOwner()) return;
1094
+ state.agentActive = false;
1095
+ state.currentBurst = [];
1096
+ clearCurrentThought();
1097
+ state.runningToolIds.clear();
1098
+ stopBlinkTimer();
1099
+ appendRunSummary();
1100
+ },
1101
+ onTurnStart: (_event, ctx) => {
1102
+ if (!isOwner()) return;
1103
+ captureTheme(ctx);
1104
+ },
1105
+ onMessageUpdate: (event, ctx) => {
1106
+ if (!isOwner()) return;
1107
+ captureTheme(ctx);
1108
+ const type = event?.assistantMessageEvent?.type;
1109
+ if (typeof type === "string" && type.startsWith("thinking_")) updateCurrentThoughtFromMessage(event.message);
1110
+ if (textSignalHasVisibleContent(event?.assistantMessageEvent)) {
1111
+ clearCurrentThought();
1112
+ state.currentBurst = [];
1113
+ }
1114
+ },
1115
+ onToolExecutionStart: (event, ctx) => {
1116
+ if (!isOwner()) return;
1117
+ captureTheme(ctx);
1118
+ beginTool(event.toolCallId, event.toolName, event.args);
1119
+ },
1120
+ onToolExecutionUpdate: (event, ctx) => {
1121
+ if (!isOwner()) return;
1122
+ captureTheme(ctx);
1123
+ updateToolResult(event.toolCallId, event.partialResult, false, true);
1124
+ },
1125
+ onToolExecutionEnd: (event, ctx) => {
1126
+ if (!isOwner()) return;
1127
+ captureTheme(ctx);
1128
+ updateToolResult(event.toolCallId, event.result, event.isError, false);
1129
+ },
1130
+ refresh: () => {
1131
+ if (isOwner()) refreshCompactTranscript();
1132
+ },
1133
+ };
1134
+ }
1135
+
1136
+ export { SUMMARY_ENTRY_TYPE };