pi-cc-extensions 0.8.45 → 0.8.47

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