@bismawy/pi-arnative 0.2.3 → 0.2.5

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.
@@ -8,19 +8,15 @@
8
8
  * │ 󱞩 first output line │
9
9
  * ╰────────────────────────────╯
10
10
  * Click / ctrl+e = full title + detail (output dim; edit = toolDiff*).
11
- * Collapsed summary: bash/write `󱞩 first output line`; grep/find/read numeric
12
- * (→ N matches / → N files / N lines); edit `󱞩 +N / -M`. Plain tools show no
13
- * summary line when expanded (output is already full) — no duplicated first line.
14
- * Duration is right-aligned and space-aware (title truncated via visibleWidth when
15
- * it doesn't fit); empty when unmeasured (restored sessions).
11
+ * Collapsed summary: bash/write `󱞩 first line`; grep/find/read numeric
12
+ * (→ N matches / → N files / N lines); edit `󱞩 +N / -M`. Expanded plain output
13
+ * gets no summary line (no duplicated first line).
16
14
  * Colors: tool name accent, paths/links tint, box lines + 󱞩 dim. Execution is a
17
15
  * pure delegate (spread of the built-in tools).
18
16
  * "has a result" lives in context.state, read at render() -> restore/reload safe,
19
- * no stale 󰔟 box. The gap between boxes (built-in Spacer) is stripped by a render
20
- * patch (same pattern as the footer patch).
21
- * Third-party tools with no renderer of their own (memory_write, scratchpad, MCP…)
22
- * get the same treatment: shell "self" + box with a one-line summary and
23
- * `[ctrl+o to expand]`; full detail only when expanded.
17
+ * no stale 󰔟 box. Third-party tools with no renderer of their own (memory_write,
18
+ * scratchpad, MCP…) get the same box: shell "self", one-line summary,
19
+ * `[ctrl+o to expand]` for the full detail.
24
20
  */
25
21
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
26
22
  import {
@@ -31,12 +27,14 @@ import {
31
27
  createReadTool,
32
28
  createWriteTool,
33
29
  getMarkdownTheme,
34
- keyText,
35
30
  ToolExecutionComponent,
36
31
  } from "@earendil-works/pi-coding-agent";
37
32
  import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
38
33
  import { homedir } from "node:os";
39
34
  import { renderBoxLines as box } from "../lib/box.ts";
35
+ import { stripAnsi } from "../lib/ansi.ts";
36
+ import { assert, isMain } from "../lib/check.ts";
37
+ import { expandKeyName } from "../lib/format.ts";
40
38
 
41
39
  type Theme = { fg(color: string, text: string): string; bg?(color: string, text: string): string };
42
40
  type TResult = { content: Array<{ type: string; text?: string }>; isError?: boolean; details?: any };
@@ -63,7 +61,7 @@ function syncTicker(): void {
63
61
  }
64
62
 
65
63
  export function spinIcon(): string {
66
- return FRAMES[Math.floor(Date.now() / 150) % FRAMES.length];
64
+ return FRAMES[Math.floor(Date.now() / 500) % FRAMES.length];
67
65
  }
68
66
 
69
67
  export class Lines {
@@ -143,7 +141,7 @@ export function titleRow(
143
141
  }
144
142
  const durW = visibleWidth(durStr);
145
143
  const avail = Math.max(8, inner - durW);
146
- const wrapW = Math.max(4, avail - 1); // minimal 1 spasi sebelum durasi
144
+ const wrapW = Math.max(4, avail - 1); // keep at least 1 space before the duration
147
145
  const lines = expanded ? wrapTextWithAnsi(head, wrapW) : [truncateToWidth(head, wrapW, "…")];
148
146
  const pad = Math.max(1, avail - visibleWidth(lines[0]));
149
147
  lines[0] = `${lines[0]}${" ".repeat(pad)}${durStr}`;
@@ -182,13 +180,7 @@ export function toolSummary(name: string, text: string, args?: any): string {
182
180
 
183
181
  // Expand hint `[ctrl+o to expand]` (key text follows the active keybinding).
184
182
  export function expandHint(th: Theme): string {
185
- let key = "ctrl+o";
186
- try {
187
- key = keyText("app.tools.expand") || key;
188
- } catch {
189
- // outside a pi session: keep the literal
190
- }
191
- return th.fg("dim", `[${key} to expand]`);
183
+ return th.fg("dim", `[${expandKeyName()} to expand]`);
192
184
  }
193
185
 
194
186
  // Expanded detail: themed markdown (same layout as a normal pi message — code
@@ -204,8 +196,7 @@ export const fullText = (r: TResult, _th: Theme, width = 0): string[] => {
204
196
  }
205
197
  };
206
198
 
207
- const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
208
- const isBlankLine = (l: string): boolean => l.replace(ANSI_RE, "").trim() === "";
199
+ const isBlankLine = (l: string): boolean => stripAnsi(l).trim() === "";
209
200
 
210
201
  // Drop blank lines at both edges (the built-in Spacer) — that's the gap between boxes.
211
202
  export function stripBlankEdges(lines: string[]): string[] {
@@ -217,11 +208,9 @@ export function stripBlankEdges(lines: string[]): string[] {
217
208
  }
218
209
 
219
210
  // The inter-box gap is the built-in Spacer(1) of ToolExecutionComponent, stripped
220
- // with a prototype render patch + global guard (same pattern as the footer patch).
221
- // ONLY lines we draw ourselves (our boxes) get stripped: third-party tools use
222
- // their own "self" shell too, so the marker is a name list (filled by minimal() +
223
- // BOXED_TOOLS) rather than the shell. Without that bound their lines would lose
224
- // the Spacer + Box padding and stick to the neighbour.
211
+ // with a prototype render patch (same pattern as the footer patch). Only lines we
212
+ // draw ourselves are stripped: third-party tools use their own "self" shell too, so
213
+ // the marker is a name list (OWN_BOX, filled by minimal() + BOXED_TOOLS), not the shell.
225
214
  const OWN_BOX = new Set<string>();
226
215
  const hasOwnRendererDef = (self: { toolDefinition?: { renderCall?: unknown; renderResult?: unknown } }): boolean =>
227
216
  Boolean(self?.toolDefinition?.renderCall || self?.toolDefinition?.renderResult);
@@ -238,19 +227,24 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
238
227
  }
239
228
 
240
229
  // --- Tools without their own renderer: same box, one-line summary ---
241
- // Without this pi renders them as a plain bg block (name + 10 output lines).
242
- // Our renderer is installed through the prototype (same pattern as the gap patch)
243
- // only for tools that define no renderCall/renderResult of their own (our own
244
- // tools and pi-web-access etc. are untouched).
230
+ // Without this pi renders them as a plain bg block (name + 10 output lines). Our
231
+ // renderer is installed through the prototype (same pattern as the gap patch) only
232
+ // for tools that define no renderCall/renderResult of their own (our own tools and
233
+ // pi-web-access etc. are untouched).
234
+ //
235
+ // BOXED_TOOLS exceptions: the pi-web-access tools whose display names that package
236
+ // hardcodes as "search "/"fetch "/"get_content " — their renderer content is used
237
+ // as-is and only wrapped in our box. `source_check` is deliberately NOT boxed: its
238
+ // partial phase (curator: URLs + approval state) would disappear because our partial
239
+ // path returns nothing. The `todo` tool (@juicesharp/rpiv-todo) is boxed with a
240
+ // check-square icon (\uf14a) and the toolPendingBg/toolSuccessBg backgrounds.
245
241
  //
246
- // Exceptions: the pi-web-access tools that look different from our box
247
- // (`web_search`, `fetch_content`, `get_search_content`; that package hardcodes
248
- // their display names as "search "/"fetch "/"get_content "). Their renderer
249
- // content is used as-is and only wrapped in our box. `source_check` is
250
- // deliberately NOT boxed: its partial phase (curator: URLs + approval state)
251
- // would disappear because our partial path returns nothing.
252
- // The `todo` tool (@juicesharp/rpiv-todo) is boxed with a check-square icon
253
- // (\uf14a) and the toolPendingBg/toolSuccessBg theme backgrounds.
242
+ // MCP tools (pi-mcp-adapter) already use the "self" shell, but their result renderer
243
+ // prints the WHOLE output on error (unbounded, outside the box) and their call args
244
+ // can run to dozens of lines: a 1-line summary + [ctrl+o to expand] is tighter while
245
+ // the invoked tool stays visible. pi-fff "override" mode re-registers `find`/`grep`
246
+ // with its own renderers, replacing our boxed versions -> box theirs too (ours carry
247
+ // renderShell "self" = minimal).
254
248
  const BOXED_TOOLS = new Map([
255
249
  ["web_search", "search"],
256
250
  ["fetch_content", "fetch"],
@@ -260,17 +254,10 @@ const BOXED_TOOLS = new Map([
260
254
  const displayName = (name: string): string => BOXED_TOOLS.get(name) ?? name;
261
255
  for (const n of BOXED_TOOLS.keys()) OWN_BOX.add(n);
262
256
 
263
- // MCP tools (pi-mcp-adapter) already use the "self" shell, but we render their
264
- // result ourselves: their result renderer prints the WHOLE output on error
265
- // (unbounded, outside the box) and the JSON title+args in the call can be dozens
266
- // of lines. A 1-line summary + [ctrl+o to expand] is tighter; their args lines
267
- // still feed the call box so the invoked MCP tool stays visible.
268
257
  const MCP_TOOL = (name: string): boolean => name === "mcp" || name === "mcpScript" || name.startsWith("mcp__");
269
258
 
270
- // pi-fff "override" mode re-registers `find`/`grep` with its own renderers, replacing our
271
- // boxed versions -> plain text again. Box theirs too; ours carry renderShell "self" (minimal).
272
259
  const FFF_OVERRIDE = ["find", "grep"];
273
- const pakaiCallMereka = (self: any): boolean =>
260
+ const usesTheirCallRenderer = (self: any): boolean =>
274
261
  BOXED_TOOLS.has(self.toolName) ||
275
262
  MCP_TOOL(self.toolName) ||
276
263
  (FFF_OVERRIDE.includes(self.toolName) && self.toolDefinition?.renderShell !== "self");
@@ -288,6 +275,18 @@ const CALL_ROWS = 2;
288
275
  const capped = (th: Theme, rows: string[], expanded: boolean): string[] =>
289
276
  expanded || rows.length <= CALL_ROWS ? rows : [...rows.slice(0, CALL_ROWS), th.fg("dim", expandHint(th))];
290
277
 
278
+ // Third-party renderers run unguarded here (at render time, unlike pi's own
279
+ // updateDisplay which wraps them in try/catch + fallback): a throw becomes an
280
+ // uncaughtException that exits pi. e.g. pi-web-access' get_content renderCall
281
+ // slices args.responseId — undefined on a partial/malformed tool call.
282
+ const safeCall = <T>(fn: () => T): T | null => {
283
+ try {
284
+ return fn();
285
+ } catch {
286
+ return null;
287
+ }
288
+ };
289
+
291
290
  // Lines from a third-party renderer component (right padding trimmed, ANSI kept).
292
291
  const componentLines = (component: any, width: number): string[] => {
293
292
  try {
@@ -333,20 +332,20 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
333
332
  };
334
333
 
335
334
  proto.getRenderShell = function (): string {
336
- if (pakaiCallMereka(this)) return "self";
335
+ if (usesTheirCallRenderer(this)) return "self";
337
336
  return hasOwnRenderer(this) ? origShell.call(this) : "self";
338
337
  };
339
338
 
340
339
  proto.getCallRenderer = function () {
341
340
  const name: string = this.toolName;
342
- const mine = pakaiCallMereka(this);
341
+ const mine = usesTheirCallRenderer(this);
343
342
  const own = origCall.call(this);
344
343
  if (own && !mine) return own;
345
344
  return (args: any, th: Theme, ctx: TCtx) =>
346
345
  new Lines((width) => {
347
346
  if ((ctx.state as Record<string, unknown> | undefined)?.hasResult) return [];
348
347
  const icon = th.fg("warning", spinIcon());
349
- const theirs = own ? componentLines(own.call(this, args, th, ctx), width - 6) : [];
348
+ const theirs = own ? componentLines(safeCall(() => own.call(this, args, th, ctx)), width - 6) : [];
350
349
  if (name === "todo" && theirs.length)
351
350
  return box(th, width, formatTodoRows(theirs, th), "toolPendingBg");
352
351
  if (mine && theirs.length)
@@ -362,7 +361,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
362
361
  // pi-fff splits title (renderCall) and result (renderResult); both go in one box.
363
362
  const fff = FFF_OVERRIDE.includes(name) && this.toolDefinition?.renderShell !== "self";
364
363
  // MCP: their result renderer is deliberately skipped (unbounded error dump).
365
- if (own && !pakaiCallMereka(this)) return own;
364
+ if (own && !usesTheirCallRenderer(this)) return own;
366
365
  const boxed = BOXED_TOOLS.has(name);
367
366
  return (result: TResult, opts: { expanded: boolean; isPartial?: boolean }, th: Theme, ctx: TCtx) => {
368
367
  if (opts.isPartial) return EMPTY;
@@ -375,7 +374,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
375
374
  // box. Their renderer content is only used on success.
376
375
  const theirs =
377
376
  (boxed || fff) && !isErr
378
- ? own.call(this, { content: result.content, details: result.details }, opts, th, ctx)
377
+ ? safeCall(() => own.call(this, { content: result.content, details: result.details }, opts, th, ctx))
379
378
  : null;
380
379
  return new Lines((width) => {
381
380
  const tint = tintOf(th);
@@ -383,13 +382,12 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
383
382
  const icon = isErr ? th.fg("error", "x") : th.fg("success", "✓");
384
383
  const boxed2 = componentLines(theirs, width - 6);
385
384
  if (fff && boxed2.length) {
386
- const callLines = componentLines(origCall.call(this)?.call(this, ctx.args, th, ctx), width - 6);
385
+ const callLines = componentLines(safeCall(() => origCall.call(this)?.call(this, ctx.args, th, ctx)), width - 6);
387
386
  return box(th, width, boxedRows(icon, [callLines[0]?.trim() || displayName(name), ...boxed2]));
388
387
  }
389
388
  if (name === "todo" && boxed2.length) {
390
389
  // todo call result (status completed / in_progress)
391
- const callComp = origCall.call(this)?.call(this, ctx.args, th, ctx);
392
- const callLines = componentLines(callComp, width - 6);
390
+ const callLines = componentLines(safeCall(() => origCall.call(this)?.call(this, ctx.args, th, ctx)), width - 6);
393
391
  const combined = [...callLines, ...boxed2];
394
392
  const bgType = isErr ? "toolErrorBg" : "toolSuccessBg";
395
393
  return box(th, width, formatTodoRows(combined, th), bgType);
@@ -467,9 +465,9 @@ function minimal(
467
465
  }
468
466
  },
469
467
  renderCall(args: any, theme: Theme, context: TCtx) {
470
- const st = (context.state ?? {}) as Record<string, unknown>;
471
468
  return new Lines((width) => {
472
- if (st.hasResult) return [];
469
+ // Read context.state live (never capture): renderResult may create it after renderCall.
470
+ if (context.state?.hasResult) return [];
473
471
  const tint = tintOf(theme);
474
472
  const inner = Math.max(8, width - 4);
475
473
  const rows = titleRow(
@@ -624,53 +622,65 @@ function arnativeTools(pi: ExtensionAPI) {
624
622
  export default arnativeTools;
625
623
 
626
624
  // Self-check: `node extensions/tools.ts`.
627
- const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].split("\\").join("/"));
628
- if (isMain) {
629
- const assert = (cond: boolean, msg: string) => {
630
- if (!cond) {
631
- console.error(`FAIL: ${msg}`);
632
- process.exit(1);
633
- }
634
- };
625
+ if (isMain(import.meta.url)) {
635
626
  const mark = (s: string) => `<${s}>`;
636
627
  const th: Theme = { fg: (_c, s) => s };
637
628
  const R = { content: [{ type: "text", text: "path:1:match a\npath:2:match b\npath:3:match c" }] };
638
629
 
639
- assert(smartTitle("echo hi") === "echo hi", "teks pendek utuh");
640
- assert(smartTitle("ls a b c d e f g h") === "ls a b c d e…", "potong di batas 6 kata");
641
- assert(smartTitle("a b c d e f g") === "a b c d e f…", "7 kata -> 6 kata + elipsis");
642
- assert(smartTitle("a b c d e f") === "a b c d e f", "6 kata utuh");
630
+ assert(smartTitle("echo hi") === "echo hi", "short text kept intact");
631
+ assert(smartTitle("ls a b c d e f g h") === "ls a b c d e…", "cut at the 6-word limit");
632
+ assert(smartTitle("a b c d e f g") === "a b c d e f…", "7 words -> 6 words + ellipsis");
633
+ assert(smartTitle("a b c d e f") === "a b c d e f", "6 words kept intact");
643
634
 
644
635
  assert(paintLinks("cd /run/media/x && echo hi", mark) === "cd </run/media/x> && echo hi", "filter path absolut");
645
636
  assert(paintLinks("git clone https://github.com/a/b", mark) === "git clone <https://github.com/a/b>", "filter link URL");
646
- assert(paintLinks("ls extensions/tools.ts", mark) === "ls <extensions/tools.ts>", "filter path relatif");
647
- assert(paintLinks("echo plain 2>&1", mark) === "echo plain 2>&1", "teks polos tak tersentuh");
637
+ assert(paintLinks("ls extensions/tools.ts", mark) === "ls <extensions/tools.ts>", "relative path filter");
638
+ assert(paintLinks("echo plain 2>&1", mark) === "echo plain 2>&1", "plain text untouched");
648
639
 
649
640
  // right-aligned duration, space-aware (overflow bug regression)
650
641
  const t1 = titleRow(th, "✓", "$", "echo hi", 30, "0.1s", false);
651
- assert(t1.length === 1 && visibleWidth(t1[0]) === 30, "durasi rata kanan pas selebar inner");
652
- assert(t1[0].endsWith("0.1s"), "durasi di ujung kanan judul");
642
+ assert(t1.length === 1 && visibleWidth(t1[0]) === 30, "duration right-aligned within the inner width");
643
+ assert(t1[0].endsWith("0.1s"), "duration at the right end of the title");
653
644
  const t2 = titleRow(th, "✓", "$", "word ".repeat(40), 30, "0.1s", false);
654
- assert(t2.length === 1 && visibleWidth(t2[0]) === 30, "collapsed: judul panjang tetap 1 baris pas");
645
+ assert(t2.length === 1 && visibleWidth(t2[0]) === 30, "collapsed: long title stays 1 line, within width");
655
646
  const t3 = titleRow(th, "✓", "$", "word ".repeat(40), 30, "0.1s", true);
656
- assert(t3.every((l) => visibleWidth(l) <= 30), "expanded: semua baris <= inner");
647
+ assert(t3.every((l) => visibleWidth(l) <= 30), "expanded: every line <= inner");
657
648
  const t4 = titleRow(th, "✓", "$", "echo hi", 30, "", false);
658
- assert(!t4[0].includes("0.1s"), "tanpa durasi: kosong (restore sesi lama)");
649
+ assert(!t4[0].includes("0.1s"), "no duration: empty (restored session)");
650
+
651
+ // crash regression: third-party renderCall throwing at build time (pi-web-access
652
+ // get_content: responseId.slice on undefined) must be contained, not crash pi
653
+ assert(
654
+ safeCall(() => {
655
+ const args: { responseId?: string } = {};
656
+ return args.responseId!.slice(0, 8);
657
+ }) === null,
658
+ "renderCall throws -> null (not a crash)",
659
+ );
660
+ assert(
661
+ componentLines(
662
+ safeCall(() => {
663
+ throw new Error("renderer crash");
664
+ }),
665
+ 10,
666
+ ).length === 0,
667
+ "renderer crash -> 0 lines",
668
+ );
659
669
 
660
670
  // no duplicate summary
661
- assert(resText(R, th, (s) => s, false, true) === "", "expand: ringkasan kosong");
662
- assert(resText(R, th, (s) => s, false, false) === "󱞩 path:1:match a", "collapsed: baris pertama tampil");
671
+ assert(resText(R, th, (s) => s, false, true) === "", "expand: empty summary");
672
+ assert(resText(R, th, (s) => s, false, false) === "󱞩 path:1:match a", "collapsed: first line shown");
663
673
  assert(numRes((n) => `→ ${n} matches`)(R, th, (s) => s, false, false) === "󱞩 → 3 matches", "grep: N matches");
664
674
  assert(
665
675
  numRes((n) => `→ ${n} matches`)({ content: [{ type: "text", text: "Error: bad" }] }, th, (s) => s, true, false) ===
666
676
  "󱞩 Error: bad",
667
- "error: jatuh ke baris pertama",
677
+ "error: falls back to the first line",
668
678
  );
669
679
 
670
680
  // inter-box gap stripped
671
- assert(stripBlankEdges(["", "a", "", "b", " ", ""]).join() === "a,,b", "baris kosong tepi dibuang");
672
- assert(stripBlankEdges(["", "\x1b[2m\x1b[22m", "x"]).join() === "x", "baris ANSI kosong = blank");
673
- assert(stripBlankEdges(["a"]).join() === "a", "tanpa blank tetap utuh");
681
+ assert(stripBlankEdges(["", "a", "", "b", " ", ""]).join() === "a,,b", "blank edge lines dropped");
682
+ assert(stripBlankEdges(["", "\x1b[2m\x1b[22m", "x"]).join() === "x", "blank ANSI line counts as blank");
683
+ assert(stripBlankEdges(["a"]).join() === "a", "no blanks: kept intact");
674
684
 
675
685
  // tools without their own renderer -> same box (memory_write etc.)
676
686
  const noRenderer = { toolName: "memory_write", toolDefinition: {} };
@@ -679,11 +689,11 @@ if (isMain) {
679
689
  assert(
680
690
  ToolExecutionComponent.prototype.hasRendererDefinition.call(noDefinition) === true &&
681
691
  ToolExecutionComponent.prototype.getRenderShell.call(noDefinition) === "self",
682
- "tool tanpa definisi: shell self (kotak kita, bukan fallback polos)",
692
+ "tool without a definition: self shell (our box, not the plain fallback)",
683
693
  );
684
694
  assert(
685
695
  ToolExecutionComponent.prototype.hasRendererDefinition.call({ toolName: "x", toolDefinition: {} }) === true,
686
- "tool dengan definisi tetap punya renderer",
696
+ "tool with a definition still has its renderer",
687
697
  );
688
698
  const noDefOut = (ToolExecutionComponent.prototype.getResultRenderer.call(noDefinition) as any)(
689
699
  { content: [{ type: "text", text: '{\n "pattern": "**/*.ts"\n}\nTool glob not found' }] },
@@ -691,52 +701,52 @@ if (isMain) {
691
701
  th,
692
702
  { args: { pattern: "**/*.ts" }, state: {} },
693
703
  ).render(60);
694
- assert(noDefOut[0].startsWith("\u256d") && noDefOut[noDefOut.length - 1].startsWith("\u2570"), "tool tanpa definisi: hasil dibungkus kotak");
704
+ assert(noDefOut[0].startsWith("\u256d") && noDefOut[noDefOut.length - 1].startsWith("\u2570"), "tool without a definition: result wrapped in a box");
695
705
  assert(
696
706
  noDefOut.some((l: string) => l.includes("\u2713 glob")) &&
697
707
  noDefOut.some((l: string) => l.includes('[ctrl+o to expand]')),
698
- "tool tanpa definisi: judul + ringkasan + expand hint",
708
+ "tool without a definition: title + summary + expand hint",
699
709
  );
700
710
  const withRenderer = { toolName: "bash", toolDefinition: { renderResult: () => undefined } };
701
- assert(ToolExecutionComponent.prototype.getRenderShell.call(noRenderer) === "self", "tool polos -> shell self");
711
+ assert(ToolExecutionComponent.prototype.getRenderShell.call(noRenderer) === "self", "plain tool -> self shell");
702
712
  assert(
703
713
  ToolExecutionComponent.prototype.getRenderShell.call(withRenderer) === "default",
704
- "tool ber-renderer -> shell aslinya (tak disentuh)",
714
+ "tool with a renderer -> its own shell (untouched)",
705
715
  );
706
716
  assert(
707
717
  toolSummary("memory_write", "Appended to MEMORY.md\n\nExisting MEMORY.md preview (1 lines)", {
708
718
  content: "## omaga-sync workflow (2026-08-23)\n- Repo: /x",
709
719
  }) === "Appended to MEMORY.md. ## omaga-sync workflow (2026-08-23)",
710
- "memory_write: ringkasan + judul seksi yang baru ditulis",
720
+ "memory_write: summary + the heading just written",
711
721
  );
712
722
  assert(
713
- toolSummary("memory_write", "Appended to daily log: /x/y.md", { content: "- catatan tanpa judul" }) ===
723
+ toolSummary("memory_write", "Appended to daily log: /x/y.md", { content: "- note without a heading" }) ===
714
724
  "Appended to daily log: /x/y.md",
715
- "memory_write daily: tanpa judul tetap baris pertama",
725
+ "memory_write daily: no heading -> first line",
716
726
  );
717
- assert(toolSummary("todo", "line1\nline2") === "line1", "tool lain: baris pertama saja");
727
+ assert(toolSummary("todo", "line1\nline2") === "line1", "other tools: first line only");
718
728
  assert(
719
729
  toolSummary("mcp", '{\n "url": "https://example.com",\n "ok": true\n}') === '"url": "https://example.com",',
720
- "baris pertama hanya '{' -> ringkasan baris berikutnya (JSON MCP)",
730
+ "first line is just '{' -> next line becomes the summary (MCP JSON)",
721
731
  );
722
732
  assert(
723
733
  toolSummary("mcp", '{\n "url": "https://example.com",\n "ok": true\n}', {}) === '"url": "https://example.com",' &&
724
734
  toolSummary("mcp", "{") === "{",
725
- "ringkasan JSON: pakai baris berisi, fallback ke baris pertama",
735
+ "JSON summary: use the content line, fall back to the first",
726
736
  );
727
- assert(toolSummary("memory_write", "Appended to MEMORY.md") === "Appended to MEMORY.md", "ballast: ringkasan biasa utuh");
737
+ assert(toolSummary("memory_write", "Appended to MEMORY.md") === "Appended to MEMORY.md", "ballast: plain summary kept intact");
728
738
 
729
739
  // distinct subtle bg: memory_write only
730
740
  assert(boxBgOf("memory_write") === "customMessageBg", "memory_write -> bg customMessageBg");
731
- assert(boxBgOf("bash") === undefined && boxBgOf("todo") === undefined, "tool lain tanpa bg tambahan");
741
+ assert(boxBgOf("bash") === undefined && boxBgOf("todo") === undefined, "other tools get no extra bg");
732
742
  const bgSeen: string[] = [];
733
743
  const thBgMem: Theme = { fg: (_c, s) => s, bg: (c, s) => (bgSeen.push(c), `<${s}>`) };
734
744
  const bgLines = box(thBgMem, 20, ["memory_write 0.1s", "󱞩 Appended to MEMORY.md"], boxBgOf("memory_write"));
735
- assert(bgSeen.length === bgLines.length && bgSeen.every((c) => c === "customMessageBg"), "bg dipakai di semua baris kotak");
736
- assert(bgLines.every((l) => visibleWidth(l.replace(/[<>]/g, "")) === 20), "bg tidak merusak lebar baris kotak");
737
- assert(bgLines[0]!.replace(/[<>]/g, "").startsWith("╭"), "garis atas tetap utuh");
745
+ assert(bgSeen.length === bgLines.length && bgSeen.every((c) => c === "customMessageBg"), "bg used on every box line");
746
+ assert(bgLines.every((l) => visibleWidth(l.replace(/[<>]/g, "")) === 20), "bg does not break the box line width");
747
+ assert(bgLines[0]!.replace(/[<>]/g, "").startsWith("╭"), "top line stays intact");
738
748
 
739
- // pi-web-access web_search/fetch_content/get_search_content: isi renderer mereka, kotak kita
749
+ // pi-web-access: their renderer content, our box
740
750
  const paResult: any = { render: () => ["\x1b[32mPi Coding Agent\x1b[39m (77 matches, 77 shown)"] };
741
751
  const paCall: any = { render: () => ["\x1b[1mget_content \x1b[22m\x1b[36mfind 4\x1b[39m"] };
742
752
  const boxedRes = {
@@ -745,7 +755,7 @@ if (isMain) {
745
755
  };
746
756
  assert(
747
757
  ToolExecutionComponent.prototype.getRenderShell.call(boxedRes) === "self",
748
- "pi-web-access: shell dipaksa self (ikut kotak kita, bukan blok bg)",
758
+ "pi-web-access: shell forced to self (joins our box, not a bg block)",
749
759
  );
750
760
  const searchRes = {
751
761
  toolName: "web_search",
@@ -753,18 +763,18 @@ if (isMain) {
753
763
  };
754
764
  assert(
755
765
  ToolExecutionComponent.prototype.getRenderShell.call(searchRes) === "self",
756
- "web_search: shell dipaksa self (ikut kotak kita)",
766
+ "web_search: shell forced to self (joins our box)",
757
767
  );
758
768
  const bRes = ToolExecutionComponent.prototype.getResultRenderer.call(boxedRes) as any;
759
769
  const bOut = bRes(
760
- { content: [{ type: "text", text: "raw panjang yang tak perlu ditampilkan" }], details: { matchCount: 77 } },
770
+ { content: [{ type: "text", text: "raw text that must not be rendered" }], details: { matchCount: 77 } },
761
771
  { expanded: false, isPartial: false },
762
772
  th,
763
773
  { args: {} },
764
774
  ).render(60);
765
- assert(bOut[0].startsWith("╭") && bOut[bOut.length - 1].startsWith("╰"), "pi-web-access: hasil dibungkus kotak kita");
766
- assert(bOut.some((l: string) => l.includes("(77 matches, 77 shown)")), "pi-web-access: info renderer mereka dipertahankan");
767
- assert(!bOut.some((l: string) => l.includes("raw panjang")), "pi-web-access: teks mentah tidak dipakai saat renderer ada");
775
+ assert(bOut[0].startsWith("╭") && bOut[bOut.length - 1].startsWith("╰"), "pi-web-access: result wrapped in our box");
776
+ assert(bOut.some((l: string) => l.includes("(77 matches, 77 shown)")), "pi-web-access: their renderer info kept");
777
+ assert(!bOut.some((l: string) => l.includes("raw text")), "pi-web-access: raw text not used when a renderer exists");
768
778
  assert(
769
779
  bRes(
770
780
  {
@@ -777,7 +787,7 @@ if (isMain) {
777
787
  )
778
788
  .render(60)
779
789
  .some((l: string) => l.includes("x get_content")),
780
- "pi-web-access: details.error -> ikon x (bukan ✓)",
790
+ "pi-web-access: details.error -> x icon (not ✓)",
781
791
  );
782
792
  const bErr = bRes(
783
793
  { content: [{ type: "text", text: "No URL specified. Provide url, urlIndex, or query." }] },
@@ -788,18 +798,18 @@ if (isMain) {
788
798
  assert(
789
799
  bErr.some((l: string) => l.includes("No URL specified")) &&
790
800
  bErr.filter((l: string) => l.includes("╭")).length === 1,
791
- "error: satu kotak saja (tanpa kotak bersarang)",
801
+ "error: a single box (no nested box)",
792
802
  );
793
803
  assert(
794
804
  bRes({ content: [{ type: "text", text: "x" }] }, { expanded: false, isPartial: true }, th, { args: {} }).render(60)
795
805
  .length === 0,
796
- "pi-web-access: fase partial kosong (tanpa kotak ganda)",
806
+ "pi-web-access: empty partial phase (no double box)",
797
807
  );
798
808
  const bCall = ToolExecutionComponent.prototype.getCallRenderer.call(boxedRes) as any;
799
809
  const bCallOut = bCall({}, th, { args: {}, state: {} }).render(60);
800
810
  assert(
801
811
  bCallOut[0].startsWith("╭") && bCallOut.some((l: string) => l.includes("find 4")),
802
- "pi-web-access: baris args renderer mereka masuk kotak",
812
+ "pi-web-access: their renderer arg lines go in the box",
803
813
  );
804
814
 
805
815
  // pi-fff override mode: find/grep re-registered with own renderers, no renderShell
@@ -808,7 +818,7 @@ if (isMain) {
808
818
  const fffFind = { toolName: "find", toolDefinition: { renderCall: () => fffCall, renderResult: () => fffRes } };
809
819
  assert(
810
820
  ToolExecutionComponent.prototype.getRenderShell.call(fffFind) === "self",
811
- "pi-fff find: shell dipaksa self (ikut kotak kita)",
821
+ "pi-fff find: shell forced to self (joins our box)",
812
822
  );
813
823
  const fffOut = (ToolExecutionComponent.prototype.getResultRenderer.call(fffFind) as any)(
814
824
  { content: [{ type: "text", text: "src/a.ts\nsrc/b.ts" }] },
@@ -816,26 +826,26 @@ if (isMain) {
816
826
  th,
817
827
  { args: { pattern: "*.ts", path: "D:/Pi/x" }, state: {} },
818
828
  ).render(60);
819
- assert(fffOut[0].startsWith("╭") && fffOut[fffOut.length - 1].startsWith("╰"), "pi-fff find: hasil dibungkus kotak");
820
- assert(fffOut.some((l: string) => l.includes("*.ts in D:/Pi/x")), "pi-fff find: judul renderCall ikut masuk kotak");
821
- assert(fffOut.some((l: string) => l.includes("6 more lines")), "pi-fff find: ringkasan renderResult ikut masuk kotak");
829
+ assert(fffOut[0].startsWith("╭") && fffOut[fffOut.length - 1].startsWith("╰"), "pi-fff find: result wrapped in a box");
830
+ assert(fffOut.some((l: string) => l.includes("*.ts in D:/Pi/x")), "pi-fff find: renderCall title goes in the box");
831
+ assert(fffOut.some((l: string) => l.includes("6 more lines")), "pi-fff find: renderResult summary goes in the box");
822
832
 
823
833
  const oursFind = { toolName: "find", toolDefinition: { renderShell: "self", renderCall: () => fffCall, renderResult: () => fffRes } };
824
834
  assert(
825
835
  (ToolExecutionComponent.prototype.getCallRenderer.call(oursFind) as any)() === fffCall &&
826
836
  (ToolExecutionComponent.prototype.getResultRenderer.call(oursFind) as any)() === fffRes,
827
- "find kita (renderShell self): renderer sendiri apa adanya, tak dibungkus kotak lagi",
837
+ "our find (renderShell self): its own renderer as-is, not boxed again",
828
838
  );
829
839
 
830
840
  // Guard: source_check is deliberately NOT boxed — its curator partial carries
831
841
  // URLs + approval state, while our partial path returns nothing. Without this
832
842
  // assert a later edit of BOXED_TOOLS would swallow it silently.
833
843
  for (const n of ["source_check"]) {
834
- const theirs = () => "renderer-mereka";
844
+ const theirs = () => "their-renderer";
835
845
  assert(
836
846
  ToolExecutionComponent.prototype.getResultRenderer.call({ toolName: n, toolDefinition: { renderResult: theirs } })() ===
837
- "renderer-mereka",
838
- `${n}: sengaja tidak dipaksa kotak (partial kurator butuh URL)`,
847
+ "their-renderer",
848
+ `${n}: deliberately not boxed (the curator partial needs URLs)`,
839
849
  );
840
850
  }
841
851
 
@@ -845,12 +855,12 @@ if (isMain) {
845
855
  const mcpTool = { toolName: "mcp", toolDefinition: { renderCall: mcpTheirs, renderResult: mcpTheirs } };
846
856
  assert(
847
857
  (ToolExecutionComponent.prototype.getResultRenderer.call(mcpTool) as any) !== mcpTheirs,
848
- "mcp: renderer hasil pihak ketiga dilewati (dump error dibatasi)",
858
+ "mcp: third-party result renderer skipped (error dump capped)",
849
859
  );
850
860
  assert(
851
861
  (ToolExecutionComponent.prototype.getResultRenderer.call({ toolName: "mcp__tinyfish", toolDefinition: { renderResult: mcpTheirs } }) as any) !==
852
862
  mcpTheirs,
853
- "mcp__<server>: renderer mereka dilewati (jalur kotak kita)",
863
+ "mcp__<server>: their renderer skipped (our box path)",
854
864
  );
855
865
  const mcpPayload = {
856
866
  content: [{ type: "text", text: "x Failed to call tool\nu Expected parameters:\n{\n \"required\": []\n}" }],
@@ -862,19 +872,19 @@ if (isMain) {
862
872
  th,
863
873
  { args: { tool: "fetch_content", server: "tinyfish" }, state: {} },
864
874
  ).render(96);
865
- assert(mcpOut.length <= 6, `mcp error collapsed tetap pendek (${mcpOut.length} baris)`);
875
+ assert(mcpOut.length <= 6, `mcp error collapsed stays short (${mcpOut.length} lines)`);
866
876
  assert(
867
877
  mcpOut.some((l: string) => l.includes("fetch_content @ tinyfish")),
868
- "mcp: judul menyebut tool MCP yang dipanggil",
878
+ "mcp: title names the invoked MCP tool",
869
879
  );
870
- assert(mcpOut.some((l: string) => l.includes("[ctrl+o to expand]")), "mcp: ada petunjuk expand");
880
+ assert(mcpOut.some((l: string) => l.includes("[ctrl+o to expand]")), "mcp: expand hint present");
871
881
  const mcpFull = (ToolExecutionComponent.prototype.getResultRenderer.call(mcpTool) as any)(
872
882
  mcpPayload,
873
883
  { expanded: true, isPartial: false },
874
884
  th,
875
885
  { args: { tool: "fetch_content", server: "tinyfish" }, state: {} },
876
886
  ).render(96);
877
- assert(mcpFull.length > mcpOut.length, "mcp: expand menampilkan dump + args lengkap");
887
+ assert(mcpFull.length > mcpOut.length, "mcp: expand shows the full dump + args");
878
888
 
879
889
  // Gap is only stripped for our own box lines.
880
890
  // The default export registers the tools (filling OWN_BOX); a pi stub suffices.
@@ -886,22 +896,22 @@ if (isMain) {
886
896
  } as never);
887
897
  assert(
888
898
  OWN_BOX.has("bash") && OWN_BOX.has("read") && OWN_BOX.has("edit") && OWN_BOX.has("fetch_content") && OWN_BOX.has("web_search"),
889
- "OWN_BOX: tool kotak kita + tool boxed terdaftar",
899
+ "OWN_BOX: our own tools + boxed tools registered",
890
900
  );
891
- assert(!OWN_BOX.has("mcp") && !OWN_BOX.has("source_check"), "OWN_BOX: renderer pihak lain tidak dirapatkan");
892
- assert(hasOwnRendererDef(mcpTool) && !hasOwnRendererDef(noRenderer), "patokan rapat: definisi renderer tool");
901
+ assert(!OWN_BOX.has("mcp") && !OWN_BOX.has("source_check"), "OWN_BOX: third-party renderers not tightened");
902
+ assert(hasOwnRendererDef(mcpTool) && !hasOwnRendererDef(noRenderer), "tightened benchmark: tool renderer definition");
893
903
  assert(
894
904
  capped(th, ["a", "b", "c"], false).length === CALL_ROWS + 1 &&
895
905
  capped(th, ["a", "b", "c"], true).length === 3 &&
896
906
  capped(th, ["a"], false).length === 1,
897
- "baris call pihak ketiga dibatasi hanya saat collapsed",
907
+ "third-party call lines capped only when collapsed",
898
908
  );
899
909
  assert(
900
910
  mcpInfo("bash", { tool: "x" }, th) === "" && mcpInfo("mcp", { tool: "x", server: "s" }, th).includes("x @ s"),
901
- "mcpInfo hanya untuk tool MCP",
911
+ "mcpInfo only for MCP tools",
902
912
  );
903
913
 
904
- const ctxState: TCtx = { args: { content: "## Judul\n- x" }, state: {} };
914
+ const ctxState: TCtx = { args: { content: "## Heading\n- x" }, state: {} };
905
915
  const resRenderer = ToolExecutionComponent.prototype.getResultRenderer.call(noRenderer) as any;
906
916
  const out = resRenderer(
907
917
  { content: [{ type: "text", text: "Appended to MEMORY.md\n\nExisting MEMORY.md preview" }] },
@@ -909,12 +919,12 @@ if (isMain) {
909
919
  th,
910
920
  ctxState,
911
921
  ).render(60);
912
- assert(out[0].startsWith("╭") && out[out.length - 1].startsWith("╰"), "hasil tool polos dibungkus kotak");
922
+ assert(out[0].startsWith("╭") && out[out.length - 1].startsWith("╰"), "plain tool result wrapped in a box");
913
923
  assert(
914
- out.some((l: string) => l.includes("Appended to MEMORY.md. ## Judul")),
915
- "kotak memuat ringkasan + judul",
924
+ out.some((l: string) => l.includes("Appended to MEMORY.md. ## Heading")),
925
+ "box carries the summary + heading",
916
926
  );
917
- assert(out.some((l: string) => l.includes("[ctrl+o to expand]")), "collapsed: ada petunjuk expand");
927
+ assert(out.some((l: string) => l.includes("[ctrl+o to expand]")), "collapsed: expand hint present");
918
928
  assert(
919
929
  resRenderer(
920
930
  { content: [{ type: "text", text: "Appended to MEMORY.md\n\nExisting preview" }] },
@@ -922,22 +932,22 @@ if (isMain) {
922
932
  th,
923
933
  ctxState,
924
934
  ).render(60).length > out.length,
925
- "expand: detail lengkap ikut tampil",
935
+ "expand: full detail shown too",
926
936
  );
927
937
 
928
938
  // stale 󰔟 box regression (restore-safe)
929
939
  const st: Record<string, unknown> = {};
930
940
  const comp = new Lines(() => (st.hasResult ? [] : ["row"]));
931
- assert(comp.render(10).length === 1, "running: kotak 󰔟 tampil");
941
+ assert(comp.render(10).length === 1, "running: 󰔟 box shown");
932
942
  st.hasResult = true;
933
- assert(comp.render(10).length === 0, "final: kotak running hilang (restore-safe)");
934
- assert(FRAMES.includes(spinIcon()), "ikon animasi selalu frame valid");
943
+ assert(comp.render(10).length === 0, "final: running box gone (restore-safe)");
944
+ assert(FRAMES.includes(spinIcon()), "animation icon is always a valid frame");
935
945
  ACTIVE = 3;
936
946
  syncTicker();
937
- assert(TICK !== null, "ticker hidup selama ada tool berjalan");
947
+ assert(TICK !== null, "ticker alive while a tool is running");
938
948
  ACTIVE = 0;
939
949
  syncTicker();
940
- assert(TICK === null, "ticker mati saat idle (tanpa leak timer)");
950
+ assert(TICK === null, "ticker stops when idle (no timer leak)");
941
951
 
942
952
  // todo render: \uf14a + 2 lines (title & status) + background box
943
953
  const todoCall = () => ({ render: () => ["todo → Test subject"] });
@@ -949,7 +959,7 @@ if (isMain) {
949
959
  };
950
960
  const todoCallOut = (ToolExecutionComponent.prototype.getCallRenderer.call(todoTool) as any)({}, thBgTodo, { state: {} }).render(60);
951
961
  assert(todoCallOut[0].includes("[bg:toolPendingBg]"), "todo call: background pending");
952
- assert(todoCallOut.some((l: string) => l.includes("\uf14a todo → Test subject")), "todo call: ikon \uf14a di judul");
962
+ assert(todoCallOut.some((l: string) => l.includes("\uf14a todo → Test subject")), "todo call: \uf14a icon in the title");
953
963
 
954
964
  const todoResultOut = (ToolExecutionComponent.prototype.getResultRenderer.call(todoTool) as any)(
955
965
  { content: [] },
@@ -958,8 +968,8 @@ if (isMain) {
958
968
  { state: {}, args: {} },
959
969
  ).render(60);
960
970
  assert(todoResultOut[0].includes("[bg:toolSuccessBg]"), "todo result: background success");
961
- assert(todoResultOut.some((l: string) => l.includes("\uf14a todo → Test subject")), "todo result: ikon \uf14a di baris 1");
962
- assert(todoResultOut.some((l: string) => l.includes("● completed")), "todo result: status dengan prefix resHead di baris 2");
971
+ assert(todoResultOut.some((l: string) => l.includes("\uf14a todo → Test subject")), "todo result: \uf14a icon on line 1");
972
+ assert(todoResultOut.some((l: string) => l.includes("● completed")), "todo result: status with the resHead prefix on line 2");
963
973
 
964
974
  // Expanded detail: themed markdown, error text intact and no longer plain dim.
965
975
  const errDetail = fullText(
@@ -967,15 +977,15 @@ if (isMain) {
967
977
  th,
968
978
  40,
969
979
  );
970
- const plain = errDetail.join("\n").replace(/\x1b\[[0-9;]*m/g, "");
971
- assert(plain.includes("Tool glob not found") && plain.includes('"pattern"'), "detail error: isi utuh");
980
+ const plain = stripAnsi(errDetail.join("\n"));
981
+ assert(plain.includes("Tool glob not found") && plain.includes('"pattern"'), "error detail: content intact");
972
982
  assert(
973
983
  errDetail.every((l: string) => visibleWidth(l) === 40),
974
- "detail error: dirender markdown (di-wrap + dipad ke lebar), bukan split baris polos",
984
+ "error detail: rendered as markdown (wrapped + padded to width), not a plain line split",
975
985
  );
976
986
  assert(
977
987
  fullText({ content: [{ type: "text", text: "a\nb" }] }, th, 0).join("|") === th.fg("dim", "a") + "|" + th.fg("dim", "b"),
978
- "tanpa width (self-check) tetap dump dim",
988
+ "no width (self-check) still dumps dim",
979
989
  );
980
990
  console.log("OK");
981
991
  }