@pi-kaush/pi-tool-call-markers 0.1.2 → 0.2.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.
- package/CHANGELOG.md +15 -0
- package/README.md +22 -13
- package/package.json +4 -3
- package/src/index.ts +358 -141
- package/src/thinking-block-merger.ts +123 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Compose capped in-progress rows before the Box paints its background, fixing elapsed tails leaking outside the tool background and restoring the block's bottom padding.
|
|
6
|
+
|
|
7
|
+
- Render in-progress calls as a single header line (elapsed time inline for `bash`) that settles into the final summary at the same height, so the transcript flows downwards without jumping.
|
|
8
|
+
- Show the real duration in successful bash summaries (`→ done · 2.3s`) instead of dropping the elapsed time on completion.
|
|
9
|
+
- Keep a call's settled shape stable once rendered: batches that settle next to an active sibling stay as individual one-liners instead of regrouping afterwards, which removes the second jump when a parallel batch completes.
|
|
10
|
+
- Only invalidate grouped-render caches on real state transitions, so bash's per-second ticks and resize invalidations stop busting them.
|
|
11
|
+
|
|
12
|
+
- Keep failed calls collapsed by default while retaining their native error background and Ctrl+O expansion.
|
|
13
|
+
- Add compact result tails for common successful tools in singleton and grouped summaries.
|
|
14
|
+
- Keep grouped bullets to one line and preserve the useful result tail on narrow terminals.
|
|
15
|
+
- Merge settled calls across assistant turns when no visible prose separates them, without reopening earlier groups while a later batch is active.
|
|
16
|
+
- Move adjacent thinking-block merging to a separate extension entrypoint bundled in this package.
|
|
17
|
+
|
|
3
18
|
## 0.1.2
|
|
4
19
|
|
|
5
20
|
- Keep grouping within one assistant tool batch, so a later pending batch does not reopen completed groups.
|
package/README.md
CHANGED
|
@@ -7,15 +7,21 @@ Collapse Pi's adjacent successful tool calls into one compact, gear-headed block
|
|
|
7
7
|
When several tool calls of the same type succeed in a row, Pi normally renders each one as its own expanded block. This extension groups them:
|
|
8
8
|
|
|
9
9
|
- **One gear header per contiguous tool type.** A run of `read` calls shares a single `⚙️ read` header; the following `write` run gets its own `⚙️ write` header.
|
|
10
|
-
- **Bulleted call summaries.** Each call in a group becomes one bullet with a short summary
|
|
10
|
+
- **Bulleted call summaries.** Each call in a group becomes one bullet with a short summary and, for common tools, a compact outcome such as `→ done`, `→ 42 lines`, or `→ +2/-1`.
|
|
11
11
|
- **Vertical spacing between tool types.** A blank line separates one tool group from the next.
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
12
|
+
- **One-line, width-safe summaries.** Long targets truncate before their useful outcome tail instead of wrapping into taller blocks.
|
|
13
|
+
- **Running calls stay one line.** While a call streams or executes, it renders as a single header line (with elapsed time inline for `bash`), so the transcript only grows downwards; the same line settles into the final `→ done · 2.3s`-style summary without changing height.
|
|
14
|
+
- **Settled shapes never re-render.** A call's settled shape is decided the first time it renders settled and never changes afterwards. Batches that finish while a sibling is still active stay as individual one-liners instead of regrouping later; batches already settled on first render (e.g. restored history) still merge into groups across assistant turns that rendered no prose.
|
|
14
15
|
- **Self-rendered tools keep their shell.** Tools that own their framing keep singleton previews intact. Grouped summaries use only their stable header instead of scraping preview or diff lines.
|
|
15
|
-
- **
|
|
16
|
-
- **Errors
|
|
17
|
-
- **Ctrl+O restores full blocks.** Expanding tools (`setToolsExpanded(true)`) brings back Pi's individual full blocks,
|
|
18
|
-
|
|
16
|
+
- **Image results stay visible.** Image-bearing results are not collapsed into text-only groups. Partial text output is held back while a call runs and surfaces through the final summary.
|
|
17
|
+
- **Errors stay compact and visibly failed.** A failed call keeps its own error-colored block and native collapsed detail until expanded.
|
|
18
|
+
- **Ctrl+O restores full blocks.** Expanding tools (`setToolsExpanded(true)`) brings back Pi's individual full blocks, including complete error details and successful results.
|
|
19
|
+
|
|
20
|
+
## Bundled thinking-block extension
|
|
21
|
+
|
|
22
|
+
The package ships a second extension entrypoint, `src/thinking-block-merger.ts`. Pi loads it independently from the tool presentation extension, so its `AssistantMessageComponent` patch and shutdown lifecycle stay isolated while install, update, and removal remain one package operation.
|
|
23
|
+
|
|
24
|
+
It combines only directly adjacent `thinking` blocks for display. Tool calls, text, and other content remain boundaries; provider blocks and signatures are not modified.
|
|
19
25
|
|
|
20
26
|
## Install
|
|
21
27
|
|
|
@@ -28,20 +34,23 @@ pi install npm:@pi-kaush/pi-tool-call-markers@0.1.0
|
|
|
28
34
|
For local development:
|
|
29
35
|
|
|
30
36
|
```bash
|
|
31
|
-
pi
|
|
37
|
+
pi \
|
|
38
|
+
-e ./extensions/pi-tool-call-markers/src/index.ts \
|
|
39
|
+
-e ./extensions/pi-tool-call-markers/src/thinking-block-merger.ts
|
|
32
40
|
```
|
|
33
41
|
|
|
34
42
|
## Compatibility and risk
|
|
35
43
|
|
|
36
|
-
|
|
44
|
+
The tool presentation entrypoint currently relies on **guarded, reversible prototype patches** against two Pi component classes:
|
|
37
45
|
|
|
38
46
|
- `ToolExecutionComponent` (render + display presentation),
|
|
39
|
-
- `Container` (transcript grouping)
|
|
40
|
-
|
|
47
|
+
- `Container` (transcript grouping).
|
|
48
|
+
|
|
49
|
+
The bundled thinking-block entrypoint separately patches `AssistantMessageComponent.updateContent`. Its patch and lifecycle do not share state with tool presentation.
|
|
41
50
|
|
|
42
|
-
Pi exposes no public transcript or tool-grouping hook today, so the extension patches those prototypes
|
|
51
|
+
Pi exposes no public transcript or tool-grouping hook today, so the extension patches those prototypes and restores the originals on `session_shutdown`. Every patch is wrapped in `try`/`catch` with an idempotency guard (`Symbol.for(...)` markers), so if Pi's internals change the extension silently no-ops and Pi's default rendering is preserved.
|
|
43
52
|
|
|
44
|
-
**Compatible Pi version:** `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` `>=0.80.6`. Because the patches touch internal prototype methods, a future Pi release that renames or restructures those methods can silently disable the
|
|
53
|
+
**Compatible Pi version:** `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui` `>=0.80.6`. Because the patches touch internal prototype methods, a future Pi release that renames or restructures those methods can silently disable the affected presentation until this package is updated. All original methods are restored on shutdown.
|
|
45
54
|
|
|
46
55
|
> TODO: migrate to a public Pi tool/transcript rendering API when one becomes available, and remove the prototype patches.
|
|
47
56
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-kaush/pi-tool-call-markers",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Compact and group Pi tool calls, with a bundled extension for adjacent thinking blocks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kaushik Gopal",
|
|
7
7
|
"type": "module",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
],
|
|
32
32
|
"pi": {
|
|
33
33
|
"extensions": [
|
|
34
|
-
"./src/index.ts"
|
|
34
|
+
"./src/index.ts",
|
|
35
|
+
"./src/thinking-block-merger.ts"
|
|
35
36
|
]
|
|
36
37
|
},
|
|
37
38
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
sliceByColumn,
|
|
10
10
|
truncateToWidth,
|
|
11
11
|
visibleWidth,
|
|
12
|
-
wrapTextWithAnsi,
|
|
13
12
|
} from "@earendil-works/pi-tui";
|
|
14
13
|
|
|
15
14
|
const BADGE = " ⚙️";
|
|
@@ -17,7 +16,6 @@ const BADGE_WIDTH = visibleWidth(BADGE);
|
|
|
17
16
|
const PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v3");
|
|
18
17
|
const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
|
|
19
18
|
const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
|
|
20
|
-
const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.v1");
|
|
21
19
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
22
20
|
const BOLD_ON_RE = /\u001b\[1m/g;
|
|
23
21
|
|
|
@@ -49,8 +47,10 @@ type ToolExecutionRow = {
|
|
|
49
47
|
isPartial?: boolean;
|
|
50
48
|
result?: {
|
|
51
49
|
isError?: boolean;
|
|
52
|
-
content?: Array<{ type?: unknown }>;
|
|
50
|
+
content?: Array<{ type?: unknown; text?: unknown }>;
|
|
51
|
+
details?: Record<string, unknown>;
|
|
53
52
|
};
|
|
53
|
+
rendererState?: { startedAt?: unknown; endedAt?: unknown };
|
|
54
54
|
contentBox?: ComponentContainer;
|
|
55
55
|
contentText?: TextComponent;
|
|
56
56
|
selfRenderContainer?: ComponentContainer;
|
|
@@ -67,6 +67,10 @@ type PresentationPatchState = {
|
|
|
67
67
|
theme?: ThemeLike;
|
|
68
68
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
69
69
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
70
|
+
// A row's settled shape is decided the first time it renders settled and
|
|
71
|
+
// never changes afterwards, so live output only ever grows downwards.
|
|
72
|
+
rowModes: WeakMap<ToolExecutionRow, "individual" | ToolExecutionRow[]>;
|
|
73
|
+
rowSignatures: WeakMap<ToolExecutionRow, string>;
|
|
70
74
|
originalRender: (width: number) => string[];
|
|
71
75
|
originalUpdateDisplay: () => void;
|
|
72
76
|
patchedRender?: (width: number) => string[];
|
|
@@ -87,25 +91,6 @@ type GroupRenderCache = {
|
|
|
87
91
|
width: number;
|
|
88
92
|
};
|
|
89
93
|
|
|
90
|
-
type AssistantMessageLike = {
|
|
91
|
-
content?: unknown[];
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
type AssistantMessageRow = {
|
|
95
|
-
updateContent(message: AssistantMessageLike): void;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
type ThinkingGroupingPatchState = {
|
|
99
|
-
originalUpdateContent: (message: AssistantMessageLike) => void;
|
|
100
|
-
patchedUpdateContent?: (message: AssistantMessageLike) => void;
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
type ThinkingContentLike = {
|
|
104
|
-
type: "thinking";
|
|
105
|
-
thinking: string;
|
|
106
|
-
[key: string]: unknown;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
94
|
function stripAnsi(text: string): string {
|
|
110
95
|
return text.replace(ANSI_RE, "");
|
|
111
96
|
}
|
|
@@ -206,7 +191,10 @@ function hideResultImages(row: ToolExecutionRow): void {
|
|
|
206
191
|
row.imageSpacers = [];
|
|
207
192
|
}
|
|
208
193
|
|
|
209
|
-
function collapseSuccessfulResult(
|
|
194
|
+
function collapseSuccessfulResult(
|
|
195
|
+
row: ToolExecutionRow,
|
|
196
|
+
theme?: ThemeLike,
|
|
197
|
+
): void {
|
|
210
198
|
if (
|
|
211
199
|
row.expanded !== false ||
|
|
212
200
|
row.isPartial !== false ||
|
|
@@ -220,7 +208,10 @@ function collapseSuccessfulResult(row: ToolExecutionRow): void {
|
|
|
220
208
|
const collapsed = row.hasRendererDefinition?.()
|
|
221
209
|
? removeResultComponent(row.contentBox)
|
|
222
210
|
: collapseGenericResult(row);
|
|
223
|
-
if (collapsed)
|
|
211
|
+
if (collapsed) {
|
|
212
|
+
hideResultImages(row);
|
|
213
|
+
decorateSuccessfulCall(row, theme);
|
|
214
|
+
}
|
|
224
215
|
}
|
|
225
216
|
|
|
226
217
|
function isToolExecutionRow(
|
|
@@ -241,6 +232,113 @@ function hasImageResult(row: ToolExecutionRow): boolean {
|
|
|
241
232
|
);
|
|
242
233
|
}
|
|
243
234
|
|
|
235
|
+
function formatDuration(ms: number): string {
|
|
236
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function bashDurationText(row: ToolExecutionRow): string | undefined {
|
|
240
|
+
const startedAt = row.rendererState?.startedAt;
|
|
241
|
+
const endedAt = row.rendererState?.endedAt;
|
|
242
|
+
if (
|
|
243
|
+
typeof startedAt !== "number" ||
|
|
244
|
+
typeof endedAt !== "number" ||
|
|
245
|
+
!Number.isFinite(startedAt) ||
|
|
246
|
+
!Number.isFinite(endedAt)
|
|
247
|
+
)
|
|
248
|
+
return undefined;
|
|
249
|
+
return formatDuration(Math.max(0, endedAt - startedAt));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function liveElapsedText(row: ToolExecutionRow): string | undefined {
|
|
253
|
+
if (row.toolName !== "bash") return undefined;
|
|
254
|
+
const startedAt = row.rendererState?.startedAt;
|
|
255
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt))
|
|
256
|
+
return undefined;
|
|
257
|
+
return formatDuration(Math.max(0, Date.now() - startedAt));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isLiveRow(row: ToolExecutionRow): boolean {
|
|
261
|
+
return (
|
|
262
|
+
row.expanded === false &&
|
|
263
|
+
(row.isPartial !== false || !row.result) &&
|
|
264
|
+
row.getRenderShell?.() !== "self" &&
|
|
265
|
+
!hasImageResult(row)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function liveTailText(
|
|
270
|
+
row: ToolExecutionRow,
|
|
271
|
+
droppedVisible: boolean,
|
|
272
|
+
theme?: ThemeLike,
|
|
273
|
+
): string | undefined {
|
|
274
|
+
if (!theme) return undefined;
|
|
275
|
+
const elapsed = liveElapsedText(row);
|
|
276
|
+
if (!droppedVisible && !elapsed) return undefined;
|
|
277
|
+
const parts: string[] = [];
|
|
278
|
+
if (droppedVisible) parts.push("…");
|
|
279
|
+
if (elapsed) parts.push("·", elapsed);
|
|
280
|
+
return theme.fg("muted", parts.join(" "));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fitLiveHeader(
|
|
284
|
+
header: string,
|
|
285
|
+
tail: string,
|
|
286
|
+
width: number,
|
|
287
|
+
droppedVisible: boolean,
|
|
288
|
+
): string {
|
|
289
|
+
const max = Math.max(1, width);
|
|
290
|
+
const head = header.trimEnd();
|
|
291
|
+
if (visibleWidth(head) + visibleWidth(tail) + 1 <= max)
|
|
292
|
+
return `${head} ${tail}`;
|
|
293
|
+
const headWidth = Math.max(1, max - visibleWidth(tail) - 1);
|
|
294
|
+
const headSuffix = droppedVisible ? "" : "…";
|
|
295
|
+
return `${truncateToWidth(head, headWidth, headSuffix, false)} ${tail}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// While a call streams or runs, pin it to a single header line with the
|
|
299
|
+
// elapsed time inline, so the block never grows and never collapses on
|
|
300
|
+
// completion; the header text simply settles into the outcome summary. The
|
|
301
|
+
// cap is composed before the Box applies background and padding, so the live
|
|
302
|
+
// line keeps the tool background edge to edge and the block's bottom padding.
|
|
303
|
+
function capLiveRowDisplay(row: ToolExecutionRow, theme?: ThemeLike): void {
|
|
304
|
+
if (row.hasRendererDefinition?.()) {
|
|
305
|
+
const container = row.contentBox;
|
|
306
|
+
if (!container || !Array.isArray(container.children)) return;
|
|
307
|
+
const hadExtraChildren = container.children.length > 1;
|
|
308
|
+
removeResultComponent(container);
|
|
309
|
+
const original = container.children[0] as ComponentLike | undefined;
|
|
310
|
+
if (!original || typeof original.render !== "function") return;
|
|
311
|
+
container.children[0] = {
|
|
312
|
+
render(width: number): string[] {
|
|
313
|
+
const lines = original.render(width);
|
|
314
|
+
const headerIndex = lines.findIndex(hasVisibleContent);
|
|
315
|
+
if (headerIndex === -1) return lines;
|
|
316
|
+
const header = lines[headerIndex] ?? "";
|
|
317
|
+
const droppedVisible =
|
|
318
|
+
hadExtraChildren ||
|
|
319
|
+
lines.slice(headerIndex + 1).some(hasVisibleContent);
|
|
320
|
+
const tail = liveTailText(row, droppedVisible, theme);
|
|
321
|
+
return [
|
|
322
|
+
tail ? fitLiveHeader(header, tail, width, droppedVisible) : header,
|
|
323
|
+
];
|
|
324
|
+
},
|
|
325
|
+
invalidate() {
|
|
326
|
+
original.invalidate();
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const text = row.contentText;
|
|
333
|
+
if (typeof text?.text !== "string" || typeof text.setText !== "function")
|
|
334
|
+
return;
|
|
335
|
+
const lines = text.text.split("\n");
|
|
336
|
+
const title = lines[0] ?? "";
|
|
337
|
+
const droppedVisible = lines.slice(1).some((line) => line.trim().length > 0);
|
|
338
|
+
const tail = liveTailText(row, droppedVisible, theme);
|
|
339
|
+
text.setText(tail ? `${title} ${tail}` : title);
|
|
340
|
+
}
|
|
341
|
+
|
|
244
342
|
function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
|
|
245
343
|
return (
|
|
246
344
|
row.expanded === false &&
|
|
@@ -284,39 +382,6 @@ function renderComponent(component: unknown, width: number): string[] {
|
|
|
284
382
|
return (component as ComponentLike).render(width);
|
|
285
383
|
}
|
|
286
384
|
|
|
287
|
-
function isThinkingContent(content: unknown): content is ThinkingContentLike {
|
|
288
|
-
return (
|
|
289
|
-
!!content &&
|
|
290
|
-
typeof content === "object" &&
|
|
291
|
-
(content as { type?: unknown }).type === "thinking" &&
|
|
292
|
-
typeof (content as { thinking?: unknown }).thinking === "string"
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
function combineAdjacentThinking(
|
|
297
|
-
message: AssistantMessageLike,
|
|
298
|
-
): AssistantMessageLike {
|
|
299
|
-
if (!Array.isArray(message.content)) return message;
|
|
300
|
-
|
|
301
|
-
// Merge a display-only copy; the original provider blocks and their signatures stay untouched.
|
|
302
|
-
let changed = false;
|
|
303
|
-
const content: unknown[] = [];
|
|
304
|
-
for (const block of message.content) {
|
|
305
|
-
const previous = content.at(-1);
|
|
306
|
-
if (isThinkingContent(previous) && isThinkingContent(block)) {
|
|
307
|
-
content[content.length - 1] = {
|
|
308
|
-
...previous,
|
|
309
|
-
thinking: `${previous.thinking.trim()}\n\n${block.thinking.trim()}`,
|
|
310
|
-
};
|
|
311
|
-
changed = true;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
content.push(block);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
return changed ? { ...message, content } : message;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
385
|
function compactArgs(args: unknown): string {
|
|
321
386
|
if (args === undefined || args === null) return "";
|
|
322
387
|
try {
|
|
@@ -327,6 +392,171 @@ function compactArgs(args: unknown): string {
|
|
|
327
392
|
}
|
|
328
393
|
}
|
|
329
394
|
|
|
395
|
+
function resultText(row: ToolExecutionRow): string {
|
|
396
|
+
const contentText = row.result?.content?.find(
|
|
397
|
+
(content) => content.type === "text" && typeof content.text === "string",
|
|
398
|
+
)?.text;
|
|
399
|
+
if (typeof contentText === "string") return contentText;
|
|
400
|
+
return row.getTextOutput?.() ?? "";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function textLineCount(text: string): number {
|
|
404
|
+
if (!text) return 0;
|
|
405
|
+
const lines = text.split("\n");
|
|
406
|
+
return text.endsWith("\n") ? lines.length - 1 : lines.length;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function resultCount(row: ToolExecutionRow, text: string): number {
|
|
410
|
+
const totalMatched = row.result?.details?.totalMatched;
|
|
411
|
+
if (typeof totalMatched === "number" && Number.isFinite(totalMatched))
|
|
412
|
+
return Math.max(0, totalMatched);
|
|
413
|
+
|
|
414
|
+
const trimmed = text.trim();
|
|
415
|
+
if (
|
|
416
|
+
!trimmed ||
|
|
417
|
+
/^(?:No matches found|No files found|\(empty directory\))/i.test(trimmed)
|
|
418
|
+
)
|
|
419
|
+
return 0;
|
|
420
|
+
|
|
421
|
+
if (row.toolName === "grep" || row.toolName === "ffgrep") {
|
|
422
|
+
const lines = trimmed.split("\n");
|
|
423
|
+
const matches = lines.filter(
|
|
424
|
+
(line) => /^.+:\d+:/.test(line) || /^\s+\d+:/.test(line),
|
|
425
|
+
).length;
|
|
426
|
+
if (matches > 0) return matches;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return trimmed
|
|
430
|
+
.split("\n")
|
|
431
|
+
.filter((line) => line.trim() && !/^\s*\[.*\]\s*$/.test(line)).length;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function readLineCount(row: ToolExecutionRow, text: string): number {
|
|
435
|
+
const outputLines = (
|
|
436
|
+
row.result?.details?.truncation as { outputLines?: unknown } | undefined
|
|
437
|
+
)?.outputLines;
|
|
438
|
+
if (typeof outputLines === "number" && Number.isFinite(outputLines))
|
|
439
|
+
return Math.max(0, outputLines);
|
|
440
|
+
|
|
441
|
+
const content = text.replace(
|
|
442
|
+
/\n\n\[[^\n]*(?:more lines|showing lines)[^\n]*\]\s*$/i,
|
|
443
|
+
"",
|
|
444
|
+
);
|
|
445
|
+
return textLineCount(content);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function diffCounts(diff: string): { added: number; removed: number } {
|
|
449
|
+
let added = 0;
|
|
450
|
+
let removed = 0;
|
|
451
|
+
for (const line of diff.split("\n")) {
|
|
452
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added++;
|
|
453
|
+
if (line.startsWith("-") && !line.startsWith("---")) removed++;
|
|
454
|
+
}
|
|
455
|
+
return { added, removed };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function outcomeSummary(row: ToolExecutionRow): string | undefined {
|
|
459
|
+
if (!isCollapsibleSuccess(row)) return undefined;
|
|
460
|
+
|
|
461
|
+
const text = resultText(row);
|
|
462
|
+
switch (row.toolName) {
|
|
463
|
+
case "bash": {
|
|
464
|
+
const duration = bashDurationText(row);
|
|
465
|
+
return duration ? `done · ${duration}` : "done";
|
|
466
|
+
}
|
|
467
|
+
case "read": {
|
|
468
|
+
const lines = readLineCount(row, text);
|
|
469
|
+
return `${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
470
|
+
}
|
|
471
|
+
case "write": {
|
|
472
|
+
const content = (row.args as { content?: unknown } | undefined)?.content;
|
|
473
|
+
if (typeof content !== "string") return "written";
|
|
474
|
+
const lines = textLineCount(content);
|
|
475
|
+
return `${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
476
|
+
}
|
|
477
|
+
case "edit": {
|
|
478
|
+
const diff = row.result?.details?.diff;
|
|
479
|
+
if (typeof diff !== "string") return "applied";
|
|
480
|
+
const { added, removed } = diffCounts(diff);
|
|
481
|
+
return `+${added}/-${removed}`;
|
|
482
|
+
}
|
|
483
|
+
case "grep":
|
|
484
|
+
case "ffgrep":
|
|
485
|
+
case "find":
|
|
486
|
+
case "fffind":
|
|
487
|
+
case "ls": {
|
|
488
|
+
const results = resultCount(row, text);
|
|
489
|
+
return `${results} ${results === 1 ? "result" : "results"}`;
|
|
490
|
+
}
|
|
491
|
+
default:
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderedOutcome(
|
|
497
|
+
row: ToolExecutionRow,
|
|
498
|
+
theme: ThemeLike,
|
|
499
|
+
): string | undefined {
|
|
500
|
+
const summary = outcomeSummary(row);
|
|
501
|
+
if (!summary) return undefined;
|
|
502
|
+
return `${theme.fg("muted", "→")} ${theme.fg("success", summary)}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function truncatePlain(text: string, width: number, suffix = "…"): string {
|
|
506
|
+
const max = Math.max(0, width);
|
|
507
|
+
const plain = stripAnsi(text);
|
|
508
|
+
if (visibleWidth(plain) <= max) return plain;
|
|
509
|
+
|
|
510
|
+
const suffixWidth = visibleWidth(suffix);
|
|
511
|
+
if (suffixWidth >= max) return sliceByColumn(suffix, 0, max, true);
|
|
512
|
+
return `${sliceByColumn(plain, 0, max - suffixWidth, true)}${suffix}`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function fitSummary(summary: string, width: number): string {
|
|
516
|
+
const max = Math.max(1, width);
|
|
517
|
+
return visibleWidth(summary) <= max ? summary : truncatePlain(summary, max);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function fitSummaryTail(head: string, tail: string, width: number): string {
|
|
521
|
+
const max = Math.max(1, width);
|
|
522
|
+
const summary = `${head.trimEnd()} ${tail}`;
|
|
523
|
+
if (visibleWidth(summary) <= max) return summary;
|
|
524
|
+
|
|
525
|
+
const tailWidth = visibleWidth(tail);
|
|
526
|
+
if (tailWidth >= max) return truncatePlain(tail, max);
|
|
527
|
+
return `${truncatePlain(head, max - tailWidth - 1)} ${tail}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function decorateSuccessfulCall(
|
|
531
|
+
row: ToolExecutionRow,
|
|
532
|
+
theme?: ThemeLike,
|
|
533
|
+
): void {
|
|
534
|
+
if (!theme || !Array.isArray(row.contentBox?.children)) return;
|
|
535
|
+
const original = row.contentBox.children[0] as ComponentLike | undefined;
|
|
536
|
+
const outcome = renderedOutcome(row, theme);
|
|
537
|
+
if (!original || typeof original.render !== "function" || !outcome) return;
|
|
538
|
+
|
|
539
|
+
row.contentBox.children[0] = {
|
|
540
|
+
render(width: number): string[] {
|
|
541
|
+
const lines = original.render(width);
|
|
542
|
+
const lineIndex = lines.findIndex(hasVisibleContent);
|
|
543
|
+
if (lineIndex === -1) return lines;
|
|
544
|
+
const line = lines[lineIndex];
|
|
545
|
+
if (line === undefined) return lines;
|
|
546
|
+
const next = [...lines];
|
|
547
|
+
next[lineIndex] = fitSummaryTail(
|
|
548
|
+
trimRenderedLine(line),
|
|
549
|
+
outcome,
|
|
550
|
+
Math.max(1, width - BADGE_WIDTH),
|
|
551
|
+
);
|
|
552
|
+
return next;
|
|
553
|
+
},
|
|
554
|
+
invalidate() {
|
|
555
|
+
original.invalidate();
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
330
560
|
function removeTrailingExpandHint(text: string): string {
|
|
331
561
|
const plain = stripAnsi(text).trimEnd();
|
|
332
562
|
const hint = plain.match(/\s+\([^)]*to expand\)$/i);
|
|
@@ -409,20 +639,22 @@ function renderedCallSummary(
|
|
|
409
639
|
: theme.fg("muted", "(no arguments)");
|
|
410
640
|
}
|
|
411
641
|
|
|
412
|
-
function
|
|
642
|
+
function compactBulletLine(
|
|
413
643
|
summary: string,
|
|
644
|
+
outcome: string | undefined,
|
|
414
645
|
width: number,
|
|
415
646
|
theme: ThemeLike,
|
|
416
|
-
): string
|
|
647
|
+
): string {
|
|
417
648
|
const prefix = ` ${theme.fg("muted", "•")} `;
|
|
418
649
|
const indent = visibleWidth(prefix);
|
|
419
|
-
if (width <= indent) return
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
650
|
+
if (width <= indent) return truncateToWidth(prefix, width, "", false);
|
|
651
|
+
const available = width - indent;
|
|
652
|
+
return (
|
|
653
|
+
prefix +
|
|
654
|
+
(outcome
|
|
655
|
+
? fitSummaryTail(summary, outcome, available)
|
|
656
|
+
: fitSummary(summary, available))
|
|
657
|
+
);
|
|
426
658
|
}
|
|
427
659
|
|
|
428
660
|
function groupedCallComponent(
|
|
@@ -445,8 +677,9 @@ function groupedCallComponent(
|
|
|
445
677
|
lines.push(truncateToWidth(heading, width, "", false));
|
|
446
678
|
previousToolName = row.toolName;
|
|
447
679
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
680
|
+
const call = renderedCallSummary(row, Math.max(1, width - 4), theme);
|
|
681
|
+
const outcome = renderedOutcome(row, theme);
|
|
682
|
+
lines.push(compactBulletLine(call, outcome, width, theme));
|
|
450
683
|
}
|
|
451
684
|
return lines;
|
|
452
685
|
},
|
|
@@ -600,7 +833,41 @@ function renderContainerWithToolGroups(
|
|
|
600
833
|
lines.push(...renderAt(index));
|
|
601
834
|
continue;
|
|
602
835
|
}
|
|
836
|
+
const decided = presentation.rowModes.get(child);
|
|
837
|
+
if (decided === "individual") {
|
|
838
|
+
lines.push(...renderAt(index));
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
if (Array.isArray(decided)) {
|
|
842
|
+
// A decided group is sticky, but transient states like Ctrl+O expansion
|
|
843
|
+
// still render members in full until they become collapsible again.
|
|
844
|
+
if (!decided.every(isCollapsibleSuccess)) {
|
|
845
|
+
lines.push(...renderAt(index));
|
|
846
|
+
continue;
|
|
847
|
+
}
|
|
848
|
+
const lastMember = decided[decided.length - 1];
|
|
849
|
+
let lastMemberIndex = index;
|
|
850
|
+
for (
|
|
851
|
+
let candidateIndex = index + 1;
|
|
852
|
+
candidateIndex < children.length;
|
|
853
|
+
candidateIndex++
|
|
854
|
+
) {
|
|
855
|
+
if (children[candidateIndex] === lastMember) {
|
|
856
|
+
lastMemberIndex = candidateIndex;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
lines.push(...renderGroupedToolRows(child, decided, width, presentation));
|
|
861
|
+
index = lastMemberIndex;
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Undecided row: pick its settled shape once. A row that first settles
|
|
866
|
+
// next to an active sibling stays individual forever, so live batches
|
|
867
|
+
// never regroup after the fact; only batches that are fully settled on
|
|
868
|
+
// first render (e.g. restored history) become groups.
|
|
603
869
|
if (hasUnsettledToolInBatch(children, index, renderAt)) {
|
|
870
|
+
presentation.rowModes.set(child, "individual");
|
|
604
871
|
lines.push(...renderAt(index));
|
|
605
872
|
continue;
|
|
606
873
|
}
|
|
@@ -613,9 +880,13 @@ function renderContainerWithToolGroups(
|
|
|
613
880
|
candidateIndex++
|
|
614
881
|
) {
|
|
615
882
|
const candidate = children[candidateIndex];
|
|
616
|
-
if (isAssistantMessageRow(candidate))
|
|
883
|
+
if (isAssistantMessageRow(candidate)) {
|
|
884
|
+
if (renderAt(candidateIndex).some(hasVisibleContent)) break;
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
617
887
|
if (isToolExecutionRow(candidate)) {
|
|
618
888
|
if (!isCollapsibleSuccess(candidate)) break;
|
|
889
|
+
if (presentation.rowModes.has(candidate)) break;
|
|
619
890
|
group.push(candidate);
|
|
620
891
|
lastMemberIndex = candidateIndex;
|
|
621
892
|
continue;
|
|
@@ -624,10 +895,12 @@ function renderContainerWithToolGroups(
|
|
|
624
895
|
}
|
|
625
896
|
|
|
626
897
|
if (group.length === 1) {
|
|
898
|
+
presentation.rowModes.set(child, "individual");
|
|
627
899
|
lines.push(...renderAt(index));
|
|
628
900
|
continue;
|
|
629
901
|
}
|
|
630
902
|
|
|
903
|
+
for (const member of group) presentation.rowModes.set(member, group);
|
|
631
904
|
lines.push(...renderGroupedToolRows(child, group, width, presentation));
|
|
632
905
|
index = lastMemberIndex;
|
|
633
906
|
}
|
|
@@ -636,69 +909,6 @@ function renderContainerWithToolGroups(
|
|
|
636
909
|
}
|
|
637
910
|
|
|
638
911
|
// TODO: Replace prototype patching with a public Pi tool/transcript rendering API when available.
|
|
639
|
-
function installThinkingGroupingPatch():
|
|
640
|
-
| ThinkingGroupingPatchState
|
|
641
|
-
| undefined {
|
|
642
|
-
try {
|
|
643
|
-
const proto =
|
|
644
|
-
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
645
|
-
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
646
|
-
updateContent?: (message: AssistantMessageLike) => void;
|
|
647
|
-
};
|
|
648
|
-
if (!proto || typeof proto.updateContent !== "function") return undefined;
|
|
649
|
-
|
|
650
|
-
const existing = proto[THINKING_GROUPING_PATCHED];
|
|
651
|
-
if (existing) return existing;
|
|
652
|
-
|
|
653
|
-
const state: ThinkingGroupingPatchState = {
|
|
654
|
-
originalUpdateContent: proto.updateContent,
|
|
655
|
-
};
|
|
656
|
-
const patchedUpdateContent = function updateContentWithCombinedThinking(
|
|
657
|
-
this: AssistantMessageRow,
|
|
658
|
-
message: AssistantMessageLike,
|
|
659
|
-
): void {
|
|
660
|
-
// Combine adjacent thinking blocks for display, but fall back to the original
|
|
661
|
-
// message if combining throws. Either way, invoke Pi's renderer exactly once.
|
|
662
|
-
let combined = message;
|
|
663
|
-
try {
|
|
664
|
-
combined = combineAdjacentThinking(message);
|
|
665
|
-
} catch {
|
|
666
|
-
// Thinking grouping is cosmetic; preserve the original message intact.
|
|
667
|
-
}
|
|
668
|
-
state.originalUpdateContent.call(this, combined);
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
state.patchedUpdateContent = patchedUpdateContent;
|
|
672
|
-
proto.updateContent = patchedUpdateContent;
|
|
673
|
-
Object.defineProperty(proto, THINKING_GROUPING_PATCHED, {
|
|
674
|
-
configurable: true,
|
|
675
|
-
value: state,
|
|
676
|
-
});
|
|
677
|
-
return state;
|
|
678
|
-
} catch {
|
|
679
|
-
// Thinking grouping is cosmetic; preserve Pi's renderer if its internals change.
|
|
680
|
-
return undefined;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function uninstallThinkingGroupingPatch(
|
|
685
|
-
state: ThinkingGroupingPatchState | undefined,
|
|
686
|
-
): void {
|
|
687
|
-
if (!state) return;
|
|
688
|
-
const proto =
|
|
689
|
-
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
690
|
-
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
691
|
-
updateContent?: (message: AssistantMessageLike) => void;
|
|
692
|
-
};
|
|
693
|
-
if (
|
|
694
|
-
proto[THINKING_GROUPING_PATCHED] !== state ||
|
|
695
|
-
proto.updateContent !== state.patchedUpdateContent
|
|
696
|
-
)
|
|
697
|
-
return;
|
|
698
|
-
proto.updateContent = state.originalUpdateContent;
|
|
699
|
-
delete proto[THINKING_GROUPING_PATCHED];
|
|
700
|
-
}
|
|
701
|
-
|
|
702
912
|
function installGroupingPatch(
|
|
703
913
|
presentation: PresentationPatchState,
|
|
704
914
|
): GroupingPatchState | undefined {
|
|
@@ -790,21 +1000,30 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
790
1000
|
const state: PresentationPatchState = {
|
|
791
1001
|
groupCache: new WeakMap(),
|
|
792
1002
|
rowVersions: new WeakMap(),
|
|
1003
|
+
rowModes: new WeakMap(),
|
|
1004
|
+
rowSignatures: new WeakMap(),
|
|
793
1005
|
originalRender: proto.render,
|
|
794
1006
|
originalUpdateDisplay: proto.updateDisplay,
|
|
795
1007
|
};
|
|
796
1008
|
const patchedUpdateDisplay = function updateDisplayWithCollapsedResult(
|
|
797
1009
|
this: ToolExecutionRow,
|
|
798
1010
|
): void {
|
|
799
|
-
|
|
800
|
-
state
|
|
1011
|
+
// Group members always bump so their leader's cache refreshes; other
|
|
1012
|
+
// rows only bump on state transitions, so bash's per-second invalidate
|
|
1013
|
+
// ticks and resize invalidations stop busting caches.
|
|
1014
|
+
const signature = `${this.isPartial}|${this.expanded}|${this.result ? 1 : 0}|${this.result?.isError ? 1 : 0}`;
|
|
1015
|
+
if (
|
|
1016
|
+
Array.isArray(state.rowModes.get(this)) ||
|
|
1017
|
+
state.rowSignatures.get(this) !== signature
|
|
1018
|
+
) {
|
|
1019
|
+
state.rowSignatures.set(this, signature);
|
|
1020
|
+
state.rowVersions.set(this, (state.rowVersions.get(this) ?? 0) + 1);
|
|
1021
|
+
state.groupCache.delete(this);
|
|
1022
|
+
}
|
|
801
1023
|
state.originalUpdateDisplay.call(this);
|
|
802
1024
|
try {
|
|
803
|
-
if (this
|
|
804
|
-
|
|
805
|
-
state.originalUpdateDisplay.call(this);
|
|
806
|
-
}
|
|
807
|
-
collapseSuccessfulResult(this);
|
|
1025
|
+
if (isLiveRow(this)) capLiveRowDisplay(this, state.theme);
|
|
1026
|
+
else collapseSuccessfulResult(this, state.theme);
|
|
808
1027
|
} catch {
|
|
809
1028
|
// Presentation is cosmetic; preserve Pi's renderer if its internals change.
|
|
810
1029
|
}
|
|
@@ -867,7 +1086,6 @@ function uninstallPresentationPatch(
|
|
|
867
1086
|
export default function (pi: ExtensionAPI) {
|
|
868
1087
|
const patch = installPresentationPatch();
|
|
869
1088
|
const grouping = patch ? installGroupingPatch(patch) : undefined;
|
|
870
|
-
const thinkingGrouping = installThinkingGroupingPatch();
|
|
871
1089
|
|
|
872
1090
|
pi.on("session_start", (_event, ctx) => {
|
|
873
1091
|
if (patch) patch.theme = ctx.ui.theme;
|
|
@@ -875,7 +1093,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
875
1093
|
});
|
|
876
1094
|
|
|
877
1095
|
pi.on("session_shutdown", () => {
|
|
878
|
-
uninstallThinkingGroupingPatch(thinkingGrouping);
|
|
879
1096
|
uninstallGroupingPatch(grouping);
|
|
880
1097
|
uninstallPresentationPatch(patch);
|
|
881
1098
|
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
const THINKING_GROUPING_PATCHED = Symbol.for("kg.pi.thinkingGrouping.v1");
|
|
5
|
+
|
|
6
|
+
type AssistantMessageLike = {
|
|
7
|
+
content?: unknown[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type AssistantMessageRow = {
|
|
11
|
+
updateContent(message: AssistantMessageLike): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ThinkingGroupingPatchState = {
|
|
15
|
+
originalUpdateContent: (message: AssistantMessageLike) => void;
|
|
16
|
+
patchedUpdateContent?: (message: AssistantMessageLike) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type ThinkingContentLike = {
|
|
20
|
+
type: "thinking";
|
|
21
|
+
thinking: string;
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isThinkingContent(content: unknown): content is ThinkingContentLike {
|
|
26
|
+
return (
|
|
27
|
+
!!content &&
|
|
28
|
+
typeof content === "object" &&
|
|
29
|
+
(content as { type?: unknown }).type === "thinking" &&
|
|
30
|
+
typeof (content as { thinking?: unknown }).thinking === "string"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function combineAdjacentThinking(
|
|
35
|
+
message: AssistantMessageLike,
|
|
36
|
+
): AssistantMessageLike {
|
|
37
|
+
if (!Array.isArray(message.content)) return message;
|
|
38
|
+
|
|
39
|
+
// Merge a display-only copy; provider blocks and signatures stay untouched.
|
|
40
|
+
let changed = false;
|
|
41
|
+
const content: unknown[] = [];
|
|
42
|
+
for (const block of message.content) {
|
|
43
|
+
const previous = content.at(-1);
|
|
44
|
+
if (isThinkingContent(previous) && isThinkingContent(block)) {
|
|
45
|
+
content[content.length - 1] = {
|
|
46
|
+
...previous,
|
|
47
|
+
thinking: `${previous.thinking.trim()}\n\n${block.thinking.trim()}`,
|
|
48
|
+
};
|
|
49
|
+
changed = true;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
content.push(block);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return changed ? { ...message, content } : message;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// TODO: Replace prototype patching with a public assistant-message rendering API.
|
|
59
|
+
function installThinkingGroupingPatch():
|
|
60
|
+
| ThinkingGroupingPatchState
|
|
61
|
+
| undefined {
|
|
62
|
+
try {
|
|
63
|
+
const proto =
|
|
64
|
+
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
65
|
+
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
66
|
+
updateContent?: (message: AssistantMessageLike) => void;
|
|
67
|
+
};
|
|
68
|
+
if (!proto || typeof proto.updateContent !== "function") return undefined;
|
|
69
|
+
|
|
70
|
+
const existing = proto[THINKING_GROUPING_PATCHED];
|
|
71
|
+
if (existing) return existing;
|
|
72
|
+
|
|
73
|
+
const state: ThinkingGroupingPatchState = {
|
|
74
|
+
originalUpdateContent: proto.updateContent,
|
|
75
|
+
};
|
|
76
|
+
const patchedUpdateContent = function updateContentWithCombinedThinking(
|
|
77
|
+
this: AssistantMessageRow,
|
|
78
|
+
message: AssistantMessageLike,
|
|
79
|
+
): void {
|
|
80
|
+
// Thinking grouping is cosmetic. If it fails, render the original once.
|
|
81
|
+
let combined = message;
|
|
82
|
+
try {
|
|
83
|
+
combined = combineAdjacentThinking(message);
|
|
84
|
+
} catch {
|
|
85
|
+
// Preserve the original message intact.
|
|
86
|
+
}
|
|
87
|
+
state.originalUpdateContent.call(this, combined);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
state.patchedUpdateContent = patchedUpdateContent;
|
|
91
|
+
proto.updateContent = patchedUpdateContent;
|
|
92
|
+
Object.defineProperty(proto, THINKING_GROUPING_PATCHED, {
|
|
93
|
+
configurable: true,
|
|
94
|
+
value: state,
|
|
95
|
+
});
|
|
96
|
+
return state;
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function uninstallThinkingGroupingPatch(
|
|
103
|
+
state: ThinkingGroupingPatchState | undefined,
|
|
104
|
+
): void {
|
|
105
|
+
if (!state) return;
|
|
106
|
+
const proto =
|
|
107
|
+
AssistantMessageComponent?.prototype as unknown as AssistantMessageRow & {
|
|
108
|
+
[THINKING_GROUPING_PATCHED]?: ThinkingGroupingPatchState;
|
|
109
|
+
updateContent?: (message: AssistantMessageLike) => void;
|
|
110
|
+
};
|
|
111
|
+
if (
|
|
112
|
+
proto[THINKING_GROUPING_PATCHED] !== state ||
|
|
113
|
+
proto.updateContent !== state.patchedUpdateContent
|
|
114
|
+
)
|
|
115
|
+
return;
|
|
116
|
+
proto.updateContent = state.originalUpdateContent;
|
|
117
|
+
delete proto[THINKING_GROUPING_PATCHED];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default function (pi: ExtensionAPI) {
|
|
121
|
+
const patch = installThinkingGroupingPatch();
|
|
122
|
+
pi.on("session_shutdown", () => uninstallThinkingGroupingPatch(patch));
|
|
123
|
+
}
|