@bismawy/pi-arnative 0.3.6 → 0.3.7
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 +11 -0
- package/extensions/section-headers.ts +37 -349
- package/extensions/tools.ts +116 -11
- package/extensions/ui-render-tweaks.ts +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.7] - 2026-10-05
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The `powershell` and `ls` tools now render in the arnative box like every other call. pi draws a tool that ships its own `renderCall`/`renderResult` but sets no `renderShell` through its own `contentBox` — a background block with no border. Both tools match that shape (their definitions spread the renderer functions onto the tool object), so they now go through `minimal()` like `bash`/`grep`/`read`. `powershell` is Windows-only: `getPowerShellConfig()` throws elsewhere, and re-registering it would activate a tool the builtin deliberately leaves off (`extensions/tools.ts`).
|
|
7
|
+
- pi-fff's default-mode tools `ffgrep` and `fffind` are boxed too. The box path only recognized the override names `find`/`grep`, which pi-fff uses when it replaces pi's builtins, so a session running it in default mode showed those calls as bare blocks (`extensions/tools.ts`).
|
|
8
|
+
- pi's `[Extension issues]` diagnostic now draws in the arnative box, stripped to two content rows: the title (warning glyph + section name, brackets dropped, `[ctrl+o to expand]` hint) over the extension's source path. pi's extra diagnostic lines are dropped — the expand hint stands in for them (`extensions/section-headers.ts`).
|
|
9
|
+
- The package-update box title lost the double space after its glyph (`extensions/ui-render-tweaks.ts`).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The `section-headers.ts` self-check moved out of the extension into `test/section-headers.test.ts` and runs under `node --test`, so `npm test` lists it as a test instead of a script that prints `OK`. This is a pilot for the other modules; the remaining in-file self-checks follow the same layout once proven (`extensions/section-headers.ts`, `test/section-headers.test.ts`, `package.json`).
|
|
13
|
+
|
|
3
14
|
## [0.3.6] - 2026-10-05
|
|
4
15
|
|
|
5
16
|
### Fixed
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* - Box divider '├────┤'. Active tab content is left-aligned, wrapped, soft.
|
|
12
12
|
* - Hides pi's built-in stacked list in loadedResourcesContainer.
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
16
|
+
import { userInfo } from "node:os";
|
|
17
17
|
import { join } from "node:path";
|
|
18
18
|
import {
|
|
19
19
|
UserMessageComponent,
|
|
@@ -30,12 +30,12 @@ import {
|
|
|
30
30
|
type TuiMouseEvent,
|
|
31
31
|
type TuiMouseEventResult,
|
|
32
32
|
} from "@earendil-works/pi-tui";
|
|
33
|
+
import type { BoxTheme } from "../lib/box.ts";
|
|
33
34
|
import { renderBoxLines } from "../lib/box.ts";
|
|
34
35
|
import { getActiveTheme, setActiveTheme, stripAnsi, themeOf } from "../lib/ansi.ts";
|
|
35
|
-
import {
|
|
36
|
-
import { capitalize, markedLabels, modelDisplayParts } from "../lib/format.ts";
|
|
36
|
+
import { capitalize, expandKeyName, modelDisplayParts } from "../lib/format.ts";
|
|
37
37
|
import { SHORTCUT_NEW_SESSION, SHORTCUT_NEXT_TAB, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
|
|
38
|
-
import { HEADER_PRESET_KEY as HEADER_PRESET_CONFIG_KEY, loadChoice,
|
|
38
|
+
import { HEADER_PRESET_KEY as HEADER_PRESET_CONFIG_KEY, loadChoice, saveChoice } from "../lib/preset-store.ts";
|
|
39
39
|
import { formatTokens, getModelAllTimeUsage } from "../lib/usage-store.ts";
|
|
40
40
|
|
|
41
41
|
export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "Shortcut";
|
|
@@ -52,6 +52,9 @@ export const SECTION_ICONS: Record<TabKey, string> = {
|
|
|
52
52
|
Shortcut: "\uf11c", // nf-fa-keyboard_o
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
// nf-cod-warning, width 1 — prefixes a pi diagnostic block's compact header.
|
|
56
|
+
const DIAGNOSTIC_ICON = "\uea6c";
|
|
57
|
+
|
|
55
58
|
export type Themeish = { fg?(color: string, text: string): string; bg?(color: string, text: string): string } | null;
|
|
56
59
|
|
|
57
60
|
const MODEL_SNAPSHOT_KEY = Symbol.for("pi-arnative.currentModel");
|
|
@@ -644,6 +647,30 @@ export class ArnativeHeader implements Component {
|
|
|
644
647
|
}
|
|
645
648
|
}
|
|
646
649
|
|
|
650
|
+
// Arnative box for pi's `[Extension issues]` diagnostic: exactly two rows, the title
|
|
651
|
+
// (warning icon + section name, brackets dropped, + expand hint) over the extension's
|
|
652
|
+
// source path. pi's extra diagnostic lines are dropped — the expand hint stands in for
|
|
653
|
+
// them. Uses only `warning` + `dim`, both defined by every pi theme, so it draws the
|
|
654
|
+
// same under any theme; `rows` is pi's own render output, row 0 being the title.
|
|
655
|
+
export function extensionIssuesLines(th: BoxTheme | null, width: number, rows: string[], hint: string): string[] {
|
|
656
|
+
const paint = (color: string, text: string) => {
|
|
657
|
+
try {
|
|
658
|
+
return th?.fg ? th.fg(color, text) : text;
|
|
659
|
+
} catch {
|
|
660
|
+
return text;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
// Rows are plain text: ANSI inside would break the width math of renderBoxLines.
|
|
664
|
+
const title = stripAnsi(rows[0] ?? "").trim().replace(/^\[(.+)\]$/, "$1");
|
|
665
|
+
const hintPlain = stripAnsi(hint).trim();
|
|
666
|
+
const detail = stripAnsi(rows[1] ?? "").trim();
|
|
667
|
+
const header = `${paint("warning", `${DIAGNOSTIC_ICON} ${title}`)}${hintPlain ? ` ${paint("dim", hintPlain)}` : ""}`;
|
|
668
|
+
return renderBoxLines(th, width, [header, detail], undefined, "warning");
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Hooks the shared `app.tools.expand` key: `ctrl+o to expand` -> `[ctrl+o to expand]`.
|
|
672
|
+
const wrapHint = (key: string): string => `[${key} to expand]`;
|
|
673
|
+
|
|
647
674
|
// Intercept loadedResourcesContainer: capture data for the tabs and hide pi's own display
|
|
648
675
|
const SECTION_HEADER_KEY = Symbol.for("pi-arnative.sectionHeadersRewritten");
|
|
649
676
|
if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[SECTION_HEADER_KEY]) {
|
|
@@ -687,7 +714,11 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
|
|
|
687
714
|
const text = themedTextOf(child);
|
|
688
715
|
if (text.includes("[Extension issues]") && typeof child.render === "function") {
|
|
689
716
|
const origRender = child.render.bind(child);
|
|
690
|
-
|
|
717
|
+
// Shortcut hint only when the shortcut registry is reachable; otherwise just the
|
|
718
|
+
// two-line compact block.
|
|
719
|
+
const rawHint = wrapHint(expandKeyName());
|
|
720
|
+
child.render = (w: number) =>
|
|
721
|
+
extensionIssuesLines(getActiveTheme<BoxTheme>(), w, origRender(w), rawHint);
|
|
691
722
|
return origAddChild.call(this, child);
|
|
692
723
|
}
|
|
693
724
|
// Any other element inside loadedResourcesContainer (e.g. Spacer) is hidden too
|
|
@@ -748,346 +779,3 @@ export default function (pi: ExtensionAPI) {
|
|
|
748
779
|
}
|
|
749
780
|
});
|
|
750
781
|
}
|
|
751
|
-
|
|
752
|
-
// Self-check: `node extensions/section-headers.ts`
|
|
753
|
-
if (isMain(import.meta.url)) {
|
|
754
|
-
|
|
755
|
-
assert(sectionNameOf("\x1b[33m[Skills]\x1b[39m\n a, b") === "Skills", "header [Skills] detected");
|
|
756
|
-
assert(sectionNameOf("pi v0.87.1") === null, "non-section text is not detected");
|
|
757
|
-
|
|
758
|
-
// Header presets: Arnative (Full) is the default; Pi (system) hands the header slot back to pi.
|
|
759
|
-
assert(DEFAULT_HEADER_PRESET === "Arnative (Full)" && HEADER_PRESETS.length === 2, "Arnative (Full) is the default header preset");
|
|
760
|
-
assert(HEADER_PRESETS.join(" | ") === "Pi (system) | Arnative (Full)", "built-in preset is listed first");
|
|
761
|
-
assert(markedLabels(HEADER_PRESETS, "Arnative (Full)").join(" | ") === " Pi (system) | ● Arnative (Full)", "preset labels mark the active one");
|
|
762
|
-
// Persisting a preset writes a config file: point it at a fixture so the real agent dir stays clean.
|
|
763
|
-
const presetFixture = mkdtempSync(join(tmpdir(), "arnative-hdr-"));
|
|
764
|
-
const prevAgentDir = process.env.PI_CODING_AGENT_DIR;
|
|
765
|
-
process.env.PI_CODING_AGENT_DIR = presetFixture;
|
|
766
|
-
resetPresetCache();
|
|
767
|
-
let installed: unknown = "untouched";
|
|
768
|
-
const presetCtx: any = { ui: { setHeader: (factory: unknown) => { installed = factory; } } };
|
|
769
|
-
setHeaderPreset("Pi (system)", presetCtx);
|
|
770
|
-
assert(installed === undefined && activeHeaderPreset() === "Pi (system)", "Pi (system) clears the custom header");
|
|
771
|
-
setHeaderPreset("Arnative (Full)", presetCtx);
|
|
772
|
-
assert(typeof installed === "function" && activeHeaderPreset() === "Arnative (Full)", "Arnative (Full) installs the header factory");
|
|
773
|
-
// Persist a NON-default value: asserting the default survives proves nothing.
|
|
774
|
-
setHeaderPreset("Pi (system)", presetCtx);
|
|
775
|
-
assert(loadChoice(HEADER_PRESET_CONFIG_KEY, HEADER_PRESETS) === "Pi (system)", "the header preset is persisted to disk");
|
|
776
|
-
delete (globalThis as Record<symbol, unknown>)[HEADER_PRESET_KEY];
|
|
777
|
-
resetPresetCache();
|
|
778
|
-
assert(activeHeaderPreset() === "Pi (system)", "a restart reads the non-default preset back from disk");
|
|
779
|
-
rmSync(presetFixture, { recursive: true, force: true });
|
|
780
|
-
if (prevAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
|
781
|
-
else process.env.PI_CODING_AGENT_DIR = prevAgentDir;
|
|
782
|
-
resetPresetCache();
|
|
783
|
-
|
|
784
|
-
// Parsing & wrapping
|
|
785
|
-
assert(extractItemsFromBody(" a, b, c").length === 3, "extractItemsFromBody splits 3 comma items");
|
|
786
|
-
assert(extractItemsFromBody(" x\n y\n z").length === 3, "extractItemsFromBody returns 3 lines");
|
|
787
|
-
|
|
788
|
-
// Directory path wrapping: never truncated, always the full path
|
|
789
|
-
const wp = wrapPath("/run/media/bisma/DATA/Pi/pi-arnative", 20);
|
|
790
|
-
assert(wp.join("") === "/run/media/bisma/DATA/Pi/pi-arnative", "wrapPath drops no segment");
|
|
791
|
-
assert(wp.every((l) => visibleWidth(l) <= 20), "wrapPath respects the width");
|
|
792
|
-
assert(wrapPath("D:\\Pi\\pi-arnative", 80).join("") === "D:/Pi/pi-arnative", "wrapPath normalizes backslashes");
|
|
793
|
-
assert(wrapPath("/a/very-long-segment-name", 6).every((l) => visibleWidth(l) <= 6), "long segment is sliced");
|
|
794
|
-
|
|
795
|
-
const wrapTest = wrapCommaItems(["item1", "item2", "item3", "item4"], 15);
|
|
796
|
-
assert(wrapTest.length >= 2, "wrapCommaItems wraps into lines");
|
|
797
|
-
assert(visibleWidth(wrapTest[0]!) <= 15, "wrapped line width within the limit");
|
|
798
|
-
|
|
799
|
-
const cLine = centerLine("test", 20);
|
|
800
|
-
assert(visibleWidth(cLine) === 20, "centerLine is exactly 20 columns");
|
|
801
|
-
assert(cLine.startsWith("│") && cLine.endsWith("│"), "centerLine starts and ends with │");
|
|
802
|
-
|
|
803
|
-
// Model name formatting
|
|
804
|
-
assert(
|
|
805
|
-
formatModelDisplayName({ name: "Gemini 3.8 Flash", provider: "antigravity" }) ===
|
|
806
|
-
"Gemini 3.8 Flash (Antigravity)",
|
|
807
|
-
"formatModelDisplayName with name and provider",
|
|
808
|
-
);
|
|
809
|
-
assert(
|
|
810
|
-
formatModelDisplayName({ id: "deepseek-v4.1-flash", provider: "yarz" }) ===
|
|
811
|
-
"Deepseek V4.1 Flash (Yarz)",
|
|
812
|
-
"formatModelDisplayName converts id to titlecase",
|
|
813
|
-
);
|
|
814
|
-
|
|
815
|
-
// Package & version resolver
|
|
816
|
-
assert(packageNameOf("@bismawy/pi-agentrouter") === "@bismawy/pi-agentrouter", "plain npm label");
|
|
817
|
-
assert(packageNameOf("footer.ts") === "footer.ts", "local label");
|
|
818
|
-
|
|
819
|
-
const gitTmp = mkdtempSync(join(tmpdir(), "pi-ver-"));
|
|
820
|
-
mkdirSync(join(gitTmp, "git", "github.com", "test/arnative"), { recursive: true });
|
|
821
|
-
writeFileSync(join(gitTmp, "git", "github.com", "test/arnative", "package.json"), JSON.stringify({ version: "9.9.9" }));
|
|
822
|
-
assert(installedVersion("test/arnative", join(gitTmp, "npm"), join(gitTmp, "git")) === "9.9.9", "version from the git clone");
|
|
823
|
-
rmSync(gitTmp, { recursive: true, force: true });
|
|
824
|
-
|
|
825
|
-
// ArnativeHeader component & mouse tab click
|
|
826
|
-
tabStore.set("Context", ["AGENTS.md"]);
|
|
827
|
-
tabStore.set("Skills", ["agents-sdk", "cloudflare"]);
|
|
828
|
-
tabStore.set("Extensions", ["@bismawy/pi-agentrouter@1.6.1", "footer.ts"]);
|
|
829
|
-
|
|
830
|
-
let renderRequested = false;
|
|
831
|
-
const fakeTui: any = {
|
|
832
|
-
requestRender: () => {
|
|
833
|
-
renderRequested = true;
|
|
834
|
-
},
|
|
835
|
-
};
|
|
836
|
-
const fakeCtx: any = {
|
|
837
|
-
model: { name: "Gemini 3.8 Flash", provider: "antigravity" },
|
|
838
|
-
thinkingLevel: "low",
|
|
839
|
-
cwd: "/run/media/bisma/DATA/Pi/pi-arnative",
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
// Only "dim" is colored, so the box line colors can be asserted.
|
|
843
|
-
const ansiTheme = { fg: (c: string, t: string) => (c === "dim" ? `\x1b[2m${t}\x1b[22m` : t) };
|
|
844
|
-
|
|
845
|
-
// Counts come from pi's loaded-resource listing. With `quietStartup: true | "header"`
|
|
846
|
-
// pi skips that listing, so the counts are unknown — shown as "—", never a misleading 0.
|
|
847
|
-
resourcesListingShown(false);
|
|
848
|
-
tabStore.set("Context", []);
|
|
849
|
-
tabStore.set("Skills", []);
|
|
850
|
-
tabStore.set("Extensions", []);
|
|
851
|
-
const hdrQuiet = new ArnativeHeader(fakeTui, ansiTheme, fakeCtx);
|
|
852
|
-
assert(hdrQuiet.getTabsData(120).map((t) => t.label).join("|").includes("Skills [—]"), "hidden listing: counts render as —, not 0");
|
|
853
|
-
hdrQuiet.activeTab = "Skills";
|
|
854
|
-
assert(
|
|
855
|
-
hdrQuiet.render(120).some((l) => stripAnsi(l).includes("(not tracked")),
|
|
856
|
-
"hidden listing: empty tab explains itself instead of a bare (empty)",
|
|
857
|
-
);
|
|
858
|
-
resourcesListingShown(true);
|
|
859
|
-
assert(new ArnativeHeader(fakeTui, ansiTheme, fakeCtx).getTabsData(120).some((t) => t.label === "Skills [0]"), "shown listing: 0 is a real count again");
|
|
860
|
-
tabStore.set("Context", ["AGENTS.md"]);
|
|
861
|
-
tabStore.set("Skills", ["agents-sdk", "cloudflare"]);
|
|
862
|
-
tabStore.set("Extensions", ["@bismawy/pi-agentrouter@1.6.1", "footer.ts"]);
|
|
863
|
-
const hdr = new ArnativeHeader(fakeTui, ansiTheme, fakeCtx);
|
|
864
|
-
assert(hdr.activeTab === "Directory", "default tab is Directory");
|
|
865
|
-
|
|
866
|
-
// Default view: the working directory path, shown on open
|
|
867
|
-
const linesDefault = hdr.render(100);
|
|
868
|
-
assert(stripAnsi(linesDefault[5]!).includes("/run/media/bisma/DATA/Pi/pi-arnative"), "directory path shown as soon as the first tab opens");
|
|
869
|
-
|
|
870
|
-
hdr.activeTab = "Model";
|
|
871
|
-
const lines100 = hdr.render(100);
|
|
872
|
-
assert(lines100.length === 7, "render 100 = border + 5 rows + border (left: mark + gap + pi version; info: brand line + tab box + data)");
|
|
873
|
-
assert(lines100.every((l) => visibleWidth(l) === 100), "every line at width 100 is exactly 100 columns");
|
|
874
|
-
assert(lines100[0]!.includes("╭") && lines100[0]!.includes("╮"), "round top border");
|
|
875
|
-
assert(lines100[6]!.includes("╰") && lines100[6]!.includes("╯"), "round bottom border");
|
|
876
|
-
// The column divider joins the frame: full-height split, no gap at the borders.
|
|
877
|
-
const jx = stripAnsi(lines100[0]!).indexOf("┬");
|
|
878
|
-
assert(jx > 0 && stripAnsi(lines100[6]!).indexOf("┴") === jx, "┬/┴ junctions sit on the border at the divider column");
|
|
879
|
-
assert(
|
|
880
|
-
lines100.slice(1, 6).every((l) => stripAnsi(l)[jx] === "│"),
|
|
881
|
-
"the divider sits in the same column on every row",
|
|
882
|
-
);
|
|
883
|
-
const logoPlain = buildLogoLines(null);
|
|
884
|
-
assert(logoPlain.length === 4 && visibleWidth(logoPlain[0]!) === 8, "pi block mark intact (4 lines x 8 columns)");
|
|
885
|
-
// P (columns 0-2) accent, i (the right bar) soft — the old wordmark's two-tone.
|
|
886
|
-
const logoTagged = buildLogoLines({
|
|
887
|
-
fg: (c: string, t: string) => (c === "accent" ? `<A>${t}</A>` : c === "accentSoft" ? `<T>${t}</T>` : t),
|
|
888
|
-
} as any);
|
|
889
|
-
assert(logoTagged[0] === "<A>██████</A> ", "mark top row: P in the theme accent");
|
|
890
|
-
assert(logoTagged[2] === "<A>████</A> <T>██</T>", "mark lower rows: P accent + i (right bar) soft");
|
|
891
|
-
assert(
|
|
892
|
-
lines100.slice(1, 5).every((l, i) => stripAnsi(l).slice(2, 10) === logoPlain[i]),
|
|
893
|
-
"pi block mark in the left column, 4 lines",
|
|
894
|
-
);
|
|
895
|
-
assert(
|
|
896
|
-
/Welcome back, \S/.test(stripAnsi(lines100[1]!)) && !stripAnsi(lines100[1]!).includes("pi v"),
|
|
897
|
-
"line 1: greeting uses the device username",
|
|
898
|
-
);
|
|
899
|
-
// Left column: the pi version sits directly under the mark (no gap).
|
|
900
|
-
assert(stripAnsi(lines100[5]!).slice(2, 12).includes("pi v"), "pi version sits under the mark in the left column");
|
|
901
|
-
// Info column: brand + greeting, then the tab box straight away (no blank line).
|
|
902
|
-
assert(/Arnative v\d+\.\d+\.\d+ · Welcome back, \S/.test(stripAnsi(lines100[1]!)), "info line 1: Arnative version · Welcome back, user");
|
|
903
|
-
assert(!stripAnsi(lines100[2]!).includes("Menu"), "tab box border carries no label");
|
|
904
|
-
// Tab box separators join its borders: ┬ above and ┴ below each "│". Compare by display
|
|
905
|
-
// column, not string index: the tab icons are astral (2 code units, 1 column).
|
|
906
|
-
const atColumn = (line: string, col: number): string | undefined => {
|
|
907
|
-
let c = 0;
|
|
908
|
-
for (const ch of stripAnsi(line)) {
|
|
909
|
-
const w = visibleWidth(ch);
|
|
910
|
-
if (col >= c && col < c + w) return ch;
|
|
911
|
-
c += w;
|
|
912
|
-
}
|
|
913
|
-
return undefined;
|
|
914
|
-
};
|
|
915
|
-
const tabSepCols = [...stripAnsi(lines100[2]!).matchAll(/┬/g)].map((m) => visibleWidth(stripAnsi(lines100[2]!).slice(0, m.index)));
|
|
916
|
-
assert(tabSepCols.length >= 2, "tab box top border has ┬ at each column divider");
|
|
917
|
-
assert(
|
|
918
|
-
tabSepCols.every((c) => atColumn(lines100[3]!, c) === "│" && atColumn(lines100[4]!, c) === "┴"),
|
|
919
|
-
"each tab divider is full height (┬ / │ / ┴ in the same column)",
|
|
920
|
-
);
|
|
921
|
-
assert(!stripAnsi(lines100[1]!).includes("Interrupt"), "shortcut legend no longer on the top line");
|
|
922
|
-
// Both brand names use the theme accent, not only "pi".
|
|
923
|
-
const accentTheme = {
|
|
924
|
-
fg: (c: string, t: string) => (c === "accent" ? `<A>${t}</A>` : c === "dim" ? `<D>${t}</D>` : t),
|
|
925
|
-
};
|
|
926
|
-
const accentHeader = new ArnativeHeader(fakeTui, accentTheme as any, fakeCtx);
|
|
927
|
-
// The marker theme returns literal "<A>" text (not ANSI), so the measured width is
|
|
928
|
-
// inflated — render wide enough that the brand/greeting line is not truncated.
|
|
929
|
-
const accentLines = accentHeader.render(140);
|
|
930
|
-
assert(accentLines[5]!.includes("<A>\x1b[1mpi"), "the pi name uses the theme accent");
|
|
931
|
-
assert(accentLines[1]!.includes("<A>\x1b[1mArnative"), "the Arnative name uses the same theme accent");
|
|
932
|
-
const greetLine = accentHeader.render(140)[1]!;
|
|
933
|
-
assert(greetLine.includes("<D>Welcome back,"), "greeting text uses the dim color");
|
|
934
|
-
assert(greetLine.includes(`<A>\x1b[1m${capitalize(deviceUser())}`), "device username uses the theme accent, first letter capitalized");
|
|
935
|
-
assert(stripAnsi(lines100[3]!).includes("Model [Gemini 3.8 Flash]"), "Model tab in the menu (Model [name])");
|
|
936
|
-
// Directory tab: name only in the menu, whole path in the content line below
|
|
937
|
-
hdr.activeTab = "Directory";
|
|
938
|
-
hdr.render(100);
|
|
939
|
-
assert(stripAnsi(lines100[3]!).includes("Directory"), "Directory tab shown in the menu");
|
|
940
|
-
assert(!stripAnsi(lines100[3]!).includes("Directory: /"), "Directory menu entry has no path (detail only on click)");
|
|
941
|
-
assert(stripAnsi(lines100[3]!).indexOf("Directory") < stripAnsi(lines100[3]!).indexOf("Model ["), "Directory tab is leftmost");
|
|
942
|
-
hdr.activeTab = "Model";
|
|
943
|
-
hdr.render(100);
|
|
944
|
-
assert(stripAnsi(lines100[5]!).includes("Gemini 3.8 Flash (Antigravity)"), "active Model tab data on the last info line");
|
|
945
|
-
// Directory tab content: full path, wrapped, nothing lost
|
|
946
|
-
hdr.activeTab = "Directory";
|
|
947
|
-
const linesDir = hdr.render(100);
|
|
948
|
-
const dirContent = stripAnsi(linesDir[5]!);
|
|
949
|
-
assert(dirContent.includes("/run/media/bisma"), "full Directory tab path (start) shown");
|
|
950
|
-
assert(dirContent.includes("pi-arnative"), "full Directory tab path (end) shown — no truncation");
|
|
951
|
-
hdr.activeTab = "Model";
|
|
952
|
-
hdr.render(100);
|
|
953
|
-
// Tab box borders are a single fully dimmed block (the "─"/"┬"/"┴" runs are colored too).
|
|
954
|
-
assert((lines100[4]!.match(/\x1b\[/g) ?? []).length === 8, "bottom box line (row 4) = one fully dimmed block");
|
|
955
|
-
assert(
|
|
956
|
-
lines100[2]!.includes("\x1b[2m╭─") && lines100[2]!.includes("┬") && lines100[2]!.includes("╮\x1b[22m"),
|
|
957
|
-
"tab box top border = one dim block carrying the ┬ junctions",
|
|
958
|
-
);
|
|
959
|
-
|
|
960
|
-
// Tab regions: Model, Context, Skills, Extensions
|
|
961
|
-
const tabY = (hdr as any).renderedTabLineY;
|
|
962
|
-
assert(tabY > 0, "tab line Y position recorded");
|
|
963
|
-
const tabRegions = (hdr as any).renderedTabRegions as Array<{ key: TabKey; startX: number; endX: number }>;
|
|
964
|
-
assert(tabRegions.length >= 3, "tab regions filled (right tabs may be cut at 100 columns)");
|
|
965
|
-
assert(tabRegions.some((r) => r.key === "Directory"), "Directory tab present in the menu (width 100)");
|
|
966
|
-
|
|
967
|
-
// Click the Extensions tab (when truncated at 100 columns, use the rightmost tab)
|
|
968
|
-
const extRegion = tabRegions.find((r) => r.key === "Extensions") ?? tabRegions[tabRegions.length - 1]!;
|
|
969
|
-
const clickExt = hdr.handleMouse({
|
|
970
|
-
type: "click",
|
|
971
|
-
button: "left",
|
|
972
|
-
x: Math.floor((extRegion.startX + extRegion.endX) / 2),
|
|
973
|
-
y: tabY,
|
|
974
|
-
} as any);
|
|
975
|
-
assert(clickExt?.handled === true, "mouse click on the tab is handled");
|
|
976
|
-
assert(hdr.activeTab === extRegion.key, "activeTab switches to the clicked tab");
|
|
977
|
-
assert(renderRequested === true, "requestRender called when the tab switches");
|
|
978
|
-
|
|
979
|
-
const linesExt = hdr.render(100);
|
|
980
|
-
assert(stripAnsi(linesExt[5]!).includes(extRegion.key === "Extensions" ? "@bismawy/pi-agentrouter@1.6.1" : ""), "active tab data on the last info line");
|
|
981
|
-
|
|
982
|
-
// Click the Skills tab (measured at a width where every tab fits)
|
|
983
|
-
const wide = hdr.render(140);
|
|
984
|
-
const wideRegions = (hdr as any).renderedTabRegions as Array<{ key: TabKey; startX: number; endX: number }>;
|
|
985
|
-
const skillsRegion = wideRegions.find((r) => r.key === "Skills") ?? wideRegions[wideRegions.length - 1]!;
|
|
986
|
-
hdr.activeTab = "Model";
|
|
987
|
-
hdr.render(140);
|
|
988
|
-
const skillsY = (hdr as any).renderedTabLineY;
|
|
989
|
-
hdr.handleMouse({
|
|
990
|
-
type: "click",
|
|
991
|
-
button: "left",
|
|
992
|
-
x: Math.floor((skillsRegion.startX + skillsRegion.endX) / 2),
|
|
993
|
-
y: skillsY,
|
|
994
|
-
} as any);
|
|
995
|
-
assert(hdr.activeTab === skillsRegion.key, `activeTab switches to ${skillsRegion.key} after the click`);
|
|
996
|
-
|
|
997
|
-
const linesSkills = hdr.render(140);
|
|
998
|
-
assert(wide.every((l) => visibleWidth(l) === 140), "lines at 140 columns stay 140");
|
|
999
|
-
assert(wideRegions.length === 6, "width 140: all 6 tabs shown (Shortcut included)");
|
|
1000
|
-
if (skillsRegion.key === "Skills") {
|
|
1001
|
-
assert(linesSkills[5]!.includes("agents-sdk, cloudflare"), "Skills tab data on the last info line");
|
|
1002
|
-
assert(!linesSkills[5]!.includes("Gemini 3.8 Flash (Antigravity)"), "Model data not shown in the Skills tab content");
|
|
1003
|
-
assert(linesSkills[3]!.includes("Model [Gemini 3.8 Flash]"), "tab menu still shown while another tab is active");
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// The Y of a tab must point at the tab menu line, not the line above
|
|
1007
|
-
const rowsPlain = lines100.map((l) => stripAnsi(l));
|
|
1008
|
-
assert(rowsPlain[tabY]!.includes("Model ["), "renderedTabLineY points at the tab menu line");
|
|
1009
|
-
assert(tabRegions.every((r) => rowsPlain[tabY]!.slice(r.startX, r.endX + 1).includes(r.key === "Model" ? "Model [" : r.key)), "tab regions align with the text on that line");
|
|
1010
|
-
|
|
1011
|
-
// Shortcut tab: the key cheatsheet lives in the tab content, not in a header line
|
|
1012
|
-
hdr.activeTab = "Shortcut";
|
|
1013
|
-
const linesShortcut = hdr.render(170);
|
|
1014
|
-
assert(
|
|
1015
|
-
((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
1016
|
-
"width 170: Shortcut tab shown (not dropped)",
|
|
1017
|
-
);
|
|
1018
|
-
const scContent = stripAnsi(linesShortcut.slice(5).join(" "));
|
|
1019
|
-
assert(scContent.includes("[Esc] Interrupt"), "Shortcut tab: legend moved into the content");
|
|
1020
|
-
assert(
|
|
1021
|
-
scContent.includes(`[${SHORTCUT_RELOAD.label}] Reload`),
|
|
1022
|
-
`Shortcut tab: reload shortcut present in the content (${SHORTCUT_RELOAD.label})`,
|
|
1023
|
-
);
|
|
1024
|
-
assert(stripAnsi(linesShortcut[1]!).includes("Arnative v"), "the Arnative brand stays on line 1 while the Shortcut tab is active");
|
|
1025
|
-
hdr.activeTab = "Model";
|
|
1026
|
-
hdr.render(140);
|
|
1027
|
-
|
|
1028
|
-
// Narrow screen: brand lines + tabs + data stay intact, rightmost tabs are dropped
|
|
1029
|
-
const narrow = hdr.render(60);
|
|
1030
|
-
assert(narrow.every((l) => visibleWidth(l) === 60), "lines at 60 columns stay 60");
|
|
1031
|
-
assert(stripAnsi(narrow[1]!).includes("Welcome back,"), "greeting stays intact at 60 columns");
|
|
1032
|
-
assert(stripAnsi(narrow[1]!).includes("Arnative v"), "Arnative brand intact at 60 columns");
|
|
1033
|
-
assert(stripAnsi(narrow[1]!).includes("Interrupt") === false, "legend does not come back to the top line");
|
|
1034
|
-
assert(
|
|
1035
|
-
!((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
1036
|
-
"rightmost tab dropped at 60 columns",
|
|
1037
|
-
);
|
|
1038
|
-
assert(stripAnsi(narrow[3]!).includes("Model ["), "Model tab still shown at 60 columns");
|
|
1039
|
-
|
|
1040
|
-
// Intercept addChild: capture the data and suppress the child
|
|
1041
|
-
const proto = Object.getPrototypeOf(UserMessageComponent.prototype) as { addChild?: unknown };
|
|
1042
|
-
const container: any = { children: [] as any[], addChild: proto.addChild as (c: any) => unknown };
|
|
1043
|
-
const fakeSection = {
|
|
1044
|
-
getCollapsedText: () => "[Skills]\n new-skill-1, new-skill-2",
|
|
1045
|
-
getExpandedText: () => "[Skills]\n new-skill-1, new-skill-2",
|
|
1046
|
-
setText: () => {},
|
|
1047
|
-
render: (_w: number) => ["should be hidden"],
|
|
1048
|
-
};
|
|
1049
|
-
container.addChild(fakeSection);
|
|
1050
|
-
assert(fakeSection.render(80).length === 0, "child of loadedResourcesContainer suppressed (render = [])");
|
|
1051
|
-
assert(tabStore.get("Skills")?.includes("new-skill-1") === true, "new data lands in tabStore");
|
|
1052
|
-
|
|
1053
|
-
// pi 0.99 ExpandableText shape: `build` callback + `state` object, no getters.
|
|
1054
|
-
const modernSection = {
|
|
1055
|
-
state: { expanded: false },
|
|
1056
|
-
build: () => "[Context]\n AGENTS.md, MEMORY.md",
|
|
1057
|
-
setText: () => {},
|
|
1058
|
-
render: (_w: number) => ["should be hidden"],
|
|
1059
|
-
};
|
|
1060
|
-
container.addChild(modernSection);
|
|
1061
|
-
assert(modernSection.render(80).length === 0, "pi 0.99 section suppressed (build + state shape)");
|
|
1062
|
-
assert(tabStore.get("Context")?.includes("MEMORY.md") === true, "pi 0.99 section data lands in tabStore");
|
|
1063
|
-
|
|
1064
|
-
// Startup help block is not a resource section and must survive untouched.
|
|
1065
|
-
const helpBlock = { build: () => "\u2580\u2580\u2588 v0.99.1\n\u2588\u2580 \u2588 escape interrupt", state: { expanded: false }, render: () => ["help"] };
|
|
1066
|
-
container.addChild(helpBlock);
|
|
1067
|
-
assert(sectionBodyOf(helpBlock) === "\u2580\u2580\u2588 v0.99.1\n\u2588\u2580 \u2588 escape interrupt", "sectionBodyOf reads the build shape");
|
|
1068
|
-
assert(sectionNameOf(sectionBodyOf(helpBlock)!) === null, "startup help block is not a resource section");
|
|
1069
|
-
|
|
1070
|
-
// pi's "[Extension issues]" diagnostic renders while the installing module's theme is
|
|
1071
|
-
// still null (a `/reload` copy never saw session_start). The box must draw uncolored,
|
|
1072
|
-
// not deref null — that crash was reported in the wild.
|
|
1073
|
-
setActiveTheme(null);
|
|
1074
|
-
const issuesChild = {
|
|
1075
|
-
build: () => "[Extension issues]\n extension \"x\" failed",
|
|
1076
|
-
render: (_w: number) => ["[Extension issues]", ' extension "x" failed'],
|
|
1077
|
-
};
|
|
1078
|
-
container.addChild(issuesChild);
|
|
1079
|
-
const issuesLines = issuesChild.render(80);
|
|
1080
|
-
assert(issuesLines[0]!.startsWith("\u256d") && issuesLines.at(-1)!.startsWith("\u2570"), "extension-issues box renders without a theme");
|
|
1081
|
-
|
|
1082
|
-
// The theme proxy is globalThis-backed, so the copy that owns the addChild patch still
|
|
1083
|
-
// paints once the other copy's session_start has stored the theme.
|
|
1084
|
-
setActiveTheme({ fg: (c: string, t: string) => `<${c}>${t}</${c}>` });
|
|
1085
|
-
const coloredChild = {
|
|
1086
|
-
build: () => "[Extension issues]\n extension \"x\" failed",
|
|
1087
|
-
render: (_w: number) => ["[Extension issues]", ' extension "x" failed'],
|
|
1088
|
-
};
|
|
1089
|
-
container.addChild(coloredChild);
|
|
1090
|
-
assert(coloredChild.render(80)[0]!.includes("<warning>"), "shared theme proxy colors the box even in the patched copy");
|
|
1091
|
-
|
|
1092
|
-
console.log("section-headers.ts self-check OK");
|
|
1093
|
-
}
|
package/extensions/tools.ts
CHANGED
|
@@ -24,6 +24,10 @@ import {
|
|
|
24
24
|
createEditTool,
|
|
25
25
|
createFindTool,
|
|
26
26
|
createGrepTool,
|
|
27
|
+
createLsTool,
|
|
28
|
+
createLsToolDefinition,
|
|
29
|
+
createPowerShellTool,
|
|
30
|
+
createPowerShellToolDefinition,
|
|
27
31
|
createReadTool,
|
|
28
32
|
createWriteTool,
|
|
29
33
|
getMarkdownTheme,
|
|
@@ -290,11 +294,23 @@ for (const n of BOXED_TOOLS.keys()) OWN_BOX.add(n);
|
|
|
290
294
|
|
|
291
295
|
const MCP_TOOL = (name: string): boolean => name === "mcp" || name === "mcpScript" || name.startsWith("mcp__");
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
297
|
+
// pi-fff / pi core/plugin tools whose renderers we rebuild in our own box when their
|
|
298
|
+
// renderer carries no explicit `renderShell` marker (our `minimal()` sets "self").
|
|
299
|
+
// pi-fff: `ffgrep`/`fffind` in the default mode, `find`/`grep` once it "overrides"
|
|
300
|
+
// pi's builtins; matching either name covers both of its modes.
|
|
301
|
+
// pi core: `powershell` and `ls` — pi's runtime keeps `createAllToolDefinitions`, which
|
|
302
|
+
// spreads `renderCall`/`renderResult` onto the tool OBJECT; `wrapToolDefinition`
|
|
303
|
+
// (used by `createPowerShellTool`/`createLsTool`) drops them instead.
|
|
304
|
+
const boxTheirRenderers = (self: any): boolean =>
|
|
305
|
+
self.toolDefinition?.renderShell !== "self" &&
|
|
306
|
+
(self.toolName === "ls" ||
|
|
307
|
+
self.toolName === "powershell" ||
|
|
308
|
+
self.toolName === "ffgrep" ||
|
|
309
|
+
self.toolName === "fffind" ||
|
|
310
|
+
self.toolName === "find" ||
|
|
311
|
+
self.toolName === "grep" ||
|
|
312
|
+
BOXED_TOOLS.has(self.toolName) ||
|
|
313
|
+
MCP_TOOL(self.toolName));
|
|
298
314
|
|
|
299
315
|
// The MCP box title names the invoked tool (full args when expanded).
|
|
300
316
|
const mcpInfo = (name: string, args: any, th: Theme): string => {
|
|
@@ -352,7 +368,11 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
352
368
|
const origCall = proto.getCallRenderer;
|
|
353
369
|
const origResult = proto.getResultRenderer;
|
|
354
370
|
const origShell = proto.getRenderShell;
|
|
355
|
-
const hasOwnRenderer = (self: any): boolean =>
|
|
371
|
+
const hasOwnRenderer = (self: any): boolean =>
|
|
372
|
+
!boxTheirRenderers(self) && Boolean(origCall.call(self) || origResult.call(self));
|
|
373
|
+
// pi-fff splits title (renderCall) and result (renderResult) — both merge into one box.
|
|
374
|
+
// Its two naming modes are covered: `find`/`grep` (override) and `ffgrep`/`fffind` (default).
|
|
375
|
+
const FFF_TOOLS = ["find", "grep", "ffgrep", "fffind"];
|
|
356
376
|
|
|
357
377
|
// A tool call with no registered definition (e.g. an unknown tool name that
|
|
358
378
|
// errored with "Tool X not found") makes hasRendererDefinition() false, so the
|
|
@@ -366,13 +386,13 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
366
386
|
};
|
|
367
387
|
|
|
368
388
|
proto.getRenderShell = function (): string {
|
|
369
|
-
if (
|
|
389
|
+
if (boxTheirRenderers(this)) return "self";
|
|
370
390
|
return hasOwnRenderer(this) ? origShell.call(this) : "self";
|
|
371
391
|
};
|
|
372
392
|
|
|
373
393
|
proto.getCallRenderer = function () {
|
|
374
394
|
const name: string = this.toolName;
|
|
375
|
-
const mine =
|
|
395
|
+
const mine = boxTheirRenderers(this);
|
|
376
396
|
const own = origCall.call(this);
|
|
377
397
|
// Chrome DevTools' own renderCall is a bare line; ours replaces it (icon + name).
|
|
378
398
|
if (own && !mine && !CDP_NAME.has(name)) return own;
|
|
@@ -394,9 +414,9 @@ if (ToolExecutionComponent?.prototype?.render && !(globalThis as Record<symbol,
|
|
|
394
414
|
const name: string = this.toolName;
|
|
395
415
|
const own = origResult.call(this);
|
|
396
416
|
// pi-fff splits title (renderCall) and result (renderResult); both go in one box.
|
|
397
|
-
const fff =
|
|
417
|
+
const fff = FFF_TOOLS.includes(name) && this.toolDefinition?.renderShell !== "self";
|
|
398
418
|
// MCP: their result renderer is deliberately skipped (unbounded error dump).
|
|
399
|
-
if (own && !
|
|
419
|
+
if (own && !boxTheirRenderers(this)) return own;
|
|
400
420
|
const boxed = BOXED_TOOLS.has(name);
|
|
401
421
|
// Chrome DevTools are in BOXED_TOOLS for the title/icon only: their renderResult
|
|
402
422
|
// (raw text, empty when collapsed) would drop our title row, so it is ignored.
|
|
@@ -660,6 +680,31 @@ function arnativeTools(pi: ExtensionAPI) {
|
|
|
660
680
|
|
|
661
681
|
// Gap is only stripped for our own boxes (see OWN_BOX above).
|
|
662
682
|
|
|
683
|
+
// powershell: pi core registers it as a separate builtin (only on Windows); without
|
|
684
|
+
// our own registration it keeps pi's "default" shell and renders as a bg block, not a
|
|
685
|
+
// box (the same reason `bash` is re-registered below). Windows only: on any other
|
|
686
|
+
// platform `getPowerShellConfig()` throws, and an unregistered tool would stay off
|
|
687
|
+
// while our registration is active on load.
|
|
688
|
+
if (process.platform === "win32")
|
|
689
|
+
minimal(
|
|
690
|
+
pi,
|
|
691
|
+
createPowerShellTool(cwd),
|
|
692
|
+
(th) => th.fg("accent", "PS>"),
|
|
693
|
+
(a, _th, soft, expanded) => {
|
|
694
|
+
const cmd = (a.command ?? "").replace(/\r?\n/g, " ").trim();
|
|
695
|
+
return paintLinks(expanded ? cmd : smartTitle(cmd), soft);
|
|
696
|
+
},
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// ls: no renderShell of its own, so pi's default shell turns it into a bg block.
|
|
700
|
+
minimal(
|
|
701
|
+
pi,
|
|
702
|
+
createLsTool(cwd),
|
|
703
|
+
(th) => th.fg("accent", "ls"),
|
|
704
|
+
(a, _th, soft) => soft(shortPath(a.path ?? ".")),
|
|
705
|
+
numRes((n) => `${n} entries`),
|
|
706
|
+
);
|
|
707
|
+
|
|
663
708
|
// bash: ` $ <6 words>…` / `✓ $ <full title when expanded> 0.1s`
|
|
664
709
|
minimal(
|
|
665
710
|
pi,
|
|
@@ -997,6 +1042,38 @@ if (isMain(import.meta.url)) {
|
|
|
997
1042
|
assert(fffOut.some((l: string) => l.includes("*.ts in D:/Pi/x")), "pi-fff find: renderCall title goes in the box");
|
|
998
1043
|
assert(fffOut.some((l: string) => l.includes("6 more lines")), "pi-fff find: renderResult summary goes in the box");
|
|
999
1044
|
|
|
1045
|
+
// pi-fff default mode (`ffgrep`/`fffind`): same renderer shape, different names.
|
|
1046
|
+
for (const n of ["ffgrep", "fffind"]) {
|
|
1047
|
+
const t = { toolName: n, toolDefinition: { renderCall: () => fffCall, renderResult: () => fffRes } };
|
|
1048
|
+
assert(
|
|
1049
|
+
ToolExecutionComponent.prototype.getRenderShell.call(t) === "self",
|
|
1050
|
+
`pi-fff ${n}: shell forced to self (default mode is boxed too)`,
|
|
1051
|
+
);
|
|
1052
|
+
const out = (ToolExecutionComponent.prototype.getResultRenderer.call(t) as any)(
|
|
1053
|
+
{ content: [{ type: "text", text: "src/a.ts\nsrc/b.ts" }] },
|
|
1054
|
+
{ expanded: false, isPartial: false },
|
|
1055
|
+
th,
|
|
1056
|
+
{ args: { pattern: "*.ts" }, state: {} },
|
|
1057
|
+
).render(60);
|
|
1058
|
+
assert(
|
|
1059
|
+
out[0].startsWith("╭") && out.some((l: string) => l.includes("6 more lines")),
|
|
1060
|
+
`pi-fff ${n}: result + summary merged into one box`,
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// pi core `powershell` / `ls` spread their renderers into the tool object and set no
|
|
1065
|
+
// renderShell, so pi's "default" shell used to render them as a bare bg block (see the
|
|
1066
|
+
// OWN_BOX block below for the registration half of this fix).
|
|
1067
|
+
for (const [n, t] of [
|
|
1068
|
+
["powershell", createPowerShellToolDefinition(cwd)],
|
|
1069
|
+
["ls", createLsToolDefinition(cwd)],
|
|
1070
|
+
] as const) {
|
|
1071
|
+
assert(
|
|
1072
|
+
ToolExecutionComponent.prototype.getRenderShell.call({ toolName: n, toolDefinition: t }) === "self",
|
|
1073
|
+
`pi core ${n}: shell forced to self (joins our box, not a bg block)`,
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1000
1077
|
const oursFind = { toolName: "find", toolDefinition: { renderShell: "self", renderCall: () => fffCall, renderResult: () => fffRes } };
|
|
1001
1078
|
assert(
|
|
1002
1079
|
(ToolExecutionComponent.prototype.getCallRenderer.call(oursFind) as any)() === fffCall &&
|
|
@@ -1067,7 +1144,35 @@ if (isMain(import.meta.url)) {
|
|
|
1067
1144
|
"OWN_BOX: our own tools + boxed tools registered",
|
|
1068
1145
|
);
|
|
1069
1146
|
assert(!OWN_BOX.has("mcp") && !OWN_BOX.has("source_check"), "OWN_BOX: third-party renderers not tightened");
|
|
1070
|
-
|
|
1147
|
+
// pi core `powershell`/`ls` spread their renderer functions onto the tool object and
|
|
1148
|
+
// set no renderShell; that is exactly the shape that used to fall into pi's "default"
|
|
1149
|
+
// shell and render as a bare bg block. Registering them through minimal() boxes them.
|
|
1150
|
+
const coreReg: Record<string, any> = {};
|
|
1151
|
+
arnativeTools({ registerTool: (t: any) => { coreReg[t.name] = t; }, registerToolRenderer: () => {}, on: () => {} } as never);
|
|
1152
|
+
for (const [n, t] of [
|
|
1153
|
+
["powershell", createPowerShellToolDefinition(cwd)],
|
|
1154
|
+
["ls", createLsToolDefinition(cwd)],
|
|
1155
|
+
] as const) {
|
|
1156
|
+
const def = t as any;
|
|
1157
|
+
assert(
|
|
1158
|
+
Boolean(def.renderCall) && Boolean(def.renderResult),
|
|
1159
|
+
`pi core ${n}: the definition pi runs spreads its renderers onto the object`,
|
|
1160
|
+
);
|
|
1161
|
+
assert(
|
|
1162
|
+
ToolExecutionComponent.prototype.getRenderShell.call({ toolName: n, toolDefinition: def }) === "self",
|
|
1163
|
+
`pi core ${n}: shell forced to self (joins our box, not a bg block)`,
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
// The guard is load-bearing: `getPowerShellConfig()` throws off Windows, and our
|
|
1167
|
+
// registration activates a tool the builtin deliberately leaves off.
|
|
1168
|
+
assert(
|
|
1169
|
+
coreReg.ls?.renderShell === "self",
|
|
1170
|
+
"pi core ls: registered through minimal() (boxing is live)",
|
|
1171
|
+
);
|
|
1172
|
+
assert(
|
|
1173
|
+
process.platform === "win32" ? coreReg.powershell?.renderShell === "self" : coreReg.powershell === undefined,
|
|
1174
|
+
"pi core powershell: registered on Windows only (getPowerShellConfig throws elsewhere)",
|
|
1175
|
+
); assert(hasOwnRendererDef(mcpTool) && !hasOwnRendererDef(noRenderer), "tightened benchmark: tool renderer definition");
|
|
1071
1176
|
assert(
|
|
1072
1177
|
capped(th, ["a", "b", "c"], false).length === CALL_ROWS + 1 &&
|
|
1073
1178
|
capped(th, ["a", "b", "c"], true).length === 3 &&
|
|
@@ -238,7 +238,7 @@ export class PackageUpdateBoxComponent implements Component {
|
|
|
238
238
|
const th = this.th;
|
|
239
239
|
const fg = (color: string, text: string) => (th ? th.fg(color, text) : text);
|
|
240
240
|
const rows = [
|
|
241
|
-
fg("warning", "\x1b[1m\uf449
|
|
241
|
+
fg("warning", "\x1b[1m\uf449 Package Updates Available\x1b[22m"),
|
|
242
242
|
`${fg("muted", "Package updates are available. Run ")}${fg("accent", "pi update --extensions")}`,
|
|
243
243
|
fg("muted", "Packages:"),
|
|
244
244
|
...this.packages.map((pkg) => `- ${pkg}`),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
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",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
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
|
-
"test": "node lib/box.ts && node lib/ansi.ts && node lib/color.ts && node lib/shortcuts.ts && node lib/preset-store.ts && node themes/gen-themes.mjs --check && node extensions/tools.ts && node extensions/footer.ts && node extensions/timestamps.ts && node extensions/
|
|
44
|
+
"test": "node lib/box.ts && node lib/ansi.ts && node lib/color.ts && node lib/shortcuts.ts && node lib/preset-store.ts && node themes/gen-themes.mjs --check && node extensions/tools.ts && node extensions/footer.ts && node extensions/timestamps.ts && node extensions/ui-render-tweaks.ts && node extensions/usage.ts && node extensions/arnative.ts && node --test",
|
|
45
45
|
"themes": "node themes/gen-themes.mjs",
|
|
46
46
|
"release:check": "node scripts/release-check.mjs pre-tag",
|
|
47
47
|
"prepublishOnly": "npm test && node scripts/release-check.mjs pre-publish"
|