@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.
- package/CHANGELOG.md +88 -0
- package/extensions/footer.ts +48 -67
- package/extensions/section-headers.ts +87 -112
- package/extensions/timestamps.ts +75 -31
- package/extensions/tools.ts +157 -147
- package/extensions/ui-render-tweaks.ts +101 -119
- package/extensions/usage.ts +17 -25
- package/lib/ansi.ts +22 -9
- package/lib/box.ts +8 -13
- package/lib/check.ts +12 -0
- package/lib/format.ts +33 -0
- package/lib/usage-store.ts +3 -1
- package/package.json +2 -1
- package/themes/gen-themes.mjs +38 -38
package/extensions/tools.ts
CHANGED
|
@@ -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
|
|
12
|
-
* (→ N matches / → N files / N lines); edit ` +N / -M`.
|
|
13
|
-
* summary 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.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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() /
|
|
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); //
|
|
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
|
-
|
|
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
|
|
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
|
|
221
|
-
//
|
|
222
|
-
//
|
|
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
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
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
|
-
//
|
|
247
|
-
// (
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
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
|
|
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 (
|
|
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 =
|
|
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 && !
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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", "
|
|
640
|
-
assert(smartTitle("ls a b c d e f g h") === "ls a b c d e…", "
|
|
641
|
-
assert(smartTitle("a b c d e f g") === "a b c d e f…", "7
|
|
642
|
-
assert(smartTitle("a b c d e f") === "a b c d e f", "6
|
|
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>", "
|
|
647
|
-
assert(paintLinks("echo plain 2>&1", mark) === "echo plain 2>&1", "
|
|
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, "
|
|
652
|
-
assert(t1[0].endsWith("0.1s"), "
|
|
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:
|
|
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:
|
|
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"), "
|
|
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:
|
|
662
|
-
assert(resText(R, th, (s) => s, false, false) === " path:1:match a", "collapsed:
|
|
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:
|
|
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", "
|
|
672
|
-
assert(stripBlankEdges(["", "\x1b[2m\x1b[22m", "x"]).join() === "x", "
|
|
673
|
-
assert(stripBlankEdges(["a"]).join() === "a", "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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: "-
|
|
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:
|
|
725
|
+
"memory_write daily: no heading -> first line",
|
|
716
726
|
);
|
|
717
|
-
assert(toolSummary("todo", "line1\nline2") === "line1", "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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:
|
|
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, "
|
|
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
|
|
736
|
-
assert(bgLines.every((l) => visibleWidth(l.replace(/[<>]/g, "")) === 20), "bg
|
|
737
|
-
assert(bgLines[0]!.replace(/[<>]/g, "").startsWith("╭"), "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
766
|
-
assert(bOut.some((l: string) => l.includes("(77 matches, 77 shown)")), "pi-web-access:
|
|
767
|
-
assert(!bOut.some((l: string) => l.includes("raw
|
|
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 ->
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
820
|
-
assert(fffOut.some((l: string) => l.includes("*.ts in D:/Pi/x")), "pi-fff find:
|
|
821
|
-
assert(fffOut.some((l: string) => l.includes("6 more lines")), "pi-fff find:
|
|
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
|
|
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
|
|
844
|
+
const theirs = () => "their-renderer";
|
|
835
845
|
assert(
|
|
836
846
|
ToolExecutionComponent.prototype.getResultRenderer.call({ toolName: n, toolDefinition: { renderResult: theirs } })() ===
|
|
837
|
-
"renderer
|
|
838
|
-
`${n}:
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
878
|
+
"mcp: title names the invoked MCP tool",
|
|
869
879
|
);
|
|
870
|
-
assert(mcpOut.some((l: string) => l.includes("[ctrl+o to expand]")), "mcp:
|
|
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
|
|
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:
|
|
899
|
+
"OWN_BOX: our own tools + boxed tools registered",
|
|
890
900
|
);
|
|
891
|
-
assert(!OWN_BOX.has("mcp") && !OWN_BOX.has("source_check"), "OWN_BOX:
|
|
892
|
-
assert(hasOwnRendererDef(mcpTool) && !hasOwnRendererDef(noRenderer), "
|
|
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
|
-
"
|
|
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
|
|
911
|
+
"mcpInfo only for MCP tools",
|
|
902
912
|
);
|
|
903
913
|
|
|
904
|
-
const ctxState: TCtx = { args: { content: "##
|
|
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("╰"), "
|
|
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. ##
|
|
915
|
-
"
|
|
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:
|
|
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
|
|
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:
|
|
941
|
+
assert(comp.render(10).length === 1, "running: box shown");
|
|
932
942
|
st.hasResult = true;
|
|
933
|
-
assert(comp.render(10).length === 0, "final:
|
|
934
|
-
assert(FRAMES.includes(spinIcon()), "
|
|
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
|
|
947
|
+
assert(TICK !== null, "ticker alive while a tool is running");
|
|
938
948
|
ACTIVE = 0;
|
|
939
949
|
syncTicker();
|
|
940
|
-
assert(TICK === null, "ticker
|
|
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:
|
|
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:
|
|
962
|
-
assert(todoResultOut.some((l: string) => l.includes("● completed")), "todo result: status
|
|
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")
|
|
971
|
-
assert(plain.includes("Tool glob not found") && plain.includes('"pattern"'), "detail
|
|
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
|
|
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
|
-
"
|
|
988
|
+
"no width (self-check) still dumps dim",
|
|
979
989
|
);
|
|
980
990
|
console.log("OK");
|
|
981
991
|
}
|