@pi-kaush/pi-tool-call-markers 0.3.15 → 0.3.16

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Draw subagent rows with the same faint rail as every other collapsed
6
+ tool call. They used a bold `│` of their own, and failed subagent plans
7
+ painted the rail in the error color.
8
+ - Stop the thinking spinner from forcing full transcript redraws. Changing
9
+ a line above the viewport makes the terminal clear and replay the whole
10
+ transcript, and the live label kept animating after answer text pushed it
11
+ out of view. The label now settles when the answer or a tool call starts,
12
+ so its duration is the real thinking time. With several thinking runs in
13
+ one message, earlier runs show a static `│ * Thought`, and the current
14
+ run animates only while it is the message's last line.
15
+ - Never animate over a click-revealed thinking trace or answer Markdown
16
+ that happens to equal the label.
17
+ - Keep `/toggle-info` replay from bringing back thinking that a message's
18
+ final update dropped.
19
+
5
20
  - Fix assistant replies vanishing behind the thinking label. The label
6
21
  restyle matched any text node containing the label, so a reply that quoted
7
22
  `Thinking...` or `│ * Thought` was replaced by the label until Ctrl+T. Only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
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",
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 SUBAGENT_MARKER = "│";
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
@@ -898,11 +897,23 @@ function codemodeCallLabel(row: ToolExecutionRow): string | undefined {
898
897
  return parts.length > 0 ? parts.join(" · ") : undefined;
899
898
  }
900
899
 
900
+ // One anchor builder for every collapsed row: the faint rail, then the
901
+ // row's bold glyph when it has one — `│ ● src/a.ts`, `│ $ npm test`. A
902
+ // subagent row anchors on the rail alone and keeps its `│ subagent …`
903
+ // reading instead of a glyph.
904
+ function collapsedAnchor(
905
+ theme: ThemeLike,
906
+ color: string,
907
+ glyph: string | undefined,
908
+ ): string {
909
+ const rail = `${fgCollapsedRail(theme, TOOL_RAIL_MARKER)} `;
910
+ return glyph === undefined
911
+ ? rail
912
+ : `${rail}${fgCollapsed(theme, color, glyph, true)} `;
913
+ }
914
+
901
915
  // The collapsed-row seam lives here and nowhere else: which anchor a row
902
- // takes, how that anchor is styled, and the call text that follows it.
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.
916
+ // takes, and the call text that follows it. Callers append the outcome tail.
906
917
  function collapsedSeam(
907
918
  row: ToolExecutionRow,
908
919
  width: number,
@@ -911,17 +922,12 @@ function collapsedSeam(
911
922
  ): { anchor: string; content: string } {
912
923
  const isBash = row.toolName === "bash";
913
924
  const isSubagent = row.toolName === "subagent";
914
- const glyph = isBash
915
- ? "$"
916
- : (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
917
- const anchor = isSubagent
918
- ? `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `
919
- : `${fgCollapsedRail(theme, GROUP_CALL_MARKER)} ${fgCollapsed(
920
- theme,
921
- color,
922
- glyph,
923
- true,
924
- )} `;
925
+ const glyph = isSubagent
926
+ ? undefined
927
+ : isBash
928
+ ? "$"
929
+ : (TOOL_CALL_GLYPHS.get(row.toolName ?? "") ?? "*");
930
+ const anchor = collapsedAnchor(theme, color, glyph);
925
931
  const budget = Math.max(1, width - visibleWidth(anchor));
926
932
  // Self-rendered rows build their own `tool {args}` label — Pi's call title
927
933
  // is gone once settled — while ordinary rows scrape the rendered call
@@ -1209,7 +1215,7 @@ function renderSubagentPlan(
1209
1215
  const previewOf = (step: SubagentStep) =>
1210
1216
  fgCollapsed(theme, detailColor, ` ${subagentStepPreview(step.task)}`);
1211
1217
 
1212
- const marker = `${fgCollapsed(theme, color, SUBAGENT_MARKER, true)} `;
1218
+ const marker = collapsedAnchor(theme, color, undefined);
1213
1219
  const budget = Math.max(1, width - visibleWidth(marker));
1214
1220
  const progress = subagentProgressText(row);
1215
1221
  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(row: AssistantMessageRow): unknown[] {
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
- return children
217
- .map((child) => (child as { child?: unknown } | undefined)?.child ?? child)
218
- .filter((node) => !isMarkdownLike(node));
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 styled = visibleThoughtLabel(label);
273
- const plainLabel = plainText(label);
274
- for (const child of hiddenLabelNodes(row)) {
275
- const textChild = child as TextLikeChild | undefined;
276
- if (
277
- typeof textChild?.text !== "string" ||
278
- typeof textChild.setText !== "function" ||
279
- plainText(textChild.text) !== plainLabel
280
- ) {
281
- continue;
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
- textChild.setText(styled);
284
- if (liveSince !== undefined) animateLiveLabel(textChild, liveSince);
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.