@pi-kaush/pi-tool-call-markers 0.2.2 → 0.2.4

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.
Files changed (3) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/index.ts +349 -32
package/README.md CHANGED
@@ -12,9 +12,9 @@ When several tool calls run in a row, Pi normally renders each one as its own ex
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
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
- - **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
+ - **Self-rendered tools get the same treatment.** Tools that own their framing (e.g. MCP adapter rows) collapse to the same one-line summary args label plus outcome with pending, success, and error backgrounds; expanding restores their full custom render.
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
- - **Errors stay compact and visibly failed.** A failed call keeps its own error-colored block and native collapsed detail until expanded.
17
+ - **Errors stay compact and visibly failed.** A failed call keeps an error-colored block: native collapsed detail for built-in tools, or a one-line summary with the first error line for self-rendered tools.
18
18
  - **Ctrl+O restores full blocks.** Expanding tools (`setToolsExpanded(true)`) brings back Pi's individual full blocks, including complete error details and successful results.
19
19
 
20
20
  ## Bundled thinking-block extension
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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
@@ -51,7 +51,11 @@ type ToolExecutionRow = {
51
51
  content?: Array<{ type?: unknown; text?: unknown }>;
52
52
  details?: Record<string, unknown>;
53
53
  };
54
- rendererState?: { startedAt?: unknown; endedAt?: unknown };
54
+ rendererState?: {
55
+ startedAt?: unknown;
56
+ endedAt?: unknown;
57
+ compactTitle?: unknown;
58
+ };
55
59
  contentBox?: ComponentContainer;
56
60
  contentText?: TextComponent;
57
61
  selfRenderContainer?: ComponentContainer;
@@ -68,7 +72,6 @@ type PresentationPatchState = {
68
72
  theme?: ThemeLike;
69
73
  collapseParallel: boolean;
70
74
  groupCache: WeakMap<ToolExecutionRow, GroupRenderCache>;
71
- liveRenderedRows: WeakSet<ToolExecutionRow>;
72
75
  rowVersions: WeakMap<ToolExecutionRow, number>;
73
76
  rowGroups: WeakMap<ToolExecutionRow, ToolExecutionRow[]>;
74
77
  rowSignatures: WeakMap<ToolExecutionRow, string>;
@@ -208,12 +211,20 @@ function collapseSuccessfulResult(
208
211
  row.expanded !== false ||
209
212
  row.isPartial !== false ||
210
213
  !row.result ||
211
- row.result.isError ||
212
- hasImageResult(row) ||
213
- row.getRenderShell?.() === "self"
214
+ hasImageResult(row)
214
215
  )
215
216
  return;
216
217
 
218
+ if (row.getRenderShell?.() === "self") {
219
+ collapseSelfRenderedRow(row, theme);
220
+ return;
221
+ }
222
+
223
+ // Default-shell failures keep Pi's native error render, which already
224
+ // applies the error background. rowHasFailed also catches MCP tools that
225
+ // use the default shell (e.g. mcpScript) and report via details.error.
226
+ if (rowHasFailed(row)) return;
227
+
217
228
  const collapsed = row.hasRendererDefinition?.()
218
229
  ? removeResultComponent(row.contentBox)
219
230
  : collapseGenericResult(row);
@@ -223,6 +234,288 @@ function collapseSuccessfulResult(
223
234
  }
224
235
  }
225
236
 
237
+ // Self-rendered tools (e.g. MCP adapter rows) own their framing, so there is
238
+ // no result component to trim. Swap the whole container for a single summary
239
+ // line in the same style as collapsed default-shell rows. Pi rebuilds the
240
+ // container on every updateDisplay, so this never corrupts expanded renders.
241
+ // Failures collapse too, with the error background and the first error line,
242
+ // since MCP error output tends to be a huge markdown blob.
243
+ function collapseSelfRenderedRow(
244
+ row: ToolExecutionRow,
245
+ theme?: ThemeLike,
246
+ ): void {
247
+ const container = row.selfRenderContainer;
248
+ if (!theme || !container || !Array.isArray(container.children)) return;
249
+
250
+ const failed = rowHasFailed(row);
251
+ const tail = failed
252
+ ? renderedErrorOutcome(row, theme)
253
+ : (renderedOutcome(row, theme) ?? renderedGenericOutcome(theme));
254
+ const box = new Box(1, 1, (text) =>
255
+ theme.bg(failed ? "toolErrorBg" : "toolSuccessBg", text),
256
+ );
257
+ box.addChild(
258
+ selfRenderedSummaryComponent(row, theme, tail, failed ? 0.5 : 1),
259
+ );
260
+ container.children = [box];
261
+ }
262
+
263
+ // The MCP adapter reports outcomes in details.error but only promotes
264
+ // tool_error/call_failed to result.isError (see its toolErrorOverride);
265
+ // remaining codes are informational guidance (ambiguous, no_instructions,
266
+ // auth flows, ...). Keep the failure code list here so every presentation
267
+ // decision — singletons and groups — shares one classification.
268
+ const MCP_FAILURE_ERROR_CODES: ReadonlySet<string> = new Set([
269
+ "tool_error",
270
+ "call_failed",
271
+ "tool_not_found",
272
+ "tool_not_found_after_reconnect",
273
+ "connect_failed",
274
+ "not_found",
275
+ "server_not_found",
276
+ "server_disabled",
277
+ "server_backoff",
278
+ "server_not_connected",
279
+ "init_failed",
280
+ "init_timeout",
281
+ "not_initialized",
282
+ "server_unavailable",
283
+ "not_connected",
284
+ "timeout",
285
+ "script_error",
286
+ // Terminal rejections: the operation never executed (validation failures,
287
+ // auth-flow failures, missing inputs), unlike continuing-guidance codes.
288
+ "missing_server",
289
+ "missing_input",
290
+ "oauth_not_supported",
291
+ "auth_start_failed",
292
+ "not_authenticated",
293
+ "auth_complete_failed",
294
+ "query_too_long",
295
+ "unsafe_pattern",
296
+ "invalid_pattern",
297
+ "empty_query",
298
+ ]);
299
+
300
+ function rowHasFailed(row: ToolExecutionRow): boolean {
301
+ if (row.result?.isError) return true;
302
+ const code = row.result?.details?.error;
303
+ return typeof code === "string" && MCP_FAILURE_ERROR_CODES.has(code);
304
+ }
305
+
306
+ function renderedErrorOutcome(row: ToolExecutionRow, theme: ThemeLike): string {
307
+ const firstLine =
308
+ resultText(row)
309
+ .split("\n")
310
+ .map((line) => line.trim())
311
+ .find((line) => line.length > 0) ?? "error";
312
+ return `${theme.fg("muted", "→")} ${theme.fg("error", firstLine)}`;
313
+ }
314
+
315
+ function selfRenderedSummaryComponent(
316
+ row: ToolExecutionRow,
317
+ theme: ThemeLike,
318
+ tail: string,
319
+ maxTailShare = 1,
320
+ ): ComponentLike {
321
+ return {
322
+ render(width: number): string[] {
323
+ // The Box already shrinks our width by its horizontal padding before
324
+ // calling render, so only the badge deducts from this headline;
325
+ // counting the padding again starves the label at narrow widths.
326
+ const budget = Math.max(1, width - BADGE_WIDTH);
327
+ const summary = selfRenderedSummary(row, budget);
328
+ // Long error lines would otherwise evict the call summary entirely;
329
+ // cap the tail's share so the label always survives.
330
+ const cappedTail =
331
+ maxTailShare >= 1
332
+ ? tail
333
+ : capFailureTail(tail, Math.floor(budget * maxTailShare), theme);
334
+ return [fitSummaryTail(summary, cappedTail, budget)];
335
+ },
336
+ invalidate() {},
337
+ };
338
+ }
339
+
340
+ // Trim a failure tail to its quota of the budget, counting the appended
341
+ // ellipsis inside the cap, so the call label keeps the leftover columns
342
+ // plus the joining space instead of being truncated to nothing. A cap too
343
+ // small for the ellipsis keeps that many literal tail characters instead;
344
+ // a zero-column cap lets the tail disappear rather than spill an ellipsis
345
+ // outside its quota.
346
+ function capFailureTail(tail: string, cap: number, theme: ThemeLike): string {
347
+ const tailWidth = visibleWidth(tail);
348
+ if (tailWidth <= cap) return tail;
349
+ const ellipsis = theme.fg("muted", "…");
350
+ const ellipsisWidth = visibleWidth(ellipsis);
351
+ if (cap <= 0) return "";
352
+ if (ellipsisWidth >= cap) return sliceByColumn(tail, 0, cap, true);
353
+ return `${sliceByColumn(tail, 0, cap - ellipsisWidth, true)}${ellipsis}`;
354
+ }
355
+
356
+ function renderedGenericOutcome(theme: ThemeLike): string {
357
+ return `${theme.fg("muted", "→")} ${theme.fg("success", "done")}`;
358
+ }
359
+
360
+ // The MCP adapter stashes its call's first line in renderer state before
361
+ // collapsing to an empty component, so prefer it over re-rendering the call.
362
+ function selfRenderedCallTitle(row: ToolExecutionRow): string | undefined {
363
+ const title = row.rendererState?.compactTitle;
364
+ return typeof title === "string" && title.trim().length > 0
365
+ ? title.trim()
366
+ : undefined;
367
+ }
368
+
369
+ // The MCP proxy passes tool arguments as a JSON-encoded string; parse and
370
+ // re-stringify so the summary shows compact JSON instead of escaped quotes.
371
+ function squashJsonish(value: unknown): string {
372
+ if (typeof value === "string") {
373
+ const trimmed = value.trim();
374
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
375
+ try {
376
+ const compact = JSON.stringify(JSON.parse(trimmed));
377
+ return compact === "{}" ? "" : compact;
378
+ } catch {
379
+ return flattenNewlines(trimmed);
380
+ }
381
+ }
382
+ return flattenNewlines(value);
383
+ }
384
+ return compactArgs(value);
385
+ }
386
+
387
+ // String fallbacks still feed the one-line "tool {args}" summary label;
388
+ // normalize embedded line breaks so a multiline argument cannot split it.
389
+ function flattenNewlines(text: string): string {
390
+ return text.replace(/[\r\n]+/g, " ");
391
+ }
392
+
393
+ // Actions the mcp proxy dispatches before any mode selector (pi-mcp-adapter
394
+ // 2.26.0). Unknown action values fall through to other modes, so their shapes
395
+ // are shown raw instead of as a guessed operation.
396
+ const MCP_ACTIONS: ReadonlySet<string> = new Set([
397
+ "ui-messages",
398
+ "auth-start",
399
+ "auth-complete",
400
+ ]);
401
+ const MCP_MODE_KEYS = [
402
+ "tool",
403
+ "connect",
404
+ "describe",
405
+ "instructions",
406
+ "action",
407
+ ] as const;
408
+
409
+ // A concise one-line label for the adapter's "mcp" proxy from the call's
410
+ // shape alone. The adapter's dispatch order is an implementation detail, so
411
+ // only a single unambiguous selector gets a named operation; conflicting,
412
+ // empty, or unrecognized selectors return undefined so the caller shows the
413
+ // complete compact args instead of claiming which operation executed.
414
+ function mcpProxyCallLabel(
415
+ record: Record<string, unknown>,
416
+ ): string | undefined {
417
+ const selectors: string[] = [];
418
+ // An empty search still runs a search; every other selector must be a
419
+ // non-empty string to name an operation at all.
420
+ if (record.search !== undefined) selectors.push("search");
421
+ for (const key of MCP_MODE_KEYS) {
422
+ const value = record[key];
423
+ if (value === undefined) continue;
424
+ // An empty selector cannot dispatch, and which fallback ran is adapter
425
+ // internals, so the raw args are kept instead.
426
+ if (typeof value !== "string" || value.length === 0) return undefined;
427
+ selectors.push(key);
428
+ }
429
+
430
+ const server =
431
+ typeof record.server === "string" && record.server.length > 0
432
+ ? record.server
433
+ : undefined;
434
+ const rest: Record<string, unknown> = { ...record };
435
+ if (server !== undefined) delete rest.server;
436
+ const serverSuffix = server === undefined ? "" : ` @ ${server}`;
437
+
438
+ if (selectors.length === 0) {
439
+ // No mode selector: the adapter lists a named server or reports status.
440
+ const label = server === undefined ? "mcp status" : `mcp list ${server}`;
441
+ const restJson = squashJsonish(rest);
442
+ return restJson ? `${label} ${restJson}` : label;
443
+ }
444
+ if (selectors.length !== 1) return undefined;
445
+
446
+ const key = selectors[0]!;
447
+ delete rest[key];
448
+
449
+ if (key === "tool") {
450
+ delete rest.args;
451
+ const inner = squashJsonish(record.args);
452
+ const target = `${String(record.tool)}${serverSuffix}`;
453
+ return inner ? `${target} ${inner}` : target;
454
+ }
455
+ if (key === "search") {
456
+ const label =
457
+ `mcp search ${String(record.search)}`.trimEnd() + serverSuffix;
458
+ const restJson = squashJsonish(rest);
459
+ return restJson ? `${label} ${restJson}` : label;
460
+ }
461
+ if (key === "action") {
462
+ const name = String(record.action);
463
+ if (!MCP_ACTIONS.has(name)) return undefined;
464
+ let label = `mcp ${name}${serverSuffix}`;
465
+ if (name === "auth-complete") {
466
+ delete rest.args;
467
+ const input = squashJsonish(record.args);
468
+ if (input) label += ` ${input}`;
469
+ }
470
+ const restJson = squashJsonish(rest);
471
+ return restJson ? `${label} ${restJson}` : label;
472
+ }
473
+ // connect / describe / instructions: the adapter dispatches these modes
474
+ // without params.server, so a server argument is shown raw rather than as
475
+ // a scope suffix that implies an operation that never ran.
476
+ const label = `mcp ${key} ${String(record[key])}`;
477
+ const restJson = squashJsonish(
478
+ server === undefined ? rest : { ...rest, server },
479
+ );
480
+ return restJson ? `${label} ${restJson}` : label;
481
+ }
482
+
483
+ // Builds a one-line "tool {args}" label from the call arguments. The
484
+ // adapter's own title drops the arguments and its pretty-printed JSON spans
485
+ // many lines, so squash the JSON ourselves to keep the row at one line.
486
+ function selfRenderedCallLabel(row: ToolExecutionRow): string {
487
+ const token = row.toolName ?? "tool";
488
+ const args = row.args;
489
+ // Only the adapter's "mcp" proxy tool uses these action shapes, so the
490
+ // label is derived from the args' shape alone; the adapter's own
491
+ // compactTitle drops parameters (limit/offset) and is not trusted here.
492
+ if (
493
+ token === "mcp" &&
494
+ args &&
495
+ typeof args === "object" &&
496
+ !Array.isArray(args)
497
+ ) {
498
+ const record = args as Record<string, unknown>;
499
+ const proxyLabel = mcpProxyCallLabel(record);
500
+ // Unambiguous shapes get a concise name; anything else keeps the complete
501
+ // compact args so the row never claims an operation that may not have run.
502
+ if (proxyLabel) return proxyLabel;
503
+ const json = squashJsonish(args);
504
+ return json ? `mcp ${json}` : "mcp status";
505
+ }
506
+ const title = selfRenderedCallTitle(row);
507
+ const json = squashJsonish(args);
508
+ // Direct tools put only the bare name in their own title, so append the
509
+ // args ourselves.
510
+ if (title && title !== token) return title;
511
+ if (json) return `${token} ${json}`;
512
+ return title ?? token;
513
+ }
514
+
515
+ function selfRenderedSummary(row: ToolExecutionRow, width: number): string {
516
+ return fitSummary(selfRenderedCallLabel(row), width);
517
+ }
518
+
226
519
  function isToolExecutionRow(
227
520
  component: unknown,
228
521
  ): component is ToolExecutionRow & ComponentLike {
@@ -270,7 +563,6 @@ function isLiveRow(row: ToolExecutionRow): boolean {
270
563
  return (
271
564
  row.expanded === false &&
272
565
  (row.isPartial !== false || !row.result) &&
273
- row.getRenderShell?.() !== "self" &&
274
566
  !hasImageResult(row)
275
567
  );
276
568
  }
@@ -310,6 +602,18 @@ function fitLiveHeader(
310
602
  // cap is composed before the Box applies background and padding, so the live
311
603
  // line keeps the tool background edge to edge and the block's bottom padding.
312
604
  function capLiveRowDisplay(row: ToolExecutionRow, theme?: ThemeLike): void {
605
+ // Pin live self-rendered rows (e.g. MCP calls) to the same one-line summary
606
+ // they settle into, so the block never grows and hops on completion.
607
+ if (row.getRenderShell?.() === "self") {
608
+ const container = row.selfRenderContainer;
609
+ if (!theme || !container || !Array.isArray(container.children)) return;
610
+ const box = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
611
+ box.addChild(
612
+ selfRenderedSummaryComponent(row, theme, theme.fg("muted", "…")),
613
+ );
614
+ container.children = [box];
615
+ return;
616
+ }
313
617
  if (row.hasRendererDefinition?.()) {
314
618
  const container = row.contentBox;
315
619
  if (!container || !Array.isArray(container.children)) return;
@@ -353,15 +657,11 @@ function isCollapsibleSuccess(row: ToolExecutionRow): boolean {
353
657
  row.expanded === false &&
354
658
  row.isPartial === false &&
355
659
  !!row.result &&
356
- !row.result.isError &&
660
+ !rowHasFailed(row) &&
357
661
  !hasImageResult(row)
358
662
  );
359
663
  }
360
664
 
361
- function isSettledToolRow(row: ToolExecutionRow): boolean {
362
- return row.isPartial === false && !!row.result;
363
- }
364
-
365
665
  function hasToolSiblingInAssistantBatch(
366
666
  children: unknown[],
367
667
  index: number,
@@ -390,10 +690,6 @@ function isGroupableToolRow(
390
690
  state: PresentationPatchState,
391
691
  ): boolean {
392
692
  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
693
  return (
398
694
  state.collapseParallel ||
399
695
  !hasToolSiblingInAssistantBatch(children, index, renderAt)
@@ -480,6 +776,10 @@ function diffCounts(diff: string): { added: number; removed: number } {
480
776
  }
481
777
 
482
778
  function outcomeSummary(row: ToolExecutionRow): string | undefined {
779
+ // Self-rendered tools own their result framing; the built-in heuristics
780
+ // (line counts, diff stats) describe default-shell rows only, so their
781
+ // settled rows always fall back to the generic outcome.
782
+ if (row.getRenderShell?.() === "self") return undefined;
483
783
  if (!isCollapsibleSuccess(row)) return undefined;
484
784
 
485
785
  const text = resultText(row);
@@ -532,18 +832,25 @@ function renderedGroupedOutcome(
532
832
  ): string | undefined {
533
833
  const outcome = renderedOutcome(row, theme);
534
834
  if (outcome) return outcome;
535
- if (!isLiveRow(row)) return undefined;
536
- const elapsed = liveElapsedText(row);
537
- return theme.fg("muted", elapsed ? `· ${elapsed}` : "…");
835
+ if (isLiveRow(row)) {
836
+ const elapsed = liveElapsedText(row);
837
+ return theme.fg("muted", elapsed ? `· ${elapsed}` : "…");
838
+ }
839
+ // Self-rendered tools have no per-tool outcome summary; keep the group's
840
+ // settled tail consistent with collapsed singletons.
841
+ if (row.getRenderShell?.() === "self") return renderedGenericOutcome(theme);
842
+ return undefined;
538
843
  }
539
844
 
845
+ // A budget too small for the suffix keeps literal text instead, so a
846
+ // one-column label never collapses into a bare ellipsis.
540
847
  function truncatePlain(text: string, width: number, suffix = "…"): string {
541
848
  const max = Math.max(0, width);
542
849
  const plain = stripAnsi(text);
543
850
  if (visibleWidth(plain) <= max) return plain;
544
851
 
545
852
  const suffixWidth = visibleWidth(suffix);
546
- if (suffixWidth >= max) return sliceByColumn(suffix, 0, max, true);
853
+ if (suffixWidth >= max) return sliceByColumn(plain, 0, max, true);
547
854
  return `${sliceByColumn(plain, 0, max - suffixWidth, true)}${suffix}`;
548
855
  }
549
856
 
@@ -552,14 +859,23 @@ function fitSummary(summary: string, width: number): string {
552
859
  return visibleWidth(summary) <= max ? summary : truncatePlain(summary, max);
553
860
  }
554
861
 
862
+ // Fits "head tail" into width while always reserving at least one literal
863
+ // head column plus the joining space; the tail takes what is left and
864
+ // disappears entirely when no columns remain.
555
865
  function fitSummaryTail(head: string, tail: string, width: number): string {
556
866
  const max = Math.max(1, width);
557
- const summary = `${head.trimEnd()} ${tail}`;
558
- if (visibleWidth(summary) <= max) return summary;
559
-
867
+ const headWidth = visibleWidth(head);
560
868
  const tailWidth = visibleWidth(tail);
561
- if (tailWidth >= max) return truncatePlain(tail, max);
562
- return `${truncatePlain(head, max - tailWidth - 1)} ${tail}`;
869
+ if (headWidth + 1 + tailWidth <= max) return `${head.trimEnd()} ${tail}`;
870
+ if (tailWidth === 0) return truncatePlain(head, max);
871
+ if (headWidth === 0) return truncatePlain(tail, max);
872
+
873
+ const labelBudget = Math.min(headWidth, Math.max(1, max - tailWidth - 1));
874
+ const tailBudget = Math.min(tailWidth, Math.max(0, max - labelBudget - 1));
875
+ if (tailBudget === 0) return truncatePlain(head, max);
876
+ const fittedTail =
877
+ tailWidth <= tailBudget ? tail : truncatePlain(tail, tailBudget);
878
+ return `${truncatePlain(head, labelBudget)} ${fittedTail}`;
563
879
  }
564
880
 
565
881
  function decorateSuccessfulCall(
@@ -620,17 +936,20 @@ function renderedCallSummary(
620
936
  width: number,
621
937
  theme: ThemeLike,
622
938
  ): string {
939
+ // Self-rendered call components change shape across the live/settled
940
+ // boundary (the adapter's call disappears once settled), so always build
941
+ // the summary from the stable args label instead of scraping the preview.
942
+ if (row.getRenderShell?.() === "self") return selfRenderedSummary(row, width);
943
+
623
944
  let component = row.callRendererComponent;
624
945
  if (!component && Array.isArray(row.contentBox?.children)) {
625
946
  component = row.contentBox.children[0] as ComponentLike | undefined;
626
947
  }
627
-
628
- const selfRendered = row.getRenderShell?.() === "self";
629
948
  if (component && typeof component.render === "function") {
630
949
  const visibleLines = component
631
950
  .render(Math.max(1, width))
632
951
  .filter(hasVisibleContent);
633
- const rendered = visibleLines.slice(0, selfRendered ? 1 : 3);
952
+ const rendered = visibleLines.slice(0, 3);
634
953
  const line = rendered[0];
635
954
  if (line) {
636
955
  const first = trimRenderedLine(line);
@@ -666,8 +985,6 @@ function renderedCallSummary(
666
985
  }
667
986
  }
668
987
 
669
- if (selfRendered) return theme.fg("muted", "(details omitted)");
670
-
671
988
  const fallback = compactArgs(row.args);
672
989
  return fallback
673
990
  ? theme.fg("accent", fallback)
@@ -820,7 +1137,9 @@ function renderGroupedToolRows(
820
1137
  width,
821
1138
  theme,
822
1139
  );
823
- const box = new Box(1, 1, (text) => theme.bg("toolSuccessBg", text));
1140
+ const box = new Box(1, 1, (text) =>
1141
+ theme.bg(hasLiveMembers ? "toolPendingBg" : "toolSuccessBg", text),
1142
+ );
824
1143
  box.addChild(summary);
825
1144
  lines = renderWithTemporaryChild(container, box, () =>
826
1145
  state.originalRender.call(row, width),
@@ -1018,7 +1337,6 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1018
1337
  const state: PresentationPatchState = {
1019
1338
  collapseParallel: envEnabled(COLLAPSE_PARALLEL_ENV, true),
1020
1339
  groupCache: new WeakMap(),
1021
- liveRenderedRows: new WeakSet(),
1022
1340
  rowVersions: new WeakMap(),
1023
1341
  rowGroups: new WeakMap(),
1024
1342
  rowSignatures: new WeakMap(),
@@ -1052,7 +1370,6 @@ function installPresentationPatch(): PresentationPatchState | undefined {
1052
1370
  this: ToolExecutionRow,
1053
1371
  width: number,
1054
1372
  ): string[] {
1055
- if (!isSettledToolRow(this)) state.liveRenderedRows.add(this);
1056
1373
  const lines = state.originalRender.call(this, width);
1057
1374
  try {
1058
1375
  return decorateHeader(this, lines, width, state.theme);