@pi-kaush/pi-tool-call-markers 0.2.0 → 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 +3 -2
- package/README.md +14 -3
- package/package.json +1 -1
- package/src/index.ts +83 -62
package/CHANGELOG.md
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
|
|
7
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
8
|
- Show the real duration in successful bash summaries (`→ done · 2.3s`) instead of dropping the elapsed time on completion.
|
|
9
|
-
-
|
|
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.
|
|
10
11
|
- Only invalidate grouped-render caches on real state transitions, so bash's per-second ticks and resize invalidations stop busting them.
|
|
11
12
|
|
|
12
13
|
- Keep failed calls collapsed by default while retaining their native error background and Ctrl+O expansion.
|
|
13
14
|
- Add compact result tails for common successful tools in singleton and grouped summaries.
|
|
14
15
|
- Keep grouped bullets to one line and preserve the useful result tail on narrow terminals.
|
|
15
|
-
- Merge
|
|
16
|
+
- Merge calls across assistant turns as soon as they appear when no visible prose or thinking separates them.
|
|
16
17
|
- Move adjacent thinking-block merging to a separate extension entrypoint bundled in this package.
|
|
17
18
|
|
|
18
19
|
## 0.1.2
|
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@ 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
|
-
- **Running calls
|
|
14
|
-
- **
|
|
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.
|
|
15
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.
|
|
16
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
17
|
- **Errors stay compact and visibly failed.** A failed call keeps its own error-colored block and native collapsed detail until expanded.
|
|
@@ -39,6 +39,17 @@ pi \
|
|
|
39
39
|
-e ./extensions/pi-tool-call-markers/src/thinking-block-merger.ts
|
|
40
40
|
```
|
|
41
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
|
+
|
|
42
53
|
## Compatibility and risk
|
|
43
54
|
|
|
44
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;
|
|
@@ -65,11 +66,11 @@ type ToolExecutionRow = {
|
|
|
65
66
|
|
|
66
67
|
type PresentationPatchState = {
|
|
67
68
|
theme?: ThemeLike;
|
|
69
|
+
collapseParallel: boolean;
|
|
68
70
|
groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
|
|
71
|
+
liveRenderedRows: WeakSet<ToolExecutionRow>;
|
|
69
72
|
rowVersions: WeakMap<ToolExecutionRow, number>;
|
|
70
|
-
|
|
71
|
-
// never changes afterwards, so live output only ever grows downwards.
|
|
72
|
-
rowModes: WeakMap<ToolExecutionRow, "individual" | ToolExecutionRow[]>;
|
|
73
|
+
rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
|
|
73
74
|
rowSignatures: WeakMap<ToolExecutionRow, string>;
|
|
74
75
|
originalRender: (width: number) => string[];
|
|
75
76
|
originalUpdateDisplay: () => void;
|
|
@@ -91,6 +92,14 @@ type GroupRenderCache = {
|
|
|
91
92
|
width: number;
|
|
92
93
|
};
|
|
93
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
|
+
|
|
94
103
|
function stripAnsi(text: string): string {
|
|
95
104
|
return text.replace(ANSI_RE, "");
|
|
96
105
|
}
|
|
@@ -353,7 +362,7 @@ function isSettledToolRow(row: ToolExecutionRow): boolean {
|
|
|
353
362
|
return row.isPartial === false && !!row.result;
|
|
354
363
|
}
|
|
355
364
|
|
|
356
|
-
function
|
|
365
|
+
function hasToolSiblingInAssistantBatch(
|
|
357
366
|
children: unknown[],
|
|
358
367
|
index: number,
|
|
359
368
|
renderAt: (index: number) => string[],
|
|
@@ -366,16 +375,31 @@ function hasUnsettledToolInBatch(
|
|
|
366
375
|
) {
|
|
367
376
|
const candidate = children[candidateIndex];
|
|
368
377
|
if (isAssistantMessageRow(candidate)) break;
|
|
369
|
-
if (isToolExecutionRow(candidate))
|
|
370
|
-
if (!isSettledToolRow(candidate)) return true;
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
378
|
+
if (isToolExecutionRow(candidate)) return true;
|
|
373
379
|
if (renderAt(candidateIndex).some(hasVisibleContent)) break;
|
|
374
380
|
}
|
|
375
381
|
}
|
|
376
382
|
return false;
|
|
377
383
|
}
|
|
378
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
|
+
|
|
379
403
|
function renderComponent(component: unknown, width: number): string[] {
|
|
380
404
|
if (!component || typeof (component as ComponentLike).render !== "function")
|
|
381
405
|
return [];
|
|
@@ -502,6 +526,17 @@ function renderedOutcome(
|
|
|
502
526
|
return `${theme.fg("muted", "→")} ${theme.fg("success", summary)}`;
|
|
503
527
|
}
|
|
504
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
|
+
|
|
505
540
|
function truncatePlain(text: string, width: number, suffix = "…"): string {
|
|
506
541
|
const max = Math.max(0, width);
|
|
507
542
|
const plain = stripAnsi(text);
|
|
@@ -678,7 +713,7 @@ function groupedCallComponent(
|
|
|
678
713
|
previousToolName = row.toolName;
|
|
679
714
|
}
|
|
680
715
|
const call = renderedCallSummary(row, Math.max(1, width - 4), theme);
|
|
681
|
-
const outcome =
|
|
716
|
+
const outcome = renderedGroupedOutcome(row, theme);
|
|
682
717
|
lines.push(compactBulletLine(call, outcome, width, theme));
|
|
683
718
|
}
|
|
684
719
|
return lines;
|
|
@@ -742,9 +777,12 @@ function renderGroupedToolRows(
|
|
|
742
777
|
const themeSample =
|
|
743
778
|
theme.fg("toolTitle", "x") +
|
|
744
779
|
theme.fg("muted", "x") +
|
|
780
|
+
theme.bg("toolPendingBg", "x") +
|
|
745
781
|
theme.bg("toolSuccessBg", "x");
|
|
746
782
|
const cached = state.groupCache.get(row);
|
|
783
|
+
const hasLiveMembers = rows.some(isLiveRow);
|
|
747
784
|
if (
|
|
785
|
+
!hasLiveMembers &&
|
|
748
786
|
cached &&
|
|
749
787
|
cached.width === width &&
|
|
750
788
|
cached.themeSample === themeSample &&
|
|
@@ -802,13 +840,17 @@ function renderGroupedToolRows(
|
|
|
802
840
|
}
|
|
803
841
|
|
|
804
842
|
const decorated = decorateHeader(row, lines, width, theme);
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
+
}
|
|
812
854
|
return decorated;
|
|
813
855
|
}
|
|
814
856
|
|
|
@@ -829,45 +871,10 @@ function renderContainerWithToolGroups(
|
|
|
829
871
|
|
|
830
872
|
for (let index = 0; index < children.length; index++) {
|
|
831
873
|
const child = children[index];
|
|
832
|
-
if (
|
|
833
|
-
|
|
834
|
-
|
|
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.
|
|
869
|
-
if (hasUnsettledToolInBatch(children, index, renderAt)) {
|
|
870
|
-
presentation.rowModes.set(child, "individual");
|
|
874
|
+
if (
|
|
875
|
+
!isToolExecutionRow(child) ||
|
|
876
|
+
!isGroupableToolRow(child, children, index, renderAt, presentation)
|
|
877
|
+
) {
|
|
871
878
|
lines.push(...renderAt(index));
|
|
872
879
|
continue;
|
|
873
880
|
}
|
|
@@ -885,8 +892,16 @@ function renderContainerWithToolGroups(
|
|
|
885
892
|
continue;
|
|
886
893
|
}
|
|
887
894
|
if (isToolExecutionRow(candidate)) {
|
|
888
|
-
if (
|
|
889
|
-
|
|
895
|
+
if (
|
|
896
|
+
!isGroupableToolRow(
|
|
897
|
+
candidate,
|
|
898
|
+
children,
|
|
899
|
+
candidateIndex,
|
|
900
|
+
renderAt,
|
|
901
|
+
presentation,
|
|
902
|
+
)
|
|
903
|
+
)
|
|
904
|
+
break;
|
|
890
905
|
group.push(candidate);
|
|
891
906
|
lastMemberIndex = candidateIndex;
|
|
892
907
|
continue;
|
|
@@ -895,13 +910,16 @@ function renderContainerWithToolGroups(
|
|
|
895
910
|
}
|
|
896
911
|
|
|
897
912
|
if (group.length === 1) {
|
|
898
|
-
presentation.rowModes.set(child, "individual");
|
|
899
913
|
lines.push(...renderAt(index));
|
|
900
914
|
continue;
|
|
901
915
|
}
|
|
902
916
|
|
|
903
|
-
for (const member of group) presentation.
|
|
904
|
-
|
|
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));
|
|
905
923
|
index = lastMemberIndex;
|
|
906
924
|
}
|
|
907
925
|
|
|
@@ -998,9 +1016,11 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
998
1016
|
return undefined;
|
|
999
1017
|
|
|
1000
1018
|
const state: PresentationPatchState = {
|
|
1019
|
+
collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
|
|
1001
1020
|
groupCache: new WeakMap(),
|
|
1021
|
+
liveRenderedRows: new WeakSet(),
|
|
1002
1022
|
rowVersions: new WeakMap(),
|
|
1003
|
-
|
|
1023
|
+
rowGroups: new WeakMap(),
|
|
1004
1024
|
rowSignatures: new WeakMap(),
|
|
1005
1025
|
originalRender: proto.render,
|
|
1006
1026
|
originalUpdateDisplay: proto.updateDisplay,
|
|
@@ -1013,7 +1033,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1013
1033
|
// ticks and resize invalidations stop busting caches.
|
|
1014
1034
|
const signature = `${this.isPartial}|${this.expanded}|${this.result ? 1 : 0}|${this.result?.isError ? 1 : 0}`;
|
|
1015
1035
|
if (
|
|
1016
|
-
|
|
1036
|
+
state.rowGroups.has(this) ||
|
|
1017
1037
|
state.rowSignatures.get(this) !== signature
|
|
1018
1038
|
) {
|
|
1019
1039
|
state.rowSignatures.set(this, signature);
|
|
@@ -1032,6 +1052,7 @@ function installPresentationPatch(): PresentationPatchState | undefined {
|
|
|
1032
1052
|
this: ToolExecutionRow,
|
|
1033
1053
|
width: number,
|
|
1034
1054
|
): string[] {
|
|
1055
|
+
if (!isSettledToolRow(this)) state.liveRenderedRows.add(this);
|
|
1035
1056
|
const lines = state.originalRender.call(this, width);
|
|
1036
1057
|
try {
|
|
1037
1058
|
return decorateHeader(this, lines, width, state.theme);
|