@pi-kaush/pi-tool-call-markers 0.3.15 → 0.3.17
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 +22 -0
- package/README.md +19 -0
- package/package.json +1 -1
- package/src/index.ts +69 -25
- package/src/thinking-block-merger.ts +48 -18
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Include bash command previews in collapsed codemode rows, with the total
|
|
6
|
+
bash call count before the first command preview. Every nested tool shows
|
|
7
|
+
its count, including ×1, on its first preview. Distinct commands
|
|
8
|
+
no longer disappear behind a generic bash count. Decode truncated argument
|
|
9
|
+
previews safely, keep live updates and width limits, and preserve native
|
|
10
|
+
expansion.
|
|
11
|
+
|
|
12
|
+
- Draw subagent rows with the same faint rail as every other collapsed
|
|
13
|
+
tool call. They used a bold `│` of their own, and failed subagent plans
|
|
14
|
+
painted the rail in the error color.
|
|
15
|
+
- Stop the thinking spinner from forcing full transcript redraws. Changing
|
|
16
|
+
a line above the viewport makes the terminal clear and replay the whole
|
|
17
|
+
transcript, and the live label kept animating after answer text pushed it
|
|
18
|
+
out of view. The label now settles when the answer or a tool call starts,
|
|
19
|
+
so its duration is the real thinking time. With several thinking runs in
|
|
20
|
+
one message, earlier runs show a static `│ * Thought`, and the current
|
|
21
|
+
run animates only while it is the message's last line.
|
|
22
|
+
- Never animate over a click-revealed thinking trace or answer Markdown
|
|
23
|
+
that happens to equal the label.
|
|
24
|
+
- Keep `/toggle-info` replay from bringing back thinking that a message's
|
|
25
|
+
final update dropped.
|
|
26
|
+
|
|
5
27
|
- Fix assistant replies vanishing behind the thinking label. The label
|
|
6
28
|
restyle matched any text node containing the label, so a reply that quoted
|
|
7
29
|
`Thinking...` or `│ * Thought` was replaced by the label until Ctrl+T. Only
|
package/README.md
CHANGED
|
@@ -31,6 +31,25 @@ Collapsed tool rows use semantic theme colors with no gear, background fill, box
|
|
|
31
31
|
- **User-run `!` bash blocks.** Execution display is owned here: user-typed commands trade Pi's green rules for the railed prompt shell (dark surface, status-colored rail) that submitted prompts use.
|
|
32
32
|
- **Native expansion remains authoritative.** `Ctrl+O` restores Pi's full individual tool rendering, including complete results, custom renderers, and error details.
|
|
33
33
|
|
|
34
|
+
## Codemode commands
|
|
35
|
+
|
|
36
|
+
Collapsed codemode rows include bash command previews instead of only tool names:
|
|
37
|
+
|
|
38
|
+
```text
|
|
39
|
+
│ ¢ bash ×1 npm test
|
|
40
|
+
│ ¢ bash ×2 git status --short, bash npm test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The first preview for each tool shows its total call count, including ×1.
|
|
44
|
+
Distinct command previews remain separate. Identical previews appear only once.
|
|
45
|
+
Long commands truncate to the row width. Pi can also truncate the stored
|
|
46
|
+
argument preview; those commands keep an ellipsis. Calls without a usable
|
|
47
|
+
command preview keep their tool name. Other nested tools show their
|
|
48
|
+
name counts, including ×1, and script options follow the call list.
|
|
49
|
+
|
|
50
|
+
Previews update while scripts run. Click a row or use Ctrl+O to restore Pi's
|
|
51
|
+
native script, nested call status, and result details.
|
|
52
|
+
|
|
34
53
|
## Subagent plans
|
|
35
54
|
|
|
36
55
|
A recognized `subagent` call renders as an unboxed plan in the shared tool aesthetic, using the same `│` tool-call rail:
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,8 +24,7 @@ import { isQuestionToolCall, renderQuestionBlock } from "./question-block.ts";
|
|
|
24
24
|
import { sanitizeInline } from "./sanitize.ts";
|
|
25
25
|
|
|
26
26
|
const OUTER_INSET = 2;
|
|
27
|
-
const
|
|
28
|
-
const GROUP_CALL_MARKER = "│";
|
|
27
|
+
const TOOL_RAIL_MARKER = "│";
|
|
29
28
|
// Collapsed rows anchor with a glyph instead of repeating the tool name;
|
|
30
29
|
// tools that read as one family share a glyph, and unmapped tools fall back
|
|
31
30
|
// to `*`. A single space separates the glyph from the call content: the
|
|
@@ -867,28 +866,66 @@ function codemodeOptionsLabel(row: ToolExecutionRow): string | undefined {
|
|
|
867
866
|
return params.length > 0 ? params.join(", ") : undefined;
|
|
868
867
|
}
|
|
869
868
|
|
|
870
|
-
//
|
|
871
|
-
//
|
|
872
|
-
//
|
|
873
|
-
|
|
869
|
+
// Pi stores a JSON argument preview, capped at 200 characters. Long bash
|
|
870
|
+
// commands can therefore end inside a JSON string. Decode only complete
|
|
871
|
+
// escape sequences from a leading command field; never evaluate script text.
|
|
872
|
+
function codemodeBashCommand(args: unknown): string | undefined {
|
|
873
|
+
if (typeof args !== "string") return undefined;
|
|
874
|
+
try {
|
|
875
|
+
const parsed: unknown = JSON.parse(args);
|
|
876
|
+
return isRecord(parsed) && typeof parsed.command === "string"
|
|
877
|
+
? parsed.command
|
|
878
|
+
: undefined;
|
|
879
|
+
} catch {
|
|
880
|
+
if (!args.endsWith("...")) return undefined;
|
|
881
|
+
const prefix = args.slice(0, -3);
|
|
882
|
+
const match =
|
|
883
|
+
/^\{\s*"command"\s*:\s*"((?:[^"\\]|\\(?:["\\/bfnrt]|u[\da-fA-F]{4}))*)/.exec(
|
|
884
|
+
prefix,
|
|
885
|
+
);
|
|
886
|
+
if (!match) return undefined;
|
|
887
|
+
try {
|
|
888
|
+
const command: unknown = JSON.parse('"' + match[1] + '"');
|
|
889
|
+
const suffix = prefix[match[0].length] === '"' ? "" : "…";
|
|
890
|
+
return typeof command === "string" ? command + suffix : undefined;
|
|
891
|
+
} catch {
|
|
892
|
+
return undefined;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// Show each tool's total count, including ×1, on its first preview.
|
|
898
|
+
// Distinct previews remain visible; repeated previews take no extra space.
|
|
874
899
|
function codemodeCallsSummary(row: ToolExecutionRow): string | undefined {
|
|
875
900
|
const calls = row.result?.details?.calls;
|
|
876
901
|
if (!Array.isArray(calls) || calls.length === 0) return undefined;
|
|
877
902
|
const counts = new Map<string, number>();
|
|
903
|
+
const labels = new Map<string, { name: string; preview: string }>();
|
|
878
904
|
for (const call of calls) {
|
|
879
905
|
if (!isRecord(call)) continue;
|
|
880
906
|
const name = call.name;
|
|
881
907
|
if (typeof name !== "string" || name.length === 0) continue;
|
|
908
|
+
const command =
|
|
909
|
+
name === "bash" ? codemodeBashCommand(call.args) : undefined;
|
|
910
|
+
const preview = command === undefined ? "" : sanitizeInline(command).trim();
|
|
911
|
+
const label = preview ? name + " " + preview : name;
|
|
882
912
|
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
913
|
+
labels.set(label, { name, preview });
|
|
883
914
|
}
|
|
884
915
|
if (counts.size === 0) return undefined;
|
|
885
|
-
|
|
886
|
-
|
|
916
|
+
const shown = new Set<string>();
|
|
917
|
+
return [...labels.values()]
|
|
918
|
+
.map(({ name, preview }) => {
|
|
919
|
+
const count = counts.get(name) ?? 1;
|
|
920
|
+
const heading = !shown.has(name) ? name + " ×" + count : name;
|
|
921
|
+
shown.add(name);
|
|
922
|
+
return preview ? heading + " " + preview : heading;
|
|
923
|
+
})
|
|
887
924
|
.join(", ");
|
|
888
925
|
}
|
|
889
926
|
|
|
890
927
|
// Codemode's collapsed label pairs what the script did with how it ran:
|
|
891
|
-
// `read ×2, fffind · "max_output_tokens": 2500`. Without either part the row
|
|
928
|
+
// `read ×2, fffind ×1 · "max_output_tokens": 2500`. Without either part the row
|
|
892
929
|
// falls through to the ordinary summary paths.
|
|
893
930
|
function codemodeCallLabel(row: ToolExecutionRow): string | undefined {
|
|
894
931
|
if (row.toolName !== "codemode") return undefined;
|
|
@@ -898,11 +935,23 @@ function codemodeCallLabel(row: ToolExecutionRow): string | undefined {
|
|
|
898
935
|
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
899
936
|
}
|
|
900
937
|
|
|
938
|
+
// One anchor builder for every collapsed row: the faint rail, then the
|
|
939
|
+
// row's bold glyph when it has one — `│ ● src/a.ts`, `│ $ npm test`. A
|
|
940
|
+
// subagent row anchors on the rail alone and keeps its `│ subagent …`
|
|
941
|
+
// reading instead of a glyph.
|
|
942
|
+
function collapsedAnchor(
|
|
943
|
+
theme: ThemeLike,
|
|
944
|
+
color: string,
|
|
945
|
+
glyph: string | undefined,
|
|
946
|
+
): string {
|
|
947
|
+
const rail = `${fgCollapsedRail(theme, TOOL_RAIL_MARKER)} `;
|
|
948
|
+
return glyph === undefined
|
|
949
|
+
? rail
|
|
950
|
+
: `${rail}${fgCollapsed(theme, color, glyph, true)} `;
|
|
951
|
+
}
|
|
952
|
+
|
|
901
953
|
// The collapsed-row seam lives here and nowhere else: which anchor a row
|
|
902
|
-
// takes,
|
|
903
|
-
// Anchors always render bold and take no joining colon — `│ ● src/a.ts`,
|
|
904
|
-
// `│ $ npm test` — and subagent fallbacks keep their `│ subagent …`
|
|
905
|
-
// reading instead of a glyph. Callers append the outcome tail.
|
|
954
|
+
// takes, and the call text that follows it. Callers append the outcome tail.
|
|
906
955
|
function collapsedSeam(
|
|
907
956
|
row: ToolExecutionRow,
|
|
908
957
|
width: number,
|
|
@@ -911,17 +960,12 @@ function collapsedSeam(
|
|
|
911
960
|
): { anchor: string; content: string } {
|
|
912
961
|
const isBash = row.toolName === "bash";
|
|
913
962
|
const isSubagent = row.toolName === "subagent";
|
|
914
|
-
const glyph =
|
|
915
|
-
?
|
|
916
|
-
:
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
theme,
|
|
921
|
-
color,
|
|
922
|
-
glyph,
|
|
923
|
-
true,
|
|
924
|
-
)} `;
|
|
963
|
+
const glyph = isSubagent
|
|
964
|
+
? undefined
|
|
965
|
+
: isBash
|
|
966
|
+
? "$"
|
|
967
|
+
: (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
|
|
968
|
+
const anchor = collapsedAnchor(theme, color, glyph);
|
|
925
969
|
const budget = Math.max(1, width - visibleWidth(anchor));
|
|
926
970
|
// Self-rendered rows build their own `tool {args}` label — Pi's call title
|
|
927
971
|
// is gone once settled — while ordinary rows scrape the rendered call
|
|
@@ -1209,7 +1253,7 @@ function renderSubagentPlan(
|
|
|
1209
1253
|
const previewOf = (step: SubagentStep) =>
|
|
1210
1254
|
fgCollapsed(theme, detailColor, ` ${subagentStepPreview(step.task)}`);
|
|
1211
1255
|
|
|
1212
|
-
const marker =
|
|
1256
|
+
const marker = collapsedAnchor(theme, color, undefined);
|
|
1213
1257
|
const budget = Math.max(1, width - visibleWidth(marker));
|
|
1214
1258
|
const progress = subagentProgressText(row);
|
|
1215
1259
|
const outcome = failed
|
|
@@ -210,12 +210,27 @@ function unitalicizeSettledThinking(row: AssistantMessageRow): void {
|
|
|
210
210
|
// matches, and a candidate's plain text must equal the label exactly. Pi 1.0
|
|
211
211
|
// wraps each thinking run in a MouseRegion; each child is judged on its own,
|
|
212
212
|
// unwrapped when it has a wrapped inner.
|
|
213
|
-
function hiddenLabelNodes(
|
|
213
|
+
function hiddenLabelNodes(
|
|
214
|
+
row: AssistantMessageRow,
|
|
215
|
+
plainLabel: string,
|
|
216
|
+
): Array<{ node: TextLikeChild; trailing: boolean }> {
|
|
214
217
|
const children = row.contentContainer?.children;
|
|
215
218
|
if (!Array.isArray(children)) return [];
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
+
const labels: Array<{ node: TextLikeChild; trailing: boolean }> = [];
|
|
220
|
+
children.forEach((child, index) => {
|
|
221
|
+
const node = ((child as { child?: unknown } | undefined)?.child ??
|
|
222
|
+
child) as TextLikeChild | undefined;
|
|
223
|
+
if (
|
|
224
|
+
isMarkdownLike(node) ||
|
|
225
|
+
typeof node?.text !== "string" ||
|
|
226
|
+
typeof node.setText !== "function" ||
|
|
227
|
+
plainText(node.text) !== plainLabel
|
|
228
|
+
) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
labels.push({ node, trailing: index === children.length - 1 });
|
|
232
|
+
});
|
|
233
|
+
return labels;
|
|
219
234
|
}
|
|
220
235
|
|
|
221
236
|
// Duck-typed, not `instanceof`: a locally loaded extension can resolve its
|
|
@@ -257,8 +272,15 @@ function animateLiveLabel(node: TextLikeChild, startedAt: number): void {
|
|
|
257
272
|
};
|
|
258
273
|
}
|
|
259
274
|
|
|
275
|
+
// Pi gives every thinking run in a message the same label. Only the last
|
|
276
|
+
// run is the current one; earlier runs read as settled so a line above later
|
|
277
|
+
// content never changes. The current run animates only when it is the
|
|
278
|
+
// message's last line and no tool rows render below the message: changing
|
|
279
|
+
// a line above the viewport makes pi-tui clear and replay the transcript.
|
|
280
|
+
// Otherwise it holds one spinner frame until it settles.
|
|
260
281
|
function restyleHiddenThinkingLabel(
|
|
261
282
|
row: AssistantMessageRow,
|
|
283
|
+
message: AssistantMessageLike,
|
|
262
284
|
liveSince?: number,
|
|
263
285
|
): void {
|
|
264
286
|
if (
|
|
@@ -269,20 +291,28 @@ function restyleHiddenThinkingLabel(
|
|
|
269
291
|
}
|
|
270
292
|
const label = row.hiddenThinkingLabel;
|
|
271
293
|
if (!stylesThinkingLabels()) return;
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
294
|
+
const labels = hiddenLabelNodes(row, plainText(label));
|
|
295
|
+
const hasToolCalls =
|
|
296
|
+
Array.isArray(message.content) &&
|
|
297
|
+
message.content.some(
|
|
298
|
+
(block) => (block as { type?: unknown } | undefined)?.type === "toolCall",
|
|
299
|
+
);
|
|
300
|
+
labels.forEach(({ node, trailing }, index) => {
|
|
301
|
+
if (index < labels.length - 1) {
|
|
302
|
+
node.setText?.(visibleThoughtLabel(SETTLED_THOUGHT));
|
|
303
|
+
return;
|
|
282
304
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
305
|
+
if (liveSince === undefined) {
|
|
306
|
+
node.setText?.(visibleThoughtLabel(label));
|
|
307
|
+
} else if (trailing && !hasToolCalls) {
|
|
308
|
+
node.setText?.(visibleThoughtLabel(label));
|
|
309
|
+
animateLiveLabel(node, liveSince);
|
|
310
|
+
} else {
|
|
311
|
+
node.setText?.(
|
|
312
|
+
visibleThoughtLabel(liveThinkingLabel(liveSince, liveSince)),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
286
316
|
}
|
|
287
317
|
|
|
288
318
|
type ThinkingTiming = {
|
|
@@ -601,7 +631,7 @@ function installThinkingGroupingPatch():
|
|
|
601
631
|
timing && timingLive(streaming, timing)
|
|
602
632
|
? timing.startedAt
|
|
603
633
|
: undefined;
|
|
604
|
-
restyleHiddenThinkingLabel(this, liveSince);
|
|
634
|
+
restyleHiddenThinkingLabel(this, combined, liveSince);
|
|
605
635
|
}
|
|
606
636
|
} catch {
|
|
607
637
|
// Keep Pi's native label styling if the row shape changes.
|