@bismawy/pi-arnative 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +10 -1
- package/extensions/tools.ts +250 -8
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.4] - 2026-10-04
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `scripts/release-check.mjs`, a read-only release preflight with one stage per step of the release (`pre-tag`, `pre-publish`, `post-publish`): it verifies a clean tree on `main` in sync with `origin/main`, no open pull requests, the `CHANGELOG.md` entry, whether the tag exists **and is pushed**, and whether the registry already serves the version. It never writes, tags, pushes or publishes.
|
|
7
|
+
- `npm run release:check` (the `pre-tag` stage) and a `prepublishOnly` hook wired to the `pre-publish` stage. A missing or unpushed tag now aborts `npm publish` with `npm error code 1`, so a forgotten `git tag` can no longer ship a release.
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- README documents the release stages and their exit codes (`0` pass, `1` check failed, `2` bad usage).
|
|
11
|
+
|
|
12
|
+
## [0.3.3] - 2026-10-04
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `codemode` now renders in the arnative box like every other tool call: side borders, green background (`toolSuccessBg`), and the script's first line prefixed with the `` summary glyph. While the tool runs the box shows the shared spinner; the `\uf489` glyph appears with the result (`extensions/tools.ts`).
|
|
16
|
+
- Collapsed `codemode` is now two rows, the same footprint as `bash`/`grep`/`read`: title + the script's first line. Nested calls and output wait behind the standard `[ctrl+o to expand]` hint. A failing run still shows its output.
|
|
17
|
+
- pi's `Script completed / Wall time / Output:` header no longer shows in the box.
|
|
18
|
+
- Shared duration wording (`2ms` under a second, `1.4s` above) and one error glyph (`x`) across every tool; a failing `codemode` run shows its whole output and never a `[ctrl+o to expand]` hint.
|
|
19
|
+
- The file tools (`bash`, `read`, `write`, `grep`, `find`, `edit`) now show `[ctrl+o to expand]` when their collapsed summary hides more than it shows — a two-line `extra +1 / -1` box no longer looks complete while its diff sits behind it. Expanded and genuinely single-line results get no hint (`extensions/tools.ts`).
|
|
20
|
+
- The `chrome_devtools_*` tools (from `@narumitw/pi-chrome-devtools`) now render in the same box instead of their bare "Chrome DevTools: …" lines. Their own `renderCall`/`renderResult` are bypassed: the call box uses the Chrome glyph and the result box the usual summary + `[ctrl+o to expand]` (`extensions/tools.ts`).
|
|
21
|
+
|
|
3
22
|
## [0.3.2] - 2026-10-04
|
|
4
23
|
|
|
5
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ pi-arnative replaces pi’s default terminal chrome with a unified, cohesive vis
|
|
|
14
14
|
|
|
15
15
|
- Warm Themes: Cohesive, contrast-checked color palettes generated without drift.
|
|
16
16
|
- Tabbed Header: Greeting, version line, and a compact box menu over Directory, Model, Context, Skills, Extensions, and Shortcut.
|
|
17
|
-
- Rounded Tool Boxes: Clear status, spinner, and duration for every tool call.
|
|
17
|
+
- Rounded Tool Boxes: Clear status, spinner, and duration for every tool call. Built-in `codemode` is boxed and collapsed to two rows (title + first script line) like `bash`/`grep`/`read`; nested calls and output sit behind `[ctrl+o to expand]` (shown whenever the collapsed summary hides more, including `edit` diffs); a failed run always shows its full output. The `chrome_devtools_*` tools box too, replacing their bare "Chrome DevTools: …" lines.
|
|
18
18
|
- Enhanced Footer: fixed 3-row grid — directory/git line, then extension status paired with token metrics.
|
|
19
19
|
- Transcript Clock: Timestamps for messages with clean bubble backgrounds.
|
|
20
20
|
- Usage Dashboard: /usage command to track tokens and session costs.
|
|
@@ -96,6 +96,15 @@ npm test # Run self-checks & theme generator check
|
|
|
96
96
|
npm run themes # Rebuild all theme variants
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
+
**Releasing:** `scripts/release-check.mjs` is a read-only preflight (never writes, tags, pushes or publishes) with one stage per step of the release:
|
|
100
|
+
|
|
101
|
+
| Stage | Run before | Checks |
|
|
102
|
+
| :--- | :--- | :--- |
|
|
103
|
+
| `pre-tag` | `git tag` | clean tree on `main`, in sync, no open PRs, `CHANGELOG.md` entry, tag free, version unpublished |
|
|
104
|
+
| `pre-publish` | `npm publish` | the above, plus the tag exists **and is pushed** |
|
|
105
|
+
| `post-publish` | done | the above, plus the version is live on the registry |
|
|
106
|
+
|
|
107
|
+
`pre-publish` runs automatically through npm's `prepublishOnly`, so a missing or unpushed tag aborts the publish (`npm error code 1`) instead of shipping a version whose tag was forgotten. `npm run release:check` is the `pre-tag` shorthand. Exit codes: `0` pass, `1` a check failed, `2` bad usage.
|
|
99
108
|
</details>
|
|
100
109
|
|
|
101
110
|
## License
|
package/extensions/tools.ts
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
createReadTool,
|
|
28
28
|
createWriteTool,
|
|
29
29
|
getMarkdownTheme,
|
|
30
|
+
highlightCode,
|
|
31
|
+
initTheme,
|
|
30
32
|
ToolExecutionComponent,
|
|
31
33
|
} from "@earendil-works/pi-coding-agent";
|
|
32
34
|
import { Markdown, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
@@ -64,6 +66,12 @@ export function spinIcon(): string {
|
|
|
64
66
|
return FRAMES[Math.floor(Date.now() / 500) % FRAMES.length];
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** Tool duration: `2ms` under a second, `1.4s` above — the wording pi itself uses. */
|
|
70
|
+
function fmtMs(ms: number | undefined): string {
|
|
71
|
+
if (ms === undefined || !Number.isFinite(ms)) return "";
|
|
72
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
export class Lines {
|
|
68
76
|
private get: (width: number) => string[];
|
|
69
77
|
constructor(get: (width: number) => string[]) {
|
|
@@ -258,7 +266,26 @@ const BOXED_TOOLS = new Map([
|
|
|
258
266
|
["get_search_content", "get_content"],
|
|
259
267
|
["todo", "todo"],
|
|
260
268
|
]);
|
|
261
|
-
|
|
269
|
+
|
|
270
|
+
// @narumitw/pi-chrome-devtools ships bare-text renderers ("Chrome DevTools: navigate",
|
|
271
|
+
// collapsed output is empty) so they are ignored and these tools take our default box
|
|
272
|
+
// path. CDP_ICON marks them in the call box (no hook for the generic tool icon).
|
|
273
|
+
const CDP_ICON = "\uf268";
|
|
274
|
+
const CDP_TOOLS: [string, string][] = [
|
|
275
|
+
["chrome_devtools_load", "load"],
|
|
276
|
+
["chrome_devtools_list_pages", "list pages"],
|
|
277
|
+
["chrome_devtools_select_page", "select page"],
|
|
278
|
+
["chrome_devtools_navigate", "navigate"],
|
|
279
|
+
["chrome_devtools_evaluate", "evaluate"],
|
|
280
|
+
["chrome_devtools_screenshot", "screenshot"],
|
|
281
|
+
["chrome_devtools_webmcp_list_tools", "list WebMCP tools"],
|
|
282
|
+
["chrome_devtools_webmcp_call_tool", "call WebMCP tool"],
|
|
283
|
+
];
|
|
284
|
+
const CDP_NAME = new Map(CDP_TOOLS);
|
|
285
|
+
for (const [n] of CDP_TOOLS) BOXED_TOOLS.set(n, n);
|
|
286
|
+
|
|
287
|
+
const displayName = (name: string): string =>
|
|
288
|
+
CDP_NAME.has(name) ? `Chrome DevTools: ${CDP_NAME.get(name)}` : (BOXED_TOOLS.get(name) ?? name);
|
|
262
289
|
for (const n of BOXED_TOOLS.keys()) OWN_BOX.add(n);
|
|
263
290
|
|
|
264
291
|
const MCP_TOOL = (name: string): boolean => name === "mcp" || name === "mcpScript" || name.startsWith("mcp__");
|
|
@@ -347,11 +374,12 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
347
374
|
const name: string = this.toolName;
|
|
348
375
|
const mine = usesTheirCallRenderer(this);
|
|
349
376
|
const own = origCall.call(this);
|
|
350
|
-
|
|
377
|
+
// Chrome DevTools' own renderCall is a bare line; ours replaces it (icon + name).
|
|
378
|
+
if (own && !mine && !CDP_NAME.has(name)) return own;
|
|
351
379
|
return (args: any, th: Theme, ctx: TCtx) =>
|
|
352
380
|
new Lines((width) => {
|
|
353
381
|
if ((ctx.state as Record<string, unknown> | undefined)?.hasResult) return [];
|
|
354
|
-
const icon = th.fg("warning", spinIcon());
|
|
382
|
+
const icon = CDP_NAME.has(name) ? th.fg("accent", CDP_ICON) : th.fg("warning", spinIcon());
|
|
355
383
|
const theirs = own ? componentLines(safeCall(() => own.call(this, args, th, ctx)), width - 6) : [];
|
|
356
384
|
if (name === "todo" && theirs.length)
|
|
357
385
|
return box(th, width, formatTodoRows(theirs, th), "toolPendingBg");
|
|
@@ -370,6 +398,9 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
370
398
|
// MCP: their result renderer is deliberately skipped (unbounded error dump).
|
|
371
399
|
if (own && !usesTheirCallRenderer(this)) return own;
|
|
372
400
|
const boxed = BOXED_TOOLS.has(name);
|
|
401
|
+
// Chrome DevTools are in BOXED_TOOLS for the title/icon only: their renderResult
|
|
402
|
+
// (raw text, empty when collapsed) would drop our title row, so it is ignored.
|
|
403
|
+
const cdp = CDP_NAME.has(name);
|
|
373
404
|
return (result: TResult, opts: { expanded: boolean; isPartial?: boolean }, th: Theme, ctx: TCtx) => {
|
|
374
405
|
if (opts.isPartial) return EMPTY;
|
|
375
406
|
((ctx.state ??= {}) as Record<string, unknown>).hasResult = true;
|
|
@@ -380,7 +411,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
380
411
|
// Errors use our own path: pi-web-access' own error box is a box inside a
|
|
381
412
|
// box. Their renderer content is only used on success.
|
|
382
413
|
const theirs =
|
|
383
|
-
(boxed || fff) && !isErr
|
|
414
|
+
(boxed || fff) && !isErr && !cdp
|
|
384
415
|
? safeCall(() => own.call(this, { content: result.content, details: result.details }, opts, th, ctx))
|
|
385
416
|
: null;
|
|
386
417
|
return new Lines((width) => {
|
|
@@ -399,7 +430,7 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
399
430
|
const bgType = isErr ? "toolErrorBg" : "toolSuccessBg";
|
|
400
431
|
return box(th, width, formatTodoRows(combined, th), bgType);
|
|
401
432
|
}
|
|
402
|
-
if (boxed2.length) return box(th, width, boxedRows(icon, boxed2));
|
|
433
|
+
if (boxed2.length && !cdp) return box(th, width, boxedRows(icon, boxed2));
|
|
403
434
|
const rows = titleRow(
|
|
404
435
|
th,
|
|
405
436
|
icon,
|
|
@@ -441,6 +472,78 @@ const formatTodoRows = (lines: string[], th: Theme): string[] => {
|
|
|
441
472
|
return out;
|
|
442
473
|
};
|
|
443
474
|
|
|
475
|
+
// codemode: the script is highlighted, the calls and output come from pi's own details.
|
|
476
|
+
function codeLines(code: string): string[] {
|
|
477
|
+
const js = code.replace(/\r/g, "").trimEnd().replace(/\t/g, " ");
|
|
478
|
+
if (!js) return [];
|
|
479
|
+
try {
|
|
480
|
+
return highlightCode(js, "javascript");
|
|
481
|
+
} catch {
|
|
482
|
+
return js.split("\n");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** `✓ todo {"action":"list"} 2ms` — status icon, tool name, muted args, dim duration. */
|
|
487
|
+
function formatCodemodeCall(call: any, th: Theme): string {
|
|
488
|
+
const [color, glyph] = STATUS_ICON[String(call?.status ?? "ok")] ?? ["error", "x"];
|
|
489
|
+
const dur = fmtMs(Number(call?.durationMs));
|
|
490
|
+
let line = ` ${th.fg(color, glyph)} ${th.fg("accent", String(call?.name ?? ""))}`;
|
|
491
|
+
if (call?.args) line += ` ${th.fg("muted", String(call.args))}`;
|
|
492
|
+
if (dur) line += ` ${th.fg("dim", dur)}`;
|
|
493
|
+
return line;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// codemode call status -> [theme colour, glyph], same mapping pi's own renderer uses.
|
|
497
|
+
const STATUS_ICON: Record<string, [string, string]> = {
|
|
498
|
+
running: ["warning", "…"],
|
|
499
|
+
ok: ["success", "✓"],
|
|
500
|
+
cancelled: ["muted", "⊘"],
|
|
501
|
+
error: ["error", "x"],
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/** Drop pi's "Script completed\nWall time …\nOutput:" header (a rejected input has none). */
|
|
505
|
+
function codemodeOutput(content: unknown): string {
|
|
506
|
+
const blocks = (Array.isArray(content) ? content : []).filter((c: any) => c?.type === "text") as Array<{ text?: string }>;
|
|
507
|
+
const header = /^Script (completed|failed)\nWall time [\d.]+ seconds\nOutput:\n/;
|
|
508
|
+
return blocks
|
|
509
|
+
.map((c, i) => (i === 0 ? String(c.text ?? "").replace(header, "") : String(c.text ?? "")))
|
|
510
|
+
.join("\n")
|
|
511
|
+
.trim();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/** Script lines; collapsed keeps only the first, like the other tools' one-line call. */
|
|
515
|
+
function codemodeScript(code: string, expanded: boolean): { lines: string[]; hidden: number } {
|
|
516
|
+
const lines = codeLines(code);
|
|
517
|
+
if (expanded || lines.length <= 1) return { lines, hidden: 0 };
|
|
518
|
+
return { lines: lines.slice(0, 1), hidden: lines.length - 1 };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Every box row: title, script, nested calls, output. Collapsed is the title plus the
|
|
522
|
+
* script's first line, with the rest behind the expand hint. Expanding — or failing —
|
|
523
|
+
* shows everything: a truncated error would read as a short, successful run. */
|
|
524
|
+
function codemodeBoxRows(
|
|
525
|
+
args: any,
|
|
526
|
+
result: { content?: unknown; details?: any } | undefined,
|
|
527
|
+
th: Theme,
|
|
528
|
+
expanded: boolean,
|
|
529
|
+
isErr: boolean,
|
|
530
|
+
icon: string,
|
|
531
|
+
): string[] {
|
|
532
|
+
const rows = [`${icon} ${th.fg("accent", "codemode")}`];
|
|
533
|
+
const script = codemodeScript(typeof args?.code === "string" ? args.code : "", expanded);
|
|
534
|
+
script.lines.forEach((line, i) => rows.push(i === 0 ? `${resHead(th)} ${line}` : ` ${line}`));
|
|
535
|
+
const calls = (result?.details?.calls as any[] | undefined) ?? [];
|
|
536
|
+
const output = codemodeOutput(result?.content);
|
|
537
|
+
const outLines = output ? output.split("\n") : [];
|
|
538
|
+
const shown = expanded || isErr;
|
|
539
|
+
if (shown) {
|
|
540
|
+
if (calls.length || outLines.length) rows.push("");
|
|
541
|
+
rows.push(...calls.map((c) => formatCodemodeCall(c, th)), ...outLines.map((l) => th.fg("toolOutput", ` ${l}`)));
|
|
542
|
+
}
|
|
543
|
+
const hidden = script.hidden + (shown ? 0 : calls.length + outLines.length);
|
|
544
|
+
if (!expanded && hidden > 0) rows[rows.length - 1] += ` ${expandHint(th)}`;
|
|
545
|
+
return rows;
|
|
546
|
+
}
|
|
444
547
|
|
|
445
548
|
// One render path for all tools (no per-tool duplication).
|
|
446
549
|
// renderCall = running box (); renderResult = final box (✓/x + right-aligned
|
|
@@ -499,8 +602,7 @@ function minimal(
|
|
|
499
602
|
((context.state ??= {}) as Record<string, unknown>).hasResult = true;
|
|
500
603
|
const text = textOf(result);
|
|
501
604
|
const isErr = Boolean(context.isError || result.isError || text.startsWith("Error"));
|
|
502
|
-
const
|
|
503
|
-
const dur = ms !== undefined ? `${(ms / 1000).toFixed(1)}s` : "";
|
|
605
|
+
const dur = fmtMs(context.toolCallId ? TIMINGS.get(context.toolCallId) : undefined);
|
|
504
606
|
const icon = isErr ? theme.fg("error", "x") : theme.fg("success", "✓");
|
|
505
607
|
return new Lines((width) => {
|
|
506
608
|
const soft = accentSoftOf(theme);
|
|
@@ -514,9 +616,15 @@ function minimal(
|
|
|
514
616
|
dur,
|
|
515
617
|
expanded,
|
|
516
618
|
);
|
|
619
|
+
// Anything the collapsed summary hides gets the standard hint; without it a
|
|
620
|
+
// two-line box (edit: `+1 / -1`) looks complete while the diff sits behind it.
|
|
621
|
+
const hasDetail = countLines(text) > 1 || Boolean((result.details as { diff?: string } | undefined)?.diff);
|
|
517
622
|
const resLine = res(result, theme, soft, isErr, expanded);
|
|
518
|
-
if (resLine) rows.push(resLine);
|
|
519
623
|
if (expanded) rows.push(...full(result, theme));
|
|
624
|
+
else if (resLine) {
|
|
625
|
+
const hint = hasDetail ? ` ${expandHint(theme)}` : "";
|
|
626
|
+
rows.push(hint ? `${truncateToWidth(resLine, Math.max(8, inner - visibleWidth(hint)), "…")}${hint}` : resLine);
|
|
627
|
+
}
|
|
520
628
|
return box(theme, width, rows);
|
|
521
629
|
});
|
|
522
630
|
},
|
|
@@ -524,6 +632,12 @@ function minimal(
|
|
|
524
632
|
}
|
|
525
633
|
|
|
526
634
|
function arnativeTools(pi: ExtensionAPI) {
|
|
635
|
+
// codemode keeps pi's own data (script + nested calls + output) wrapped in the arnative
|
|
636
|
+
// box, bg like every other tool. A resolver runs first and returns our renderers for
|
|
637
|
+
// that name; `next()` would give pi's unboxed ones.
|
|
638
|
+
OWN_BOX.add("codemode");
|
|
639
|
+
pi.registerToolRenderer((toolName, next) => (toolName === "codemode" ? codemodeRenderers : next()));
|
|
640
|
+
|
|
527
641
|
// Re-render source for the spinner + ticker cleanup when the session closes
|
|
528
642
|
pi.on("session_start", async (_event, ctx) => {
|
|
529
643
|
requestRenderFn = () => {
|
|
@@ -626,6 +740,33 @@ function arnativeTools(pi: ExtensionAPI) {
|
|
|
626
740
|
);
|
|
627
741
|
}
|
|
628
742
|
|
|
743
|
+
// codemode: pi's renderer wrapped in our box. renderShell "self" keeps the
|
|
744
|
+
// execution defaults; renderCall/renderResult draw the box. state.hasResult
|
|
745
|
+
// guarantees one box per call, also after reload/restore.
|
|
746
|
+
const codemodeRenderers = {
|
|
747
|
+
renderShell: "self" as const,
|
|
748
|
+
renderCall(args: any, th: Theme, ctx: TCtx) {
|
|
749
|
+
return new Lines((width) => {
|
|
750
|
+
if (ctx.state?.hasResult) return [];
|
|
751
|
+
const isErr = Boolean(ctx.isError);
|
|
752
|
+
const bg = isErr ? "toolErrorBg" : "toolPendingBg";
|
|
753
|
+
// Same spinner as every other box while the tool runs; the codemode glyph
|
|
754
|
+
// only appears once the result is in (renderResult).
|
|
755
|
+
const icon = th.fg("warning", spinIcon());
|
|
756
|
+
return box(th, width, codemodeBoxRows(args, undefined, th, Boolean(ctx.expanded), isErr, icon), bg);
|
|
757
|
+
});
|
|
758
|
+
},
|
|
759
|
+
renderResult(result: TResult & { details?: any }, opts: { expanded: boolean; isPartial?: boolean }, th: Theme, ctx: TCtx) {
|
|
760
|
+
if (opts.isPartial) return EMPTY;
|
|
761
|
+
((ctx.state ??= {}) as Record<string, unknown>).hasResult = true;
|
|
762
|
+
const isErr = Boolean(ctx.isError || result.isError);
|
|
763
|
+
const icon = isErr ? th.fg("error", "x") : th.fg("success", "\uf489");
|
|
764
|
+
return new Lines((width) =>
|
|
765
|
+
box(th, width, codemodeBoxRows(ctx.args, result, th, Boolean(opts.expanded), isErr, icon), isErr ? "toolErrorBg" : "toolSuccessBg"),
|
|
766
|
+
);
|
|
767
|
+
},
|
|
768
|
+
};
|
|
769
|
+
|
|
629
770
|
export default arnativeTools;
|
|
630
771
|
|
|
631
772
|
// Self-check: `node extensions/tools.ts`.
|
|
@@ -914,6 +1055,7 @@ if (isMain(import.meta.url)) {
|
|
|
914
1055
|
// The default export registers the tools (filling OWN_BOX); a pi stub suffices.
|
|
915
1056
|
arnativeTools({
|
|
916
1057
|
registerTool: () => {},
|
|
1058
|
+
registerToolRenderer: () => {},
|
|
917
1059
|
on: () => {},
|
|
918
1060
|
registerShortcut: () => {},
|
|
919
1061
|
registerMessageRenderer: () => {},
|
|
@@ -1011,5 +1153,105 @@ if (isMain(import.meta.url)) {
|
|
|
1011
1153
|
fullText({ content: [{ type: "text", text: "a\nb" }] }, th, 0).join("|") === th.fg("dim", "a") + "|" + th.fg("dim", "b"),
|
|
1012
1154
|
"no width (self-check) still dumps dim",
|
|
1013
1155
|
);
|
|
1156
|
+
|
|
1157
|
+
// codemode: one box per call, driven through the real component (what pi uses).
|
|
1158
|
+
const cmArgs = { code: 'const l = await tools.todo({ action: "list" });\nconsole.log(l);' };
|
|
1159
|
+
const cmRes = {
|
|
1160
|
+
content: [
|
|
1161
|
+
{ type: "text", text: "Script completed\nWall time 0.01 seconds\nOutput:\n" },
|
|
1162
|
+
{ type: "text", text: "alpha: ok" },
|
|
1163
|
+
],
|
|
1164
|
+
details: { calls: [{ status: "ok", name: "todo", args: '{"action":"list"}', durationMs: 2 }] },
|
|
1165
|
+
};
|
|
1166
|
+
initTheme(); // ToolExecutionComponent resolves a real theme, not the fg-passthrough stub
|
|
1167
|
+
const cmRender = (args: any, result: any, expanded = false) => {
|
|
1168
|
+
const c: any = new ToolExecutionComponent("codemode", "1", args, {}, codemodeRenderers as any, { requestRender() {} }, cwd);
|
|
1169
|
+
if (result) c.updateResult(result, false);
|
|
1170
|
+
c.setExpanded(expanded);
|
|
1171
|
+
// pi prefixes the box with a spacer blank line; drop blanks so the rows are predictable.
|
|
1172
|
+
return (c.render(100) as string[]).map(stripAnsi).filter((l) => l !== "");
|
|
1173
|
+
};
|
|
1174
|
+
const cmCollapsed = cmRender(cmArgs, cmRes);
|
|
1175
|
+
assert(cmCollapsed.length === 4 && cmCollapsed[0]!.startsWith("╭") && cmCollapsed[3]!.startsWith("╰"), "codemode: collapsed box is title + one script line inside the border");
|
|
1176
|
+
assert(cmCollapsed[2]!.includes("const l = await tools.todo") && cmCollapsed[2]!.includes("[ctrl+o to expand]"), "codemode: collapsed shows the first script line + hint");
|
|
1177
|
+
const cmExpanded = cmRender(cmArgs, cmRes, true);
|
|
1178
|
+
assert(cmExpanded.some((l) => l.includes("✓ todo") && l.includes("2ms")) && cmExpanded.some((l) => l.includes("alpha: ok")), "codemode: expanded shows the nested calls and the output");
|
|
1179
|
+
// A failing run is never truncated: the whole output is shown without a hint.
|
|
1180
|
+
const longErr = { content: [{ type: "text", text: "Script failed\nWall time 0.1 seconds\nOutput:\n" + Array.from({ length: 12 }, (_, i) => `line ${i + 1}`).join("\n") }], isError: true };
|
|
1181
|
+
const cmErr = cmRender({ code: "boom()" }, longErr);
|
|
1182
|
+
assert(cmErr.some((l) => l.includes("line 12")) && !cmErr.some((l) => l.includes("to expand")), "codemode: a failed run shows all its output, no hint");
|
|
1183
|
+
// Running: the shared spinner, not the codemode glyph, and no hint while nothing is hidden.
|
|
1184
|
+
const cmRunning = cmRender({ code: "console.log(1);" }, undefined);
|
|
1185
|
+
assert(FRAMES.some((f) => cmRunning.join("\n").includes(f)) && !cmRunning.join("\n").includes("\uf489"), "codemode: running box uses the shared spinner, not the codemode glyph");
|
|
1186
|
+
assert(!cmRunning.some((l) => l.includes("to expand")), "codemode: nothing hidden on a single-line script");
|
|
1187
|
+
assert([...cmCollapsed, ...cmExpanded, ...cmErr].every((l) => visibleWidth(l) === 100), "codemode: box fills the given width");
|
|
1188
|
+
assert(fmtMs(2) === "2ms" && fmtMs(1400) === "1.4s" && fmtMs(undefined) === "", "duration wording: ms under a second, s above");
|
|
1189
|
+
assert(codemodeOutput(cmRes.content) === "alpha: ok", "codemode: Script completed header dropped (own block)");
|
|
1190
|
+
assert(codemodeOutput([{ type: "text", text: "Script completed\nWall time 0.01 seconds\nOutput:\n2" }]) === "2", "codemode: header dropped when joined with the output");
|
|
1191
|
+
// minimal(): a collapsed summary hides more than it shows -> hint; expanded shows all.
|
|
1192
|
+
{
|
|
1193
|
+
const reg2: Record<string, any> = {};
|
|
1194
|
+
const fakePi: any = { registerTool: (t: any) => { reg2[t.name] = t; }, registerToolRenderer: () => {}, on: () => {} };
|
|
1195
|
+
arnativeTools(fakePi);
|
|
1196
|
+
const minRender = (name: string, args: any, result: any, expanded = false) => {
|
|
1197
|
+
const c: any = new ToolExecutionComponent(name, "1", args, {}, reg2[name], { requestRender() {} }, cwd);
|
|
1198
|
+
c.updateResult(result, false);
|
|
1199
|
+
c.setExpanded(expanded);
|
|
1200
|
+
return (c.render(74) as string[]).map(stripAnsi).filter((l) => l !== "");
|
|
1201
|
+
};
|
|
1202
|
+
const diffRes = { content: [{ type: "text", text: "ok" }], details: { diff: "@@\n-old\n+new" } };
|
|
1203
|
+
const ed = minRender("edit", { path: "README.md" }, diffRes);
|
|
1204
|
+
assert(ed.some((l) => l.includes("+1 / -1") && l.includes("to expand")), "minimal(): collapsed edit summary carries the expand hint");
|
|
1205
|
+
assert(!minRender("edit", { path: "README.md" }, diffRes, true).some((l) => l.includes("to expand")), "minimal(): expanded edit drops the hint");
|
|
1206
|
+
assert(
|
|
1207
|
+
minRender("bash", { command: "true" }, { content: [{ type: "text", text: "done" }], details: {} }).every((l) => !l.includes("to expand")),
|
|
1208
|
+
"minimal(): a single-line output has nothing hidden, so no hint",
|
|
1209
|
+
);
|
|
1210
|
+
assert(
|
|
1211
|
+
minRender("edit", { path: "README.md" }, diffRes).every((l) => visibleWidth(l) === 74),
|
|
1212
|
+
"minimal(): hint row still fills the box width",
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Chrome DevTools: their bare renderers are replaced by our box (title + summary).
|
|
1217
|
+
const textComp = (text: string, color?: string) => ({
|
|
1218
|
+
render: (w: number) => (text ? text.split("\n").map((l) => (color ? th.fg(color, l) : l).slice(0, Math.max(0, w))) : []),
|
|
1219
|
+
invalidate() {},
|
|
1220
|
+
});
|
|
1221
|
+
const cdpRes = {
|
|
1222
|
+
toolName: "chrome_devtools_navigate",
|
|
1223
|
+
toolDefinition: {
|
|
1224
|
+
renderCall: () => textComp("Chrome DevTools: navigate"),
|
|
1225
|
+
renderResult: () => textComp(""),
|
|
1226
|
+
},
|
|
1227
|
+
};
|
|
1228
|
+
// The call renderer is not theirs (their bare line would swallow our icon/title).
|
|
1229
|
+
assert(
|
|
1230
|
+
ToolExecutionComponent.prototype.getCallRenderer.call(cdpRes) !== (cdpRes.toolDefinition.renderCall as any)(),
|
|
1231
|
+
"chrome-devtools: their bare call renderer is replaced by our box",
|
|
1232
|
+
);
|
|
1233
|
+
const cdpResult = { content: [{ type: "text", text: 'navigated\n{\n "pageId": "p1",\n "url": "https://example.com"\n}' }], details: {} };
|
|
1234
|
+
const cdpComp = (result: any, expanded = false) => {
|
|
1235
|
+
const c: any = new ToolExecutionComponent("chrome_devtools_navigate", "1", { url: "https://example.com" }, {}, cdpRes.toolDefinition as any, { requestRender() {} }, cwd);
|
|
1236
|
+
if (result) c.updateResult(result, false);
|
|
1237
|
+
c.setExpanded(expanded);
|
|
1238
|
+
return (c.render(74) as string[]).map(stripAnsi).filter((l) => l !== "");
|
|
1239
|
+
};
|
|
1240
|
+
const cdpCollapsed = cdpComp(cdpResult);
|
|
1241
|
+
assert(
|
|
1242
|
+
cdpCollapsed[0]!.startsWith("╭") && cdpCollapsed[cdpCollapsed.length - 1]!.startsWith("╰") && cdpCollapsed.some((l) => l.includes("navigate")),
|
|
1243
|
+
"chrome-devtools: result renders in our box with the tool name",
|
|
1244
|
+
);
|
|
1245
|
+
const cdpCall = ToolExecutionComponent.prototype.getCallRenderer.call(cdpRes) as any;
|
|
1246
|
+
const cdpCallOut: string[] = cdpCall({ url: "https://example.com" }, th, { args: {}, state: {} }).render(74);
|
|
1247
|
+
assert(
|
|
1248
|
+
cdpCallOut[0]!.startsWith("╭") && cdpCallOut.some((l) => stripAnsi(l).includes("Chrome DevTools: navigate")),
|
|
1249
|
+
"chrome-devtools: call box keeps the tool title and the CDP glyph",
|
|
1250
|
+
);
|
|
1251
|
+
assert(stripAnsi(cdpCallOut[1]!).includes(CDP_ICON), "chrome-devtools: call box uses the Chrome glyph, not the spinner");
|
|
1252
|
+
const cdpExpanded = cdpComp(cdpResult, true);
|
|
1253
|
+
assert(cdpExpanded.some((l) => l.includes('"pageId": "p1"')), "chrome-devtools: expanded shows the full tool output");
|
|
1254
|
+
assert(cdpCollapsed.some((l) => l.includes("navigated") && l.includes("to expand")), "chrome-devtools: collapsed shows the first line + hint");
|
|
1255
|
+
assert([...cdpCollapsed, ...cdpExpanded].every((l) => visibleWidth(l) === 74), "chrome-devtools: box fills the given width");
|
|
1014
1256
|
console.log("OK");
|
|
1015
1257
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Refined aesthetics. Cohesive tools. Built for Pi. Warm themes, rounded tool boxes, tabbed header, compact footer and a /usage dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
"scripts": {
|
|
43
43
|
"dev": "pi --extension ./extensions/tools.ts --extension ./extensions/section-headers.ts --extension ./extensions/footer.ts --extension ./extensions/timestamps.ts --extension ./extensions/ui-render-tweaks.ts --extension ./extensions/usage.ts --extension ./extensions/arnative.ts",
|
|
44
44
|
"test": "node lib/box.ts && node lib/ansi.ts && node lib/shortcuts.ts && node themes/gen-themes.mjs --check && node extensions/tools.ts && node extensions/footer.ts && node extensions/timestamps.ts && node extensions/section-headers.ts && node extensions/ui-render-tweaks.ts && node extensions/usage.ts && node extensions/arnative.ts",
|
|
45
|
-
"themes": "node themes/gen-themes.mjs"
|
|
45
|
+
"themes": "node themes/gen-themes.mjs",
|
|
46
|
+
"release:check": "node scripts/release-check.mjs pre-tag",
|
|
47
|
+
"prepublishOnly": "node scripts/release-check.mjs pre-publish"
|
|
46
48
|
}
|
|
47
49
|
}
|