@pi-kaush/pi-tool-call-markers 0.1.3 → 0.2.1
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 +9 -1
- package/README.md +15 -3
- package/package.json +1 -1
- package/src/index.ts +210 -25
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
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
|
+
- Group adjacent calls while they are still running, so new bullets grow downward and successful settlement updates outcomes in place without upward reflow.
|
|
10
|
+
- Add `PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL`, enabled by default, to keep same-assistant-message calls individual while still grouping sequential calls across quiet turns.
|
|
11
|
+
- Only invalidate grouped-render caches on real state transitions, so bash's per-second ticks and resize invalidations stop busting them.
|
|
12
|
+
|
|
5
13
|
- Keep failed calls collapsed by default while retaining their native error background and Ctrl+O expansion.
|
|
6
14
|
- Add compact result tails for common successful tools in singleton and grouped summaries.
|
|
7
15
|
- Keep grouped bullets to one line and preserve the useful result tail on narrow terminals.
|
|
8
|
-
- Merge
|
|
16
|
+
- Merge calls across assistant turns as soon as they appear when no visible prose or thinking separates them.
|
|
9
17
|
- Move adjacent thinking-block merging to a separate extension entrypoint bundled in this package.
|
|
10
18
|
|
|
11
19
|
## 0.1.2
|
package/README.md
CHANGED
|
@@ -4,15 +4,16 @@ Collapse Pi's adjacent successful tool calls into one compact, gear-headed block
|
|
|
4
4
|
|
|
5
5
|
## What it changes
|
|
6
6
|
|
|
7
|
-
When several tool calls
|
|
7
|
+
When several tool calls run 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
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
12
|
- **One-line, width-safe summaries.** Long targets truncate before their useful outcome tail instead of wrapping into taller blocks.
|
|
13
|
-
- **
|
|
13
|
+
- **Running calls group immediately.** Adjacent calls become bullets as they appear, and each pending marker or elapsed `bash` time updates to the final outcome in place. Successful settlement does not shrink the block, so the transcript only grows downwards.
|
|
14
|
+
- **Sequential calls merge across quiet turns.** A later call joins the existing group immediately when no visible prose or thinking separates it; visible assistant content remains a hard boundary.
|
|
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
|
+
- **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.
|
|
16
17
|
- **Errors stay compact and visibly failed.** A failed call keeps its own error-colored block and native collapsed detail until expanded.
|
|
17
18
|
- **Ctrl+O restores full blocks.** Expanding tools (`setToolsExpanded(true)`) brings back Pi's individual full blocks, including complete error details and successful results.
|
|
18
19
|
|
|
@@ -38,6 +39,17 @@ pi \
|
|
|
38
39
|
-e ./extensions/pi-tool-call-markers/src/thinking-block-merger.ts
|
|
39
40
|
```
|
|
40
41
|
|
|
42
|
+
## Configuration
|
|
43
|
+
|
|
44
|
+
Grouping calls from the same assistant message is enabled by default. Pi normally executes those calls in parallel. To keep those same-message calls as individual compact rows while continuing to group sequential calls across quiet turns, start Pi with:
|
|
45
|
+
|
|
46
|
+
```fish
|
|
47
|
+
set -lx PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL 0
|
|
48
|
+
pi
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`0`, `false`, `no`, and `off` disable parallel grouping. `1`, `true`, `yes`, and `on` enable it. The value is read when the extension loads.
|
|
52
|
+
|
|
41
53
|
## Compatibility and risk
|
|
42
54
|
|
|
43
55
|
The tool presentation entrypoint currently relies on **guarded, reversible prototype patches** against two Pi component classes:
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ const LEGACY_PRESENTATION_PATCHED = Symbol.for("kg.pi.toolPresentation.v2");
|
|
|
18
18
|
const GROUPING_PATCHED = Symbol.for("kg.pi.toolGrouping.v1");
|
|
19
19
|
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
20
20
|
const BOLD_ON_RE = /\u001b\[1m/g;
|
|
21
|
+
const COLLAPSE_PARALLEL_ENV = "PI_TOOL_CALL_MARKERS_COLLAPSE_PARALLEL";
|
|
21
22
|
|
|
22
23
|
type ThemeLike = {
|
|
23
24
|
bold(text: string): string;
|
|
@@ -50,6 +51,7 @@ type ToolExecutionRow = {
|
|
|
50
51
|
content?: Array<{ type?: unknown; text?: unknown }>;
|
|
51
52
|
details?: Record<string, unknown>;
|
|
52
53
|
};
|
|
54
|
+
rendererState?: { startedAt?: unknown; endedAt?: unknown };
|
|
53
55
|
contentBox?: ComponentContainer;
|
|
54
56
|
contentText?: TextComponent;
|
|
55
57
|
selfRenderContainer?: ComponentContainer;
|
|
@@ -64,8 +66,12 @@ type ToolExecutionRow = {
|
|
|
64
66
|
|
|
65
67
|
type PresentationPatchState = {
|
|
66
68
|
theme?: ThemeLike;
|
|
69
|
+
collapseParallel: boolean;
|
|
67
70
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
71
|
+
liveRenderedRows: WeakSet<ToolExecutionRow>;
|
|
68
72
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
73
|
+
rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
|
|
74
|
+
rowSignatures: WeakMap<ToolExecutionRow, string>;
|
|
69
75
|
originalRender: (width: number) => string[];
|
|
70
76
|
originalUpdateDisplay: () => void;
|
|
71
77
|
patchedRender?: (width: number) => string[];
|
|
@@ -86,6 +92,14 @@ type GroupRenderCache = {
|
|
|
86
92
|
width: number;
|
|
87
93
|
};
|
|
88
94
|
|
|
95
|
+
function envEnabled(name: string, defaultValue: boolean): boolean {
|
|
96
|
+
const value = process.env[name]?.trim().toLowerCase();
|
|
97
|
+
if (!value) return defaultValue;
|
|
98
|
+
if (["1", "true", "yes", "on"].includes(value)) return true;
|
|
99
|
+
if (["0", "false", "no", "off"].includes(value)) return false;
|
|
100
|
+
return defaultValue;
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
function stripAnsi(text: string): string {
|
|
90
104
|
return text.replace(ANSI_RE, "");
|
|
91
105
|
}
|
|
@@ -227,6 +241,113 @@ function hasImageResult(row: ToolExecutionRow): boolean {
|
|
|
227
241
|
);
|
|
228
242
|
}
|
|
229
243
|
|
|
244
|
+
function formatDuration(ms: number): string {
|
|
245
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function bashDurationText(row: ToolExecutionRow): string | undefined {
|
|
249
|
+
const startedAt = row.rendererState?.startedAt;
|
|
250
|
+
const endedAt = row.rendererState?.endedAt;
|
|
251
|
+
if (
|
|
252
|
+
typeof startedAt !== "number" ||
|
|
253
|
+
typeof endedAt !== "number" ||
|
|
254
|
+
!Number.isFinite(startedAt) ||
|
|
255
|
+
!Number.isFinite(endedAt)
|
|
256
|
+
)
|
|
257
|
+
return undefined;
|
|
258
|
+
return formatDuration(Math.max(0, endedAt - startedAt));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function liveElapsedText(row: ToolExecutionRow): string | undefined {
|
|
262
|
+
if (row.toolName !== "bash") return undefined;
|
|
263
|
+
const startedAt = row.rendererState?.startedAt;
|
|
264
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt))
|
|
265
|
+
return undefined;
|
|
266
|
+
return formatDuration(Math.max(0, Date.now() - startedAt));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function isLiveRow(row: ToolExecutionRow): boolean {
|
|
270
|
+
return (
|
|
271
|
+
row.expanded === false &&
|
|
272
|
+
(row.isPartial !== false || !row.result) &&
|
|
273
|
+
row.getRenderShell?.() !== "self" &&
|
|
274
|
+
!hasImageResult(row)
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function liveTailText(
|
|
279
|
+
row: ToolExecutionRow,
|
|
280
|
+
droppedVisible: boolean,
|
|
281
|
+
theme?: ThemeLike,
|
|
282
|
+
): string | undefined {
|
|
283
|
+
if (!theme) return undefined;
|
|
284
|
+
const elapsed = liveElapsedText(row);
|
|
285
|
+
if (!droppedVisible && !elapsed) return undefined;
|
|
286
|
+
const parts: string[] = [];
|
|
287
|
+
if (droppedVisible) parts.push("…");
|
|
288
|
+
if (elapsed) parts.push("·", elapsed);
|
|
289
|
+
return theme.fg("muted", parts.join(" "));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function fitLiveHeader(
|
|
293
|
+
header: string,
|
|
294
|
+
tail: string,
|
|
295
|
+
width: number,
|
|
296
|
+
droppedVisible: boolean,
|
|
297
|
+
): string {
|
|
298
|
+
const max = Math.max(1, width);
|
|
299
|
+
const head = header.trimEnd();
|
|
300
|
+
if (visibleWidth(head) + visibleWidth(tail) + 1 <= max)
|
|
301
|
+
return `${head} ${tail}`;
|
|
302
|
+
const headWidth = Math.max(1, max - visibleWidth(tail) - 1);
|
|
303
|
+
const headSuffix = droppedVisible ? "" : "…";
|
|
304
|
+
return `${truncateToWidth(head, headWidth, headSuffix, false)} ${tail}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// While a call streams or runs, pin it to a single header line with the
|
|
308
|
+
// elapsed time inline, so the block never grows and never collapses on
|
|
309
|
+
// completion; the header text simply settles into the outcome summary. The
|
|
310
|
+
// cap is composed before the Box applies background and padding, so the live
|
|
311
|
+
// line keeps the tool background edge to edge and the block's bottom padding.
|
|
312
|
+
function capLiveRowDisplay(row: ToolExecutionRow, theme?: ThemeLike): void {
|
|
313
|
+
if (row.hasRendererDefinition?.()) {
|
|
314
|
+
const container = row.contentBox;
|
|
315
|
+
if (!container || !Array.isArray(container.children)) return;
|
|
316
|
+
const hadExtraChildren = container.children.length > 1;
|
|
317
|
+
removeResultComponent(container);
|
|
318
|
+
const original = container.children[0] as ComponentLike | undefined;
|
|
319
|
+
if (!original || typeof original.render !== "function") return;
|
|
320
|
+
container.children[0] = {
|
|
321
|
+
render(width: number): string[] {
|
|
322
|
+
const lines = original.render(width);
|
|
323
|
+
const headerIndex = lines.findIndex(hasVisibleContent);
|
|
324
|
+
if (headerIndex === -1) return lines;
|
|
325
|
+
const header = lines[headerIndex] ?? "";
|
|
326
|
+
const droppedVisible =
|
|
327
|
+
hadExtraChildren ||
|
|
328
|
+
lines.slice(headerIndex + 1).some(hasVisibleContent);
|
|
329
|
+
const tail = liveTailText(row, droppedVisible, theme);
|
|
330
|
+
return [
|
|
331
|
+
tail ? fitLiveHeader(header, tail, width, droppedVisible) : header,
|
|
332
|
+
];
|
|
333
|
+
},
|
|
334
|
+
invalidate() {
|
|
335
|
+
original.invalidate();
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const text = row.contentText;
|
|
342
|
+
if (typeof text?.text !== "string" || typeof text.setText !== "function")
|
|
343
|
+
return;
|
|
344
|
+
const lines = text.text.split("\n");
|
|
345
|
+
const title = lines[0] ?? "";
|
|
346
|
+
const droppedVisible = lines.slice(1).some((line) => line.trim().length > 0);
|
|
347
|
+
const tail = liveTailText(row, droppedVisible, theme);
|
|
348
|
+
text.setText(tail ? `${title} ${tail}` : title);
|
|
349
|
+
}
|
|
350
|
+
|
|
230
351
|
function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
|
|
231
352
|
return (
|
|
232
353
|
row.expanded === false &&
|
|
@@ -241,7 +362,7 @@ function isSettledToolRow(row: ToolExecutionRow): boolean {
|
|
|
241
362
|
return row.isPartial === false && !!row.result;
|
|
242
363
|
}
|
|
243
364
|
|
|
244
|
-
function
|
|
365
|
+
function hasToolSiblingInAssistantBatch(
|
|
245
366
|
children: unknown[],
|
|
246
367
|
index: number,
|
|
247
368
|
renderAt: (index: number) => string[],
|
|
@@ -254,16 +375,31 @@ function hasUnsettledToolInBatch(
|
|
|
254
375
|
) {
|
|
255
376
|
const candidate = children[candidateIndex];
|
|
256
377
|
if (isAssistantMessageRow(candidate)) break;
|
|
257
|
-
if (isToolExecutionRow(candidate))
|
|
258
|
-
if (!isSettledToolRow(candidate)) return true;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
378
|
+
if (isToolExecutionRow(candidate)) return true;
|
|
261
379
|
if (renderAt(candidateIndex).some(hasVisibleContent)) break;
|
|
262
380
|
}
|
|
263
381
|
}
|
|
264
382
|
return false;
|
|
265
383
|
}
|
|
266
384
|
|
|
385
|
+
function isGroupableToolRow(
|
|
386
|
+
row: ToolExecutionRow,
|
|
387
|
+
children: unknown[],
|
|
388
|
+
index: number,
|
|
389
|
+
renderAt: (index: number) => string[],
|
|
390
|
+
state: PresentationPatchState,
|
|
391
|
+
): boolean {
|
|
392
|
+
if (!isLiveRow(row) && !isCollapsibleSuccess(row)) return false;
|
|
393
|
+
// Self-rendered tools can change height while active. Keep live instances
|
|
394
|
+
// individual so settling never collapses an already-painted preview.
|
|
395
|
+
if (row.getRenderShell?.() === "self" && state.liveRenderedRows.has(row))
|
|
396
|
+
return false;
|
|
397
|
+
return (
|
|
398
|
+
state.collapseParallel ||
|
|
399
|
+
!hasToolSiblingInAssistantBatch(children, index, renderAt)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
267
403
|
function renderComponent(component: unknown, width: number): string[] {
|
|
268
404
|
if (!component || typeof (component as ComponentLike).render !== "function")
|
|
269
405
|
return [];
|
|
@@ -348,8 +484,10 @@ function outcomeSummary(row: ToolExecutionRow): string | undefined {
|
|
|
348
484
|
|
|
349
485
|
const text = resultText(row);
|
|
350
486
|
switch (row.toolName) {
|
|
351
|
-
case "bash":
|
|
352
|
-
|
|
487
|
+
case "bash": {
|
|
488
|
+
const duration = bashDurationText(row);
|
|
489
|
+
return duration ? `done · ${duration}` : "done";
|
|
490
|
+
}
|
|
353
491
|
case "read": {
|
|
354
492
|
const lines = readLineCount(row, text);
|
|
355
493
|
return `${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
@@ -388,6 +526,17 @@ function renderedOutcome(
|
|
|
388
526
|
return `${theme.fg("muted", "→")} ${theme.fg("success", summary)}`;
|
|
389
527
|
}
|
|
390
528
|
|
|
529
|
+
function renderedGroupedOutcome(
|
|
530
|
+
row: ToolExecutionRow,
|
|
531
|
+
theme: ThemeLike,
|
|
532
|
+
): string | undefined {
|
|
533
|
+
const outcome = renderedOutcome(row, theme);
|
|
534
|
+
if (outcome) return outcome;
|
|
535
|
+
if (!isLiveRow(row)) return undefined;
|
|
536
|
+
const elapsed = liveElapsedText(row);
|
|
537
|
+
return theme.fg("muted", elapsed ? `· ${elapsed}` : "…");
|
|
538
|
+
}
|
|
539
|
+
|
|
391
540
|
function truncatePlain(text: string, width: number, suffix = "…"): string {
|
|
392
541
|
const max = Math.max(0, width);
|
|
393
542
|
const plain = stripAnsi(text);
|
|
@@ -564,7 +713,7 @@ function groupedCallComponent(
|
|
|
564
713
|
previousToolName = row.toolName;
|
|
565
714
|
}
|
|
566
715
|
const call = renderedCallSummary(row, Math.max(1, width - 4), theme);
|
|
567
|
-
const outcome =
|
|
716
|
+
const outcome = renderedGroupedOutcome(row, theme);
|
|
568
717
|
lines.push(compactBulletLine(call, outcome, width, theme));
|
|
569
718
|
}
|
|
570
719
|
return lines;
|
|
@@ -628,9 +777,12 @@ function renderGroupedToolRows(
|
|
|
628
777
|
const themeSample =
|
|
629
778
|
theme.fg("toolTitle", "x") +
|
|
630
779
|
theme.fg("muted", "x") +
|
|
780
|
+
theme.bg("toolPendingBg", "x") +
|
|
631
781
|
theme.bg("toolSuccessBg", "x");
|
|
632
782
|
const cached = state.groupCache.get(row);
|
|
783
|
+
const hasLiveMembers = rows.some(isLiveRow);
|
|
633
784
|
if (
|
|
785
|
+
!hasLiveMembers &&
|
|
634
786
|
cached &&
|
|
635
787
|
cached.width === width &&
|
|
636
788
|
cached.themeSample === themeSample &&
|
|
@@ -688,13 +840,17 @@ function renderGroupedToolRows(
|
|
|
688
840
|
}
|
|
689
841
|
|
|
690
842
|
const decorated = decorateHeader(row, lines, width, theme);
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
843
|
+
if (hasLiveMembers) {
|
|
844
|
+
state.groupCache.delete(row);
|
|
845
|
+
} else {
|
|
846
|
+
state.groupCache.set(row, {
|
|
847
|
+
lines: decorated,
|
|
848
|
+
members: [...rows],
|
|
849
|
+
memberVersions: rows.map((member) => state.rowVersions.get(member) ?? 0),
|
|
850
|
+
themeSample,
|
|
851
|
+
width,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
698
854
|
return decorated;
|
|
699
855
|
}
|
|
700
856
|
|
|
@@ -715,11 +871,10 @@ function renderContainerWithToolGroups(
|
|
|
715
871
|
|
|
716
872
|
for (let index = 0; index < children.length; index++) {
|
|
717
873
|
const child = children[index];
|
|
718
|
-
if (
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if (hasUnsettledToolInBatch(children, index, renderAt)) {
|
|
874
|
+
if (
|
|
875
|
+
!isToolExecutionRow(child) ||
|
|
876
|
+
!isGroupableToolRow(child, children, index, renderAt, presentation)
|
|
877
|
+
) {
|
|
723
878
|
lines.push(...renderAt(index));
|
|
724
879
|
continue;
|
|
725
880
|
}
|
|
@@ -737,7 +892,16 @@ function renderContainerWithToolGroups(
|
|
|
737
892
|
continue;
|
|
738
893
|
}
|
|
739
894
|
if (isToolExecutionRow(candidate)) {
|
|
740
|
-
if (
|
|
895
|
+
if (
|
|
896
|
+
!isGroupableToolRow(
|
|
897
|
+
candidate,
|
|
898
|
+
children,
|
|
899
|
+
candidateIndex,
|
|
900
|
+
renderAt,
|
|
901
|
+
presentation,
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
break;
|
|
741
905
|
group.push(candidate);
|
|
742
906
|
lastMemberIndex = candidateIndex;
|
|
743
907
|
continue;
|
|
@@ -750,7 +914,12 @@ function renderContainerWithToolGroups(
|
|
|
750
914
|
continue;
|
|
751
915
|
}
|
|
752
916
|
|
|
753
|
-
|
|
917
|
+
for (const member of group) presentation.rowGroups.set(member, group);
|
|
918
|
+
// An active member supplies the pending background. Once every call
|
|
919
|
+
// settles, the first member supplies the success background. Either way,
|
|
920
|
+
// the grouped row keeps the same number of lines.
|
|
921
|
+
const shellRow = group.find(isLiveRow) ?? child;
|
|
922
|
+
lines.push(...renderGroupedToolRows(shellRow, group, width, presentation));
|
|
754
923
|
index = lastMemberIndex;
|
|
755
924
|
}
|
|
756
925
|
|
|
@@ -847,19 +1016,34 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
847
1016
|
return undefined;
|
|
848
1017
|
|
|
849
1018
|
const state: PresentationPatchState = {
|
|
1019
|
+
collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
|
|
850
1020
|
groupCache: new WeakMap(),
|
|
1021
|
+
liveRenderedRows: new WeakSet(),
|
|
851
1022
|
rowVersions: new WeakMap(),
|
|
1023
|
+
rowGroups: new WeakMap(),
|
|
1024
|
+
rowSignatures: new WeakMap(),
|
|
852
1025
|
originalRender: proto.render,
|
|
853
1026
|
originalUpdateDisplay: proto.updateDisplay,
|
|
854
1027
|
};
|
|
855
1028
|
const patchedUpdateDisplay = function updateDisplayWithCollapsedResult(
|
|
856
1029
|
this: ToolExecutionRow,
|
|
857
1030
|
): void {
|
|
858
|
-
|
|
859
|
-
state
|
|
1031
|
+
// Group members always bump so their leader's cache refreshes; other
|
|
1032
|
+
// rows only bump on state transitions, so bash's per-second invalidate
|
|
1033
|
+
// ticks and resize invalidations stop busting caches.
|
|
1034
|
+
const signature = `${this.isPartial}|${this.expanded}|${this.result ? 1 : 0}|${this.result?.isError ? 1 : 0}`;
|
|
1035
|
+
if (
|
|
1036
|
+
state.rowGroups.has(this) ||
|
|
1037
|
+
state.rowSignatures.get(this) !== signature
|
|
1038
|
+
) {
|
|
1039
|
+
state.rowSignatures.set(this, signature);
|
|
1040
|
+
state.rowVersions.set(this, (state.rowVersions.get(this) ?? 0) + 1);
|
|
1041
|
+
state.groupCache.delete(this);
|
|
1042
|
+
}
|
|
860
1043
|
state.originalUpdateDisplay.call(this);
|
|
861
1044
|
try {
|
|
862
|
-
|
|
1045
|
+
if (isLiveRow(this)) capLiveRowDisplay(this, state.theme);
|
|
1046
|
+
else collapseSuccessfulResult(this, state.theme);
|
|
863
1047
|
} catch {
|
|
864
1048
|
// Presentation is cosmetic; preserve Pi's renderer if its internals change.
|
|
865
1049
|
}
|
|
@@ -868,6 +1052,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
868
1052
|
this: ToolExecutionRow,
|
|
869
1053
|
width: number,
|
|
870
1054
|
): string[] {
|
|
1055
|
+
if (!isSettledToolRow(this)) state.liveRenderedRows.add(this);
|
|
871
1056
|
const lines = state.originalRender.call(this, width);
|
|
872
1057
|
try {
|
|
873
1058
|
return decorateHeader(this, lines, width, state.theme);
|