@pi-kaush/pi-tool-call-markers 0.1.3 → 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 +7 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/index.ts +169 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
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
|
+
- 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
|
+
|
|
5
12
|
- Keep failed calls collapsed by default while retaining their native error background and Ctrl+O expansion.
|
|
6
13
|
- Add compact result tails for common successful tools in singleton and grouped summaries.
|
|
7
14
|
- Keep grouped bullets to one line and preserve the useful result tail on narrow terminals.
|
package/README.md
CHANGED
|
@@ -10,9 +10,10 @@ When several tool calls of the same type succeed in a row, Pi normally renders e
|
|
|
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 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
|
+
- **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
|
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -50,6 +50,7 @@ type ToolExecutionRow = {
|
|
|
50
50
|
content?: Array<{ type?: unknown; text?: unknown }>;
|
|
51
51
|
details?: Record<string, unknown>;
|
|
52
52
|
};
|
|
53
|
+
rendererState?: { startedAt?: unknown; endedAt?: unknown };
|
|
53
54
|
contentBox?: ComponentContainer;
|
|
54
55
|
contentText?: TextComponent;
|
|
55
56
|
selfRenderContainer?: ComponentContainer;
|
|
@@ -66,6 +67,10 @@ type PresentationPatchState = {
|
|
|
66
67
|
theme?: ThemeLike;
|
|
67
68
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
68
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>;
|
|
69
74
|
originalRender: (width: number) => string[];
|
|
70
75
|
originalUpdateDisplay: () => void;
|
|
71
76
|
patchedRender?: (width: number) => string[];
|
|
@@ -227,6 +232,113 @@ function hasImageResult(row: ToolExecutionRow): boolean {
|
|
|
227
232
|
);
|
|
228
233
|
}
|
|
229
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
|
+
|
|
230
342
|
function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
|
|
231
343
|
return (
|
|
232
344
|
row.expanded === false &&
|
|
@@ -348,8 +460,10 @@ function outcomeSummary(row: ToolExecutionRow): string | undefined {
|
|
|
348
460
|
|
|
349
461
|
const text = resultText(row);
|
|
350
462
|
switch (row.toolName) {
|
|
351
|
-
case "bash":
|
|
352
|
-
|
|
463
|
+
case "bash": {
|
|
464
|
+
const duration = bashDurationText(row);
|
|
465
|
+
return duration ? `done · ${duration}` : "done";
|
|
466
|
+
}
|
|
353
467
|
case "read": {
|
|
354
468
|
const lines = readLineCount(row, text);
|
|
355
469
|
return `${lines} ${lines === 1 ? "line" : "lines"}`;
|
|
@@ -719,7 +833,41 @@ function renderContainerWithToolGroups(
|
|
|
719
833
|
lines.push(...renderAt(index));
|
|
720
834
|
continue;
|
|
721
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.
|
|
722
869
|
if (hasUnsettledToolInBatch(children, index, renderAt)) {
|
|
870
|
+
presentation.rowModes.set(child, "individual");
|
|
723
871
|
lines.push(...renderAt(index));
|
|
724
872
|
continue;
|
|
725
873
|
}
|
|
@@ -738,6 +886,7 @@ function renderContainerWithToolGroups(
|
|
|
738
886
|
}
|
|
739
887
|
if (isToolExecutionRow(candidate)) {
|
|
740
888
|
if (!isCollapsibleSuccess(candidate)) break;
|
|
889
|
+
if (presentation.rowModes.has(candidate)) break;
|
|
741
890
|
group.push(candidate);
|
|
742
891
|
lastMemberIndex = candidateIndex;
|
|
743
892
|
continue;
|
|
@@ -746,10 +895,12 @@ function renderContainerWithToolGroups(
|
|
|
746
895
|
}
|
|
747
896
|
|
|
748
897
|
if (group.length === 1) {
|
|
898
|
+
presentation.rowModes.set(child, "individual");
|
|
749
899
|
lines.push(...renderAt(index));
|
|
750
900
|
continue;
|
|
751
901
|
}
|
|
752
902
|
|
|
903
|
+
for (const member of group) presentation.rowModes.set(member, group);
|
|
753
904
|
lines.push(...renderGroupedToolRows(child, group, width, presentation));
|
|
754
905
|
index = lastMemberIndex;
|
|
755
906
|
}
|
|
@@ -849,17 +1000,30 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
849
1000
|
const state: PresentationPatchState = {
|
|
850
1001
|
groupCache: new WeakMap(),
|
|
851
1002
|
rowVersions: new WeakMap(),
|
|
1003
|
+
rowModes: new WeakMap(),
|
|
1004
|
+
rowSignatures: new WeakMap(),
|
|
852
1005
|
originalRender: proto.render,
|
|
853
1006
|
originalUpdateDisplay: proto.updateDisplay,
|
|
854
1007
|
};
|
|
855
1008
|
const patchedUpdateDisplay = function updateDisplayWithCollapsedResult(
|
|
856
1009
|
this: ToolExecutionRow,
|
|
857
1010
|
): void {
|
|
858
|
-
|
|
859
|
-
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
|
+
}
|
|
860
1023
|
state.originalUpdateDisplay.call(this);
|
|
861
1024
|
try {
|
|
862
|
-
|
|
1025
|
+
if (isLiveRow(this)) capLiveRowDisplay(this, state.theme);
|
|
1026
|
+
else collapseSuccessfulResult(this, state.theme);
|
|
863
1027
|
} catch {
|
|
864
1028
|
// Presentation is cosmetic; preserve Pi's renderer if its internals change.
|
|
865
1029
|
}
|