@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.
- package/README.md +2 -2
- package/package.json +1 -1
- 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
|
|
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
|
|
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
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?: {
|
|
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
|
|
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
|
|
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 (
|
|
536
|
-
|
|
537
|
-
|
|
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(
|
|
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
|
|
558
|
-
if (visibleWidth(summary) <= max) return summary;
|
|
559
|
-
|
|
867
|
+
const headWidth = visibleWidth(head);
|
|
560
868
|
const tailWidth = visibleWidth(tail);
|
|
561
|
-
if (tailWidth
|
|
562
|
-
return
|
|
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,
|
|
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) =>
|
|
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);
|