@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +146 -36
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-kaush/pi-tool-call-markers",
3
- "version": "0.2.3",
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
@@ -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
- const budget = Math.max(1, width - BADGE_WIDTH - 2);
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
- return `${sliceByColumn(tail, 0, Math.max(0, cap - visibleWidth(ellipsis)), true)}${ellipsis}`;
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. Build the
379
- // label from the args: the adapter's own compactTitle drops parameters
380
- // (limit/offset) and can misname the action, so it is not trusted here.
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
- if (typeof record.tool === "string") {
389
- const inner = squashJsonish(record.args);
390
- const target =
391
- typeof record.server === "string"
392
- ? `${record.tool} @ ${record.server}`
393
- : record.tool;
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
- // Proxy actions format nicer in the adapter's own title ("mcp search …");
414
- // direct tools put only the bare name there, so append the args ourselves.
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(suffix, 0, max, true);
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 summary = `${head.trimEnd()} ${tail}`;
764
- if (visibleWidth(summary) <= max) return summary;
765
-
867
+ const headWidth = visibleWidth(head);
766
868
  const tailWidth = visibleWidth(tail);
767
- if (tailWidth >= max) return truncatePlain(tail, max);
768
- 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}`;
769
879
  }
770
880
 
771
881
  function decorateSuccessfulCall(