@pi-kaush/pi-tool-call-markers 0.2.3 → 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/package.json +1 -1
- package/src/index.ts +146 -36
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -283,6 +283,18 @@ const MCP_FAILURE_ERROR_CODES: ReadonlySet<string> = new Set([
|
|
|
283
283
|
"not_connected",
|
|
284
284
|
"timeout",
|
|
285
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",
|
|
286
298
|
]);
|
|
287
299
|
|
|
288
300
|
function rowHasFailed(row: ToolExecutionRow): boolean {
|
|
@@ -308,7 +320,10 @@ function selfRenderedSummaryComponent(
|
|
|
308
320
|
): ComponentLike {
|
|
309
321
|
return {
|
|
310
322
|
render(width: number): string[] {
|
|
311
|
-
|
|
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);
|
|
312
327
|
const summary = selfRenderedSummary(row, budget);
|
|
313
328
|
// Long error lines would otherwise evict the call summary entirely;
|
|
314
329
|
// cap the tail's share so the label always survives.
|
|
@@ -324,12 +339,18 @@ function selfRenderedSummaryComponent(
|
|
|
324
339
|
|
|
325
340
|
// Trim a failure tail to its quota of the budget, counting the appended
|
|
326
341
|
// ellipsis inside the cap, so the call label keeps the leftover columns
|
|
327
|
-
// plus the joining space instead of being truncated to nothing.
|
|
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.
|
|
328
346
|
function capFailureTail(tail: string, cap: number, theme: ThemeLike): string {
|
|
329
347
|
const tailWidth = visibleWidth(tail);
|
|
330
348
|
if (tailWidth <= cap) return tail;
|
|
331
349
|
const ellipsis = theme.fg("muted", "…");
|
|
332
|
-
|
|
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}`;
|
|
333
354
|
}
|
|
334
355
|
|
|
335
356
|
function renderedGenericOutcome(theme: ThemeLike): string {
|
|
@@ -369,15 +390,105 @@ function flattenNewlines(text: string): string {
|
|
|
369
390
|
return text.replace(/[\r\n]+/g, " ");
|
|
370
391
|
}
|
|
371
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
|
+
|
|
372
483
|
// Builds a one-line "tool {args}" label from the call arguments. The
|
|
373
484
|
// adapter's own title drops the arguments and its pretty-printed JSON spans
|
|
374
485
|
// many lines, so squash the JSON ourselves to keep the row at one line.
|
|
375
486
|
function selfRenderedCallLabel(row: ToolExecutionRow): string {
|
|
376
487
|
const token = row.toolName ?? "tool";
|
|
377
488
|
const args = row.args;
|
|
378
|
-
// Only the adapter's "mcp" proxy tool uses these action shapes
|
|
379
|
-
// label from the args
|
|
380
|
-
// (limit/offset) and
|
|
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.
|
|
381
492
|
if (
|
|
382
493
|
token === "mcp" &&
|
|
383
494
|
args &&
|
|
@@ -385,33 +496,17 @@ function selfRenderedCallLabel(row: ToolExecutionRow): string {
|
|
|
385
496
|
!Array.isArray(args)
|
|
386
497
|
) {
|
|
387
498
|
const record = args as Record<string, unknown>;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return inner ? `${target} ${inner}` : target;
|
|
395
|
-
}
|
|
396
|
-
for (const key of ["search", "connect", "describe", "action"] as const) {
|
|
397
|
-
const value = record[key];
|
|
398
|
-
if (typeof value !== "string" || value.length === 0) continue;
|
|
399
|
-
const rest: Record<string, unknown> = { ...record };
|
|
400
|
-
delete rest[key];
|
|
401
|
-
let label = key === "action" ? `mcp ${value}` : `mcp ${key} ${value}`;
|
|
402
|
-
if (typeof rest.server === "string") {
|
|
403
|
-
label += ` @ ${rest.server}`;
|
|
404
|
-
delete rest.server;
|
|
405
|
-
}
|
|
406
|
-
const restJson = squashJsonish(rest);
|
|
407
|
-
return restJson ? `${label} ${restJson}` : label;
|
|
408
|
-
}
|
|
409
|
-
if (typeof record.server === "string") return `mcp list ${record.server}`;
|
|
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";
|
|
410
505
|
}
|
|
411
506
|
const title = selfRenderedCallTitle(row);
|
|
412
507
|
const json = squashJsonish(args);
|
|
413
|
-
//
|
|
414
|
-
//
|
|
508
|
+
// Direct tools put only the bare name in their own title, so append the
|
|
509
|
+
// args ourselves.
|
|
415
510
|
if (title && title !== token) return title;
|
|
416
511
|
if (json) return `${token} ${json}`;
|
|
417
512
|
return title ?? token;
|
|
@@ -681,6 +776,10 @@ function diffCounts(diff: string): { added: number; removed: number } {
|
|
|
681
776
|
}
|
|
682
777
|
|
|
683
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;
|
|
684
783
|
if (!isCollapsibleSuccess(row)) return undefined;
|
|
685
784
|
|
|
686
785
|
const text = resultText(row);
|
|
@@ -743,13 +842,15 @@ function renderedGroupedOutcome(
|
|
|
743
842
|
return undefined;
|
|
744
843
|
}
|
|
745
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.
|
|
746
847
|
function truncatePlain(text: string, width: number, suffix = "…"): string {
|
|
747
848
|
const max = Math.max(0, width);
|
|
748
849
|
const plain = stripAnsi(text);
|
|
749
850
|
if (visibleWidth(plain) <= max) return plain;
|
|
750
851
|
|
|
751
852
|
const suffixWidth = visibleWidth(suffix);
|
|
752
|
-
if (suffixWidth >= max) return sliceByColumn(
|
|
853
|
+
if (suffixWidth >= max) return sliceByColumn(plain, 0, max, true);
|
|
753
854
|
return `${sliceByColumn(plain, 0, max - suffixWidth, true)}${suffix}`;
|
|
754
855
|
}
|
|
755
856
|
|
|
@@ -758,14 +859,23 @@ function fitSummary(summary: string, width: number): string {
|
|
|
758
859
|
return visibleWidth(summary) <= max ? summary : truncatePlain(summary, max);
|
|
759
860
|
}
|
|
760
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.
|
|
761
865
|
function fitSummaryTail(head: string, tail: string, width: number): string {
|
|
762
866
|
const max = Math.max(1, width);
|
|
763
|
-
const
|
|
764
|
-
if (visibleWidth(summary) <= max) return summary;
|
|
765
|
-
|
|
867
|
+
const headWidth = visibleWidth(head);
|
|
766
868
|
const tailWidth = visibleWidth(tail);
|
|
767
|
-
if (tailWidth
|
|
768
|
-
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}`;
|
|
769
879
|
}
|
|
770
880
|
|
|
771
881
|
function decorateSuccessfulCall(
|