@bismawy/pi-arnative 0.2.12 → 0.3.1
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 +42 -0
- package/README.md +8 -5
- package/extensions/arnative.ts +363 -0
- package/extensions/footer.ts +259 -136
- package/extensions/section-headers.ts +216 -138
- package/extensions/timestamps.ts +5 -2
- package/extensions/tools.ts +30 -40
- package/extensions/ui-render-tweaks.ts +87 -23
- package/extensions/usage.ts +37 -13
- package/lib/ansi.ts +23 -0
- package/lib/color.ts +118 -0
- package/lib/format.ts +6 -0
- package/lib/usage-store.ts +25 -16
- package/package.json +5 -4
- package/themes/arnative-cyberpunk.json +70 -78
- package/themes/arnative-dracula.json +70 -78
- package/themes/arnative-emerald.json +70 -78
- package/themes/arnative-gruvbox.json +70 -78
- package/themes/arnative-matrix.json +70 -78
- package/themes/arnative-nord.json +70 -78
- package/themes/arnative-sun.json +70 -78
- package/themes/arnative-synthwave.json +70 -78
- package/themes/arnative-violet.json +70 -78
- package/themes/arnative-zinc.json +70 -78
- package/themes/arnative.json +70 -78
- package/themes/gen-themes.mjs +301 -224
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Arnative header box & horizontal tab system:
|
|
3
|
-
* - Round-corner box
|
|
4
|
-
* divider
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* - Round-corner box: left column = the pi block mark (4x8, P accent / i soft) with
|
|
4
|
+
* "pi vX" under it; a vertical divider; then the info column = "Arnative vY
|
|
5
|
+
* · Welcome back, <User>", the tab menu, the tab data.
|
|
6
|
+
* - Greeting = OS account name (first letter capitalized). The tab box has no label;
|
|
7
|
+
* its column dividers join the borders with ┬/┴ (full height).
|
|
8
|
+
* - Interactive tab menu (mouse click): Model + Context/Skills/Extensions/Shortcut
|
|
9
|
+
* with item counts. Icon = accent, text = soft. The key cheatsheet lives in the
|
|
9
10
|
* Shortcut tab, not in a header line.
|
|
10
|
-
* - Box divider '├────┤'. Active tab content is left-aligned, wrapped,
|
|
11
|
+
* - Box divider '├────┤'. Active tab content is left-aligned, wrapped, soft.
|
|
11
12
|
* - Hides pi's built-in stacked list in loadedResourcesContainer.
|
|
12
13
|
*/
|
|
13
14
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -32,14 +33,14 @@ import {
|
|
|
32
33
|
import { renderBoxLines } from "../lib/box.ts";
|
|
33
34
|
import { stripAnsi, themeOf } from "../lib/ansi.ts";
|
|
34
35
|
import { assert, isMain } from "../lib/check.ts";
|
|
35
|
-
import { capitalize, modelDisplayParts } from "../lib/format.ts";
|
|
36
|
+
import { capitalize, markedLabels, modelDisplayParts } from "../lib/format.ts";
|
|
36
37
|
import { SHORTCUT_NEW_SESSION, SHORTCUT_NEXT_TAB, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
|
|
37
38
|
import { formatTokens, getModelAllTimeUsage } from "../lib/usage-store.ts";
|
|
38
39
|
|
|
39
|
-
export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "
|
|
40
|
+
export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "Shortcut";
|
|
40
41
|
|
|
41
42
|
// Tab order for the next-tab shortcut and for the menu line.
|
|
42
|
-
export const TAB_ORDER: TabKey[] = ["Directory", "Model", "Context", "Skills", "Extensions", "
|
|
43
|
+
export const TAB_ORDER: TabKey[] = ["Directory", "Model", "Context", "Skills", "Extensions", "Shortcut"];
|
|
43
44
|
|
|
44
45
|
export const SECTION_ICONS: Record<TabKey, string> = {
|
|
45
46
|
Model: "\uf1b2",
|
|
@@ -47,7 +48,6 @@ export const SECTION_ICONS: Record<TabKey, string> = {
|
|
|
47
48
|
Context: "\udb84\uddd7",
|
|
48
49
|
Skills: "\uec21",
|
|
49
50
|
Extensions: "\ueea8",
|
|
50
|
-
Themes: "\uee72",
|
|
51
51
|
Shortcut: "\uf11c", // nf-fa-keyboard_o
|
|
52
52
|
};
|
|
53
53
|
|
|
@@ -94,12 +94,12 @@ export function themedTextOf(child: any): string {
|
|
|
94
94
|
return typeof child?.getText === "function" ? String(child.getText()) : "";
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
export function sectionNameOf(text: string): "Context" | "Skills" | "Extensions" |
|
|
97
|
+
export function sectionNameOf(text: string): "Context" | "Skills" | "Extensions" | null {
|
|
98
98
|
const nl = text.indexOf("\n");
|
|
99
99
|
const first = stripAnsi(nl === -1 ? text : text.slice(0, nl)).trim();
|
|
100
100
|
const m = /^\[([A-Za-z]+)\]$/.exec(first);
|
|
101
101
|
const name = m ? m[1] : null;
|
|
102
|
-
if (name === "Context" || name === "Skills" || name === "Extensions"
|
|
102
|
+
if (name === "Context" || name === "Skills" || name === "Extensions") {
|
|
103
103
|
return name;
|
|
104
104
|
}
|
|
105
105
|
return null;
|
|
@@ -268,30 +268,22 @@ export function centerLine(text: string, width: number, side = "│"): string {
|
|
|
268
268
|
return side + " ".repeat(left) + text + " ".repeat(right) + side;
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// pi.dev
|
|
271
|
+
// pi.dev block mark: the logo grid at terminal aspect (2 chars per column), 4 lines.
|
|
272
|
+
// P (the salmon/blue structure, columns 0-2) in the theme accent; i (the yellow
|
|
273
|
+
// bar, column 3) in the theme soft — same P/i split as the old wordmark.
|
|
272
274
|
export function buildLogoLines(th: Themeish): string[] {
|
|
273
|
-
const
|
|
274
|
-
const
|
|
275
|
-
|
|
275
|
+
const a = (s: string) => fgFirst(th, ["accent"], s);
|
|
276
|
+
const t = (s: string) => fgFirst(th, ["accentSoft", "text"], s);
|
|
276
277
|
return [
|
|
277
|
-
`${
|
|
278
|
-
`${
|
|
279
|
-
`${
|
|
280
|
-
`${
|
|
281
|
-
`${p("███")} ${i("███")}`,
|
|
282
|
-
`${p("███")} ${i("███")}`,
|
|
278
|
+
`${a("██████")} `,
|
|
279
|
+
`${a("██ ██")} `,
|
|
280
|
+
`${a("████")} ${t("██")}`,
|
|
281
|
+
`${a("██")} ${t("██")}`,
|
|
283
282
|
];
|
|
284
283
|
}
|
|
285
284
|
|
|
286
285
|
export const ASCII_LOGO_LINES = buildLogoLines(null);
|
|
287
286
|
|
|
288
|
-
// Logo column width, derived from the art so it is not a magic number.
|
|
289
|
-
const LOGO_W = Math.max(...ASCII_LOGO_LINES.map((l) => visibleWidth(l)));
|
|
290
|
-
// Info area starts at column: "│ " (2) + logo (LOGO_W) + " │ " (3).
|
|
291
|
-
const INFO_X = LOGO_W + 5;
|
|
292
|
-
// Columns left for info: width - "│ " - logo - " │ " - info - " │".
|
|
293
|
-
const infoWidth = (width: number) => Math.max(8, width - LOGO_W - 7);
|
|
294
|
-
|
|
295
287
|
// Singleton store for loaded resources
|
|
296
288
|
const STORE_KEY = Symbol.for("pi-arnative.resourceStore");
|
|
297
289
|
export const tabStore: Map<TabKey, string[]> =
|
|
@@ -302,11 +294,48 @@ export const tabStore: Map<TabKey, string[]> =
|
|
|
302
294
|
["Context", []],
|
|
303
295
|
["Skills", []],
|
|
304
296
|
["Extensions", []],
|
|
305
|
-
["Themes", []],
|
|
306
297
|
]);
|
|
307
298
|
(globalThis as Record<symbol, Map<TabKey, string[]>>)[STORE_KEY] = tabStore;
|
|
308
299
|
|
|
309
|
-
|
|
300
|
+
// Header preset chosen through `/arnative headers`. "Arnative (Full)" = this extension's
|
|
301
|
+
// header; "Pi (system)" = pi's built-in header. The picker lives in arnative.ts, so the
|
|
302
|
+
// choice is shared through globalThis like the other cross-module state.
|
|
303
|
+
// Listed built-in-first: the `(Full)` suffix is rendered dim, so the plain row reads cleaner on top.
|
|
304
|
+
export const HEADER_PRESETS = ["Pi (system)", "Arnative (Full)"] as const;
|
|
305
|
+
export type HeaderPreset = (typeof HEADER_PRESETS)[number];
|
|
306
|
+
export const DEFAULT_HEADER_PRESET: HeaderPreset = "Arnative (Full)";
|
|
307
|
+
const HEADER_PRESET_KEY = Symbol.for("pi-arnative.headerPreset");
|
|
308
|
+
|
|
309
|
+
export function activeHeaderPreset(): HeaderPreset {
|
|
310
|
+
const stored = (globalThis as Record<symbol, unknown>)[HEADER_PRESET_KEY];
|
|
311
|
+
return HEADER_PRESETS.includes(stored as HeaderPreset) ? (stored as HeaderPreset) : DEFAULT_HEADER_PRESET;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Hand the header slot to the active preset: arnative's box, or pi's built-in ("Pi (system)"). */
|
|
315
|
+
export function applyHeaderPreset(ctx: ExtensionContext): void {
|
|
316
|
+
if (activeHeaderPreset() === "Pi (system)") {
|
|
317
|
+
ctx.ui?.setHeader?.(undefined);
|
|
318
|
+
} else {
|
|
319
|
+
ctx.ui?.setHeader?.((tui, theme) => new ArnativeHeader(tui, theme, ctx));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Select a preset and apply it immediately (real-time: the header swaps on the spot). */
|
|
324
|
+
export function setHeaderPreset(name: HeaderPreset, ctx: ExtensionContext): void {
|
|
325
|
+
(globalThis as Record<symbol, unknown>)[HEADER_PRESET_KEY] = name;
|
|
326
|
+
applyHeaderPreset(ctx);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// The installed header instance. globalThis-backed because `/reload` re-imports the
|
|
330
|
+
// extension modules and arnative.ts imports this file, so two copies can coexist.
|
|
331
|
+
const HEADER_INSTANCE_KEY = Symbol.for("pi-arnative.headerInstance");
|
|
332
|
+
function headerInstance(): ArnativeHeader | null {
|
|
333
|
+
return (globalThis as Record<symbol, ArnativeHeader | undefined>)[HEADER_INSTANCE_KEY] ?? null;
|
|
334
|
+
}
|
|
335
|
+
function setHeaderInstance(instance: ArnativeHeader | null): void {
|
|
336
|
+
if (instance) (globalThis as Record<symbol, ArnativeHeader>)[HEADER_INSTANCE_KEY] = instance;
|
|
337
|
+
else delete (globalThis as Record<symbol, ArnativeHeader | undefined>)[HEADER_INSTANCE_KEY];
|
|
338
|
+
}
|
|
310
339
|
|
|
311
340
|
// Model name formatted exactly like in the footer: Gemini 3.8 Flash (Antigravity), Deepseek V4.1 Flash (Yarz), etc.
|
|
312
341
|
export function formatModelDisplayName(model?: { id?: string; name?: string; provider?: string }): string {
|
|
@@ -334,14 +363,14 @@ export class ArnativeHeader implements Component {
|
|
|
334
363
|
this.tui = tui;
|
|
335
364
|
this.themeProxy = themeProxy;
|
|
336
365
|
this.ctx = ctx;
|
|
337
|
-
|
|
366
|
+
setHeaderInstance(this);
|
|
338
367
|
}
|
|
339
368
|
|
|
340
369
|
invalidate(): void {}
|
|
341
370
|
|
|
342
371
|
dispose(): void {
|
|
343
|
-
if (
|
|
344
|
-
|
|
372
|
+
if (headerInstance() === this) {
|
|
373
|
+
setHeaderInstance(null);
|
|
345
374
|
}
|
|
346
375
|
}
|
|
347
376
|
|
|
@@ -389,13 +418,6 @@ export class ArnativeHeader implements Component {
|
|
|
389
418
|
count: tabStore.get("Extensions")?.length ?? 0,
|
|
390
419
|
label: `Extensions [${tabStore.get("Extensions")?.length ?? 0}]`,
|
|
391
420
|
},
|
|
392
|
-
{
|
|
393
|
-
key: "Themes",
|
|
394
|
-
name: "Themes",
|
|
395
|
-
icon: SECTION_ICONS.Themes,
|
|
396
|
-
count: tabStore.get("Themes")?.length ?? 0,
|
|
397
|
-
label: `Themes [${tabStore.get("Themes")?.length ?? 0}]`,
|
|
398
|
-
},
|
|
399
421
|
{
|
|
400
422
|
key: "Shortcut",
|
|
401
423
|
name: "Shortcut",
|
|
@@ -443,33 +465,42 @@ export class ArnativeHeader implements Component {
|
|
|
443
465
|
}
|
|
444
466
|
const th = this.themeProxy || (activeThemeProxy as Themeish);
|
|
445
467
|
const side = fgFirst(th, ["dim"], "│");
|
|
446
|
-
const dash = "─".repeat(Math.max(0, width - 2));
|
|
447
468
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
469
|
+
const infoLines: string[] = [];
|
|
470
|
+
const brand = (name: string) => fgFirst(th, ["accent", "accentSoft"], `\x1b[1m${name}\x1b[22m`);
|
|
471
|
+
const dim = (text: string) => fgFirst(th, ["dim"], text);
|
|
472
|
+
|
|
473
|
+
// Left column: the mark with the pi version under it (no gap). The column is as
|
|
474
|
+
// wide as the widest of them.
|
|
475
|
+
const versionStr = VERSION || "0.87.1";
|
|
476
|
+
const leftLines = [
|
|
477
|
+
...buildLogoLines(th),
|
|
478
|
+
`${brand("pi")} ${dim(`v${versionStr}`)}`,
|
|
479
|
+
];
|
|
480
|
+
const colW = Math.max(...leftLines.map((l) => visibleWidth(l)));
|
|
481
|
+
const padLeft = (text: string) => text + " ".repeat(Math.max(0, colW - visibleWidth(text)));
|
|
482
|
+
// Columns left for info: width - "│ " - left column - " │ " - info - " │".
|
|
483
|
+
const infoW = Math.max(8, width - colW - 7);
|
|
451
484
|
const infoLine = (text: string): string => {
|
|
452
485
|
const t = truncateToWidth(text, infoW);
|
|
453
486
|
return t + " ".repeat(infoW - visibleWidth(t));
|
|
454
487
|
};
|
|
455
|
-
|
|
456
|
-
const out: string[] = [fgFirst(th, ["dim"], `╭${dash}╮`)];
|
|
457
|
-
const infoLines: string[] = [];
|
|
458
488
|
// Inner box: the tab menu line is framed, content is (infoW - 2) columns.
|
|
459
489
|
const innerW = Math.max(1, infoW - 2);
|
|
460
|
-
const brand = (name: string) => fgFirst(th, ["accent", "tint"], `\x1b[1m${name}\x1b[22m`);
|
|
461
|
-
const dim = (text: string) => fgFirst(th, ["dim"], text);
|
|
462
490
|
|
|
463
|
-
//
|
|
464
|
-
|
|
491
|
+
// The column divider runs the full height: it joins the top/bottom borders with
|
|
492
|
+
// ┬/┴ so there is no gap between the split and the frame.
|
|
493
|
+
const jx = colW + 3;
|
|
494
|
+
const border = (l: string, mid: string, r: string) =>
|
|
495
|
+
fgFirst(th, ["dim"], `${l}${"─".repeat(jx - 1)}${mid}${"─".repeat(Math.max(0, width - jx - 2))}${r}`);
|
|
496
|
+
const out: string[] = [border("╭", "┬", "╮")];
|
|
465
497
|
|
|
466
|
-
//
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
498
|
+
// 1. Brand + greeting on one line ("Arnative vX · Welcome back, <User>"; the username
|
|
499
|
+
// is capitalized), then a blank line before the tab box.
|
|
500
|
+
const arnVer = ARNATIVE_VERSION ? ` ${dim(`v${ARNATIVE_VERSION}`)}` : "";
|
|
501
|
+
infoLines.push(`${brand("Arnative")}${arnVer}${dim(" · ")}${dim("Welcome back,")} ${brand(capitalize(deviceUser()))}`);
|
|
471
502
|
|
|
472
|
-
// 2. Tab menu: icon = accent, text =
|
|
503
|
+
// 2. Tab menu: icon = accent, text = soft (bold when active)
|
|
473
504
|
const tabs = this.getTabsData(width);
|
|
474
505
|
const sepTabPlain = " │ ";
|
|
475
506
|
const sepTab = fgFirst(th, ["dim"], sepTabPlain);
|
|
@@ -481,7 +512,7 @@ export class ArnativeHeader implements Component {
|
|
|
481
512
|
const isActive = t.key === this.activeTab;
|
|
482
513
|
const iconStyled = isActive ? fgFirst(th, ["accent"], t.icon) : fgFirst(th, ["dim"], t.icon);
|
|
483
514
|
const labelStyled = isActive
|
|
484
|
-
? fgFirst(th, ["
|
|
515
|
+
? fgFirst(th, ["accentSoft", "text"], `\x1b[1m${label}\x1b[22m`)
|
|
485
516
|
: fgFirst(th, ["dim"], label);
|
|
486
517
|
return { key: t.key, plain, formatted: `${iconStyled} ${labelStyled}` };
|
|
487
518
|
};
|
|
@@ -499,7 +530,7 @@ export class ArnativeHeader implements Component {
|
|
|
499
530
|
}
|
|
500
531
|
this.renderedTabRegions = [];
|
|
501
532
|
// The tab row has its own left border + a leading space before the first tab.
|
|
502
|
-
let curX =
|
|
533
|
+
let curX = colW + 7;
|
|
503
534
|
for (const t of kept) {
|
|
504
535
|
const w = visibleWidth(t.plain);
|
|
505
536
|
this.renderedTabRegions.push({ key: t.key, startX: curX, endX: curX + w - 1 });
|
|
@@ -512,23 +543,29 @@ export class ArnativeHeader implements Component {
|
|
|
512
543
|
const t = truncateToWidth(text, tabInnerW);
|
|
513
544
|
return `${side}${t}${" ".repeat(tabInnerW - visibleWidth(t))}${side}`;
|
|
514
545
|
};
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
546
|
+
// Columns of the " │ " separators inside the tab row, so the borders can join them
|
|
547
|
+
// with ┬/┴ and every divider runs the full height of the box.
|
|
548
|
+
const tabSeps: number[] = [];
|
|
549
|
+
for (let i = 0, acc = 1; i < kept.length; i++) {
|
|
550
|
+
acc += visibleWidth(kept[i]!.plain);
|
|
551
|
+
if (i < kept.length - 1) {
|
|
552
|
+
tabSeps.push(acc + 1);
|
|
553
|
+
acc += 3;
|
|
521
554
|
}
|
|
522
|
-
|
|
555
|
+
}
|
|
556
|
+
const tabRule = (l: string, r: string, junction: string): string => {
|
|
557
|
+
const inner = new Array<string>(tabInnerW).fill("─");
|
|
558
|
+
for (const s of tabSeps) if (s >= 0 && s < tabInnerW) inner[s] = junction;
|
|
559
|
+
return fgFirst(th, ["dim"], `${l}${inner.join("")}${r}`);
|
|
523
560
|
};
|
|
524
|
-
infoLines.push(tabRule("╭", "╮", "
|
|
561
|
+
infoLines.push(tabRule("╭", "╮", "┬"));
|
|
525
562
|
// One space from the left border, then " │ " between tabs.
|
|
526
563
|
infoLines.push(tabRow(tabText));
|
|
527
564
|
// Tab line = top border (index 0) + the infoLines above + this line.
|
|
528
565
|
this.renderedTabLineY = infoLines.length;
|
|
529
|
-
infoLines.push(tabRule("╰", "╯"));
|
|
566
|
+
infoLines.push(tabRule("╰", "╯", "┴"));
|
|
530
567
|
|
|
531
|
-
// 3. Active tab data (left-aligned, neatly wrapped,
|
|
568
|
+
// 3. Active tab data (left-aligned, neatly wrapped, soft)
|
|
532
569
|
let activeItems: string[] = [];
|
|
533
570
|
if (this.activeTab === "Model") {
|
|
534
571
|
const modelObj = this.ctx?.model ?? (globalThis as Record<symbol, any>)[MODEL_SNAPSHOT_KEY];
|
|
@@ -536,22 +573,22 @@ export class ArnativeHeader implements Component {
|
|
|
536
573
|
const thLvl = this.ctx?.thinkingLevel ?? (globalThis as Record<symbol, any>)[THINKING_SNAPSHOT_KEY];
|
|
537
574
|
const thinkLevel = thLvl && thLvl !== "off" ? capitalize(thLvl) : "Off";
|
|
538
575
|
const sep = fgFirst(th, ["dim"], " · ");
|
|
539
|
-
// Model name in
|
|
576
|
+
// Model name in soft, its "(Provider)" tail dim.
|
|
540
577
|
const provSuffix = fullName.match(/(\s*\([^()]*\))$/)?.[1] ?? "";
|
|
541
578
|
const nameMain = provSuffix ? fullName.slice(0, -provSuffix.length) : fullName;
|
|
542
|
-
let line = `${fgFirst(th, ["
|
|
579
|
+
let line = `${fgFirst(th, ["accentSoft", "text"], nameMain)}${provSuffix ? fgFirst(th, ["dim"], provSuffix) : ""}${sep}${fgFirst(th, ["accentSoft", "text"], `Thinking: ${thinkLevel}`)}`;
|
|
543
580
|
|
|
544
581
|
// All-time usage of that model (same as /usage): ↑... ↓... ...
|
|
545
582
|
const usage = getModelAllTimeUsage(modelObj?.id);
|
|
546
583
|
const tokenParts: string[] = [];
|
|
547
584
|
if (usage.input > 0) {
|
|
548
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "↑")}${fgFirst(th, ["
|
|
585
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "↑")}${fgFirst(th, ["accentSoft", "text"], formatTokens(usage.input))}`);
|
|
549
586
|
}
|
|
550
587
|
if (usage.output > 0) {
|
|
551
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "↓")}${fgFirst(th, ["
|
|
588
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "↓")}${fgFirst(th, ["accentSoft", "text"], formatTokens(usage.output))}`);
|
|
552
589
|
}
|
|
553
590
|
if (usage.cacheRead > 0) {
|
|
554
|
-
tokenParts.push(`${fgFirst(th, ["accent"], "\uf49b ")}${fgFirst(th, ["
|
|
591
|
+
tokenParts.push(`${fgFirst(th, ["accent"], "\uf49b ")}${fgFirst(th, ["accentSoft", "text"], formatTokens(usage.cacheRead))}`);
|
|
555
592
|
}
|
|
556
593
|
if (tokenParts.length > 0) {
|
|
557
594
|
line += `${sep}${tokenParts.join(" ")}`;
|
|
@@ -564,35 +601,33 @@ export class ArnativeHeader implements Component {
|
|
|
564
601
|
|
|
565
602
|
if (this.activeTab === "Directory") {
|
|
566
603
|
for (const line of wrapPath(getCwd(this.ctx), infoW)) {
|
|
567
|
-
infoLines.push(fgFirst(th, ["
|
|
604
|
+
infoLines.push(fgFirst(th, ["accentSoft", "text"], line));
|
|
568
605
|
}
|
|
569
606
|
} else if (this.activeTab === "Shortcut") {
|
|
570
607
|
for (const line of wrapTokens(SHORTCUTS, infoW)) {
|
|
571
|
-
infoLines.push(fgFirst(th, ["
|
|
608
|
+
infoLines.push(fgFirst(th, ["accentSoft", "text"], line));
|
|
572
609
|
}
|
|
573
610
|
} else if (activeItems.length === 0) {
|
|
574
|
-
infoLines.push(fgFirst(th, ["
|
|
611
|
+
infoLines.push(fgFirst(th, ["accentSoft"], "(empty)"));
|
|
575
612
|
} else if (this.activeTab === "Model") {
|
|
576
|
-
// The Model tab already carries
|
|
613
|
+
// The Model tab already carries soft styling + a dim separator
|
|
577
614
|
infoLines.push(...activeItems);
|
|
578
615
|
} else {
|
|
579
616
|
for (const line of wrapCommaItems(activeItems, infoW)) {
|
|
580
|
-
// Point 5: selected menu data uses
|
|
581
|
-
infoLines.push(fgFirst(th, ["
|
|
617
|
+
// Point 5: selected menu data uses soft only
|
|
618
|
+
infoLines.push(fgFirst(th, ["accentSoft", "text"], line));
|
|
582
619
|
}
|
|
583
620
|
}
|
|
584
621
|
|
|
585
|
-
// Compose lines:
|
|
586
|
-
//
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const logo = logoLines[i] ?? blankLogo;
|
|
591
|
-
out.push(`${side} ${logo} ${side} ${infoLine(infoLines[i] ?? "")} ${side}`);
|
|
622
|
+
// Compose lines: left column (mark + brand), vertical divider, left-aligned info.
|
|
623
|
+
// Whichever column is shorter is padded with blanks.
|
|
624
|
+
const rows = Math.max(leftLines.length, infoLines.length);
|
|
625
|
+
for (let i = 0; i < rows; i++) {
|
|
626
|
+
out.push(`${side} ${padLeft(leftLines[i] ?? "")} ${side} ${infoLine(infoLines[i] ?? "")} ${side}`);
|
|
592
627
|
}
|
|
593
628
|
|
|
594
629
|
// Bottom border
|
|
595
|
-
out.push(
|
|
630
|
+
out.push(border("╰", "┴", "╯"));
|
|
596
631
|
|
|
597
632
|
return out;
|
|
598
633
|
}
|
|
@@ -608,6 +643,8 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
|
|
|
608
643
|
if (typeof containerProto?.addChild === "function") {
|
|
609
644
|
const origAddChild = containerProto.addChild;
|
|
610
645
|
containerProto.addChild = function (child: any): unknown {
|
|
646
|
+
// "Pi (system)" preset = pi's own header, so leave pi's startup list untouched.
|
|
647
|
+
if (activeHeaderPreset() === "Pi (system)") return origAddChild.call(this, child);
|
|
611
648
|
try {
|
|
612
649
|
const body = sectionBodyOf(child);
|
|
613
650
|
const sectionName = body === null ? null : sectionNameOf(body);
|
|
@@ -629,8 +666,9 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
|
|
|
629
666
|
child.render = () => [];
|
|
630
667
|
|
|
631
668
|
// Re-render the header when it is already installed
|
|
632
|
-
|
|
633
|
-
|
|
669
|
+
const hdr = headerInstance();
|
|
670
|
+
if (hdr?.tui) {
|
|
671
|
+
hdr.tui.requestRender();
|
|
634
672
|
}
|
|
635
673
|
} else if ((this as Record<string, unknown>)._isLoadedResourcesContainer) {
|
|
636
674
|
// pi core's "[Extension issues]" -> wrap in an arnative box.
|
|
@@ -656,7 +694,7 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
|
|
|
656
694
|
export default function (pi: ExtensionAPI) {
|
|
657
695
|
// Next header tab — every platform variant of the shortcut (lib/shortcuts.ts).
|
|
658
696
|
const nextTab = async () => {
|
|
659
|
-
const hdr =
|
|
697
|
+
const hdr = headerInstance();
|
|
660
698
|
if (!hdr) return;
|
|
661
699
|
const order = TAB_ORDER;
|
|
662
700
|
hdr.activeTab = order[(order.indexOf(hdr.activeTab) + 1) % order.length]!;
|
|
@@ -668,10 +706,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
668
706
|
|
|
669
707
|
pi.on("session_start", async (_event, ctx) => {
|
|
670
708
|
activeThemeProxy = themeOf<Themeish>(ctx) ?? activeThemeProxy;
|
|
671
|
-
// Register the
|
|
672
|
-
ctx
|
|
673
|
-
return new ArnativeHeader(tui, theme, ctx);
|
|
674
|
-
});
|
|
709
|
+
// Register the header for the active preset
|
|
710
|
+
applyHeaderPreset(ctx);
|
|
675
711
|
});
|
|
676
712
|
|
|
677
713
|
pi.on("turn_start", async (_event, ctx) => {
|
|
@@ -682,8 +718,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
682
718
|
|
|
683
719
|
pi.on("turn_end", async () => {
|
|
684
720
|
// Usage is re-read at render time (collectUsageSummary caches 5s), so poking a render is enough.
|
|
685
|
-
|
|
686
|
-
|
|
721
|
+
const hdr = headerInstance();
|
|
722
|
+
if (hdr?.tui) {
|
|
723
|
+
hdr.tui.requestRender();
|
|
687
724
|
}
|
|
688
725
|
});
|
|
689
726
|
}
|
|
@@ -694,6 +731,17 @@ if (isMain(import.meta.url)) {
|
|
|
694
731
|
assert(sectionNameOf("\x1b[33m[Skills]\x1b[39m\n a, b") === "Skills", "header [Skills] detected");
|
|
695
732
|
assert(sectionNameOf("pi v0.87.1") === null, "non-section text is not detected");
|
|
696
733
|
|
|
734
|
+
// Header presets: Arnative (Full) is the default; Pi (system) hands the header slot back to pi.
|
|
735
|
+
assert(DEFAULT_HEADER_PRESET === "Arnative (Full)" && HEADER_PRESETS.length === 2, "Arnative (Full) is the default header preset");
|
|
736
|
+
assert(HEADER_PRESETS.join(" | ") === "Pi (system) | Arnative (Full)", "built-in preset is listed first");
|
|
737
|
+
assert(markedLabels(HEADER_PRESETS, "Arnative (Full)").join(" | ") === " Pi (system) | ● Arnative (Full)", "preset labels mark the active one");
|
|
738
|
+
let installed: unknown = "untouched";
|
|
739
|
+
const presetCtx: any = { ui: { setHeader: (factory: unknown) => { installed = factory; } } };
|
|
740
|
+
setHeaderPreset("Pi (system)", presetCtx);
|
|
741
|
+
assert(installed === undefined && activeHeaderPreset() === "Pi (system)", "Pi (system) clears the custom header");
|
|
742
|
+
setHeaderPreset("Arnative (Full)", presetCtx);
|
|
743
|
+
assert(typeof installed === "function" && activeHeaderPreset() === "Arnative (Full)", "Arnative (Full) installs the header factory");
|
|
744
|
+
|
|
697
745
|
// Parsing & wrapping
|
|
698
746
|
assert(extractItemsFromBody(" a, b, c").length === 3, "extractItemsFromBody splits 3 comma items");
|
|
699
747
|
assert(extractItemsFromBody(" x\n y\n z").length === 3, "extractItemsFromBody returns 3 lines");
|
|
@@ -739,7 +787,6 @@ if (isMain(import.meta.url)) {
|
|
|
739
787
|
tabStore.set("Context", ["AGENTS.md"]);
|
|
740
788
|
tabStore.set("Skills", ["agents-sdk", "cloudflare"]);
|
|
741
789
|
tabStore.set("Extensions", ["@bismawy/pi-agentrouter@1.6.1", "footer.ts"]);
|
|
742
|
-
tabStore.set("Themes", ["arnative", "dark"]);
|
|
743
790
|
|
|
744
791
|
let renderRequested = false;
|
|
745
792
|
const fakeTui: any = {
|
|
@@ -760,68 +807,99 @@ if (isMain(import.meta.url)) {
|
|
|
760
807
|
|
|
761
808
|
// Default view: the working directory path, shown on open
|
|
762
809
|
const linesDefault = hdr.render(100);
|
|
763
|
-
assert(stripAnsi(linesDefault[
|
|
810
|
+
assert(stripAnsi(linesDefault[5]!).includes("/run/media/bisma/DATA/Pi/pi-arnative"), "directory path shown as soon as the first tab opens");
|
|
764
811
|
|
|
765
812
|
hdr.activeTab = "Model";
|
|
766
813
|
const lines100 = hdr.render(100);
|
|
767
|
-
assert(lines100.length ===
|
|
814
|
+
assert(lines100.length === 7, "render 100 = border + 5 rows + border (left: mark + gap + pi version; info: brand line + tab box + data)");
|
|
768
815
|
assert(lines100.every((l) => visibleWidth(l) === 100), "every line at width 100 is exactly 100 columns");
|
|
769
816
|
assert(lines100[0]!.includes("╭") && lines100[0]!.includes("╮"), "round top border");
|
|
770
|
-
assert(lines100[
|
|
817
|
+
assert(lines100[6]!.includes("╰") && lines100[6]!.includes("╯"), "round bottom border");
|
|
818
|
+
// The column divider joins the frame: full-height split, no gap at the borders.
|
|
819
|
+
const jx = stripAnsi(lines100[0]!).indexOf("┬");
|
|
820
|
+
assert(jx > 0 && stripAnsi(lines100[6]!).indexOf("┴") === jx, "┬/┴ junctions sit on the border at the divider column");
|
|
821
|
+
assert(
|
|
822
|
+
lines100.slice(1, 6).every((l) => stripAnsi(l)[jx] === "│"),
|
|
823
|
+
"the divider sits in the same column on every row",
|
|
824
|
+
);
|
|
771
825
|
const logoPlain = buildLogoLines(null);
|
|
772
|
-
assert(logoPlain.length ===
|
|
826
|
+
assert(logoPlain.length === 4 && visibleWidth(logoPlain[0]!) === 8, "pi block mark intact (4 lines x 8 columns)");
|
|
827
|
+
// P (columns 0-2) accent, i (the right bar) soft — the old wordmark's two-tone.
|
|
828
|
+
const logoTagged = buildLogoLines({
|
|
829
|
+
fg: (c: string, t: string) => (c === "accent" ? `<A>${t}</A>` : c === "accentSoft" ? `<T>${t}</T>` : t),
|
|
830
|
+
} as any);
|
|
831
|
+
assert(logoTagged[0] === "<A>██████</A> ", "mark top row: P in the theme accent");
|
|
832
|
+
assert(logoTagged[2] === "<A>████</A> <T>██</T>", "mark lower rows: P accent + i (right bar) soft");
|
|
773
833
|
assert(
|
|
774
|
-
lines100.slice(1,
|
|
775
|
-
"
|
|
834
|
+
lines100.slice(1, 5).every((l, i) => stripAnsi(l).slice(2, 10) === logoPlain[i]),
|
|
835
|
+
"pi block mark in the left column, 4 lines",
|
|
776
836
|
);
|
|
777
837
|
assert(
|
|
778
838
|
/Welcome back, \S/.test(stripAnsi(lines100[1]!)) && !stripAnsi(lines100[1]!).includes("pi v"),
|
|
779
839
|
"line 1: greeting uses the device username",
|
|
780
840
|
);
|
|
841
|
+
// Left column: the pi version sits directly under the mark (no gap).
|
|
842
|
+
assert(stripAnsi(lines100[5]!).slice(2, 12).includes("pi v"), "pi version sits under the mark in the left column");
|
|
843
|
+
// Info column: brand + greeting, then the tab box straight away (no blank line).
|
|
844
|
+
assert(/Arnative v\d+\.\d+\.\d+ · Welcome back, \S/.test(stripAnsi(lines100[1]!)), "info line 1: Arnative version · Welcome back, user");
|
|
845
|
+
assert(!stripAnsi(lines100[2]!).includes("Menu"), "tab box border carries no label");
|
|
846
|
+
// Tab box separators join its borders: ┬ above and ┴ below each "│". Compare by display
|
|
847
|
+
// column, not string index: the tab icons are astral (2 code units, 1 column).
|
|
848
|
+
const atColumn = (line: string, col: number): string | undefined => {
|
|
849
|
+
let c = 0;
|
|
850
|
+
for (const ch of stripAnsi(line)) {
|
|
851
|
+
const w = visibleWidth(ch);
|
|
852
|
+
if (col >= c && col < c + w) return ch;
|
|
853
|
+
c += w;
|
|
854
|
+
}
|
|
855
|
+
return undefined;
|
|
856
|
+
};
|
|
857
|
+
const tabSepCols = [...stripAnsi(lines100[2]!).matchAll(/┬/g)].map((m) => visibleWidth(stripAnsi(lines100[2]!).slice(0, m.index)));
|
|
858
|
+
assert(tabSepCols.length >= 2, "tab box top border has ┬ at each column divider");
|
|
781
859
|
assert(
|
|
782
|
-
|
|
783
|
-
"
|
|
860
|
+
tabSepCols.every((c) => atColumn(lines100[3]!, c) === "│" && atColumn(lines100[4]!, c) === "┴"),
|
|
861
|
+
"each tab divider is full height (┬ / │ / ┴ in the same column)",
|
|
784
862
|
);
|
|
785
|
-
assert(/╭─ Menu ─+╮/.test(stripAnsi(lines100[3]!)), "tab box top border carries the Menu label");
|
|
786
863
|
assert(!stripAnsi(lines100[1]!).includes("Interrupt"), "shortcut legend no longer on the top line");
|
|
787
864
|
// Both brand names use the theme accent, not only "pi".
|
|
788
865
|
const accentTheme = {
|
|
789
866
|
fg: (c: string, t: string) => (c === "accent" ? `<A>${t}</A>` : c === "dim" ? `<D>${t}</D>` : t),
|
|
790
867
|
};
|
|
791
868
|
const accentHeader = new ArnativeHeader(fakeTui, accentTheme as any, fakeCtx);
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
869
|
+
// The marker theme returns literal "<A>" text (not ANSI), so the measured width is
|
|
870
|
+
// inflated — render wide enough that the brand/greeting line is not truncated.
|
|
871
|
+
const accentLines = accentHeader.render(140);
|
|
872
|
+
assert(accentLines[5]!.includes("<A>\x1b[1mpi"), "the pi name uses the theme accent");
|
|
873
|
+
assert(accentLines[1]!.includes("<A>\x1b[1mArnative"), "the Arnative name uses the same theme accent");
|
|
874
|
+
const greetLine = accentHeader.render(140)[1]!;
|
|
796
875
|
assert(greetLine.includes("<D>Welcome back,"), "greeting text uses the dim color");
|
|
797
|
-
assert(greetLine.includes(`<A>\x1b[1m${deviceUser()}`), "device username uses the theme accent");
|
|
798
|
-
assert(stripAnsi(lines100[
|
|
876
|
+
assert(greetLine.includes(`<A>\x1b[1m${capitalize(deviceUser())}`), "device username uses the theme accent, first letter capitalized");
|
|
877
|
+
assert(stripAnsi(lines100[3]!).includes("Model [Gemini 3.8 Flash]"), "Model tab in the menu (Model [name])");
|
|
799
878
|
// Directory tab: name only in the menu, whole path in the content line below
|
|
800
879
|
hdr.activeTab = "Directory";
|
|
801
880
|
hdr.render(100);
|
|
802
|
-
assert(stripAnsi(lines100[
|
|
803
|
-
assert(!stripAnsi(lines100[
|
|
804
|
-
assert(stripAnsi(lines100[
|
|
881
|
+
assert(stripAnsi(lines100[3]!).includes("Directory"), "Directory tab shown in the menu");
|
|
882
|
+
assert(!stripAnsi(lines100[3]!).includes("Directory: /"), "Directory menu entry has no path (detail only on click)");
|
|
883
|
+
assert(stripAnsi(lines100[3]!).indexOf("Directory") < stripAnsi(lines100[3]!).indexOf("Model ["), "Directory tab is leftmost");
|
|
805
884
|
hdr.activeTab = "Model";
|
|
806
885
|
hdr.render(100);
|
|
807
|
-
assert(stripAnsi(lines100[
|
|
886
|
+
assert(stripAnsi(lines100[5]!).includes("Gemini 3.8 Flash (Antigravity)"), "active Model tab data on the last info line");
|
|
808
887
|
// Directory tab content: full path, wrapped, nothing lost
|
|
809
888
|
hdr.activeTab = "Directory";
|
|
810
889
|
const linesDir = hdr.render(100);
|
|
811
|
-
const dirContent = stripAnsi(linesDir[
|
|
890
|
+
const dirContent = stripAnsi(linesDir[5]!);
|
|
812
891
|
assert(dirContent.includes("/run/media/bisma"), "full Directory tab path (start) shown");
|
|
813
892
|
assert(dirContent.includes("pi-arnative"), "full Directory tab path (end) shown — no truncation");
|
|
814
893
|
hdr.activeTab = "Model";
|
|
815
894
|
hdr.render(100);
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
assert((lines100[5]!.match(/\x1b\[/g) ?? []).length === 8, 'bottom box line (row 5) = one fully dimmed block');
|
|
895
|
+
// Tab box borders are a single fully dimmed block (the "─"/"┬"/"┴" runs are colored too).
|
|
896
|
+
assert((lines100[4]!.match(/\x1b\[/g) ?? []).length === 8, "bottom box line (row 4) = one fully dimmed block");
|
|
819
897
|
assert(
|
|
820
|
-
lines100[
|
|
821
|
-
|
|
898
|
+
lines100[2]!.includes("\x1b[2m╭─") && lines100[2]!.includes("┬") && lines100[2]!.includes("╮\x1b[22m"),
|
|
899
|
+
"tab box top border = one dim block carrying the ┬ junctions",
|
|
822
900
|
);
|
|
823
901
|
|
|
824
|
-
// Tab regions: Model, Context, Skills, Extensions
|
|
902
|
+
// Tab regions: Model, Context, Skills, Extensions
|
|
825
903
|
const tabY = (hdr as any).renderedTabLineY;
|
|
826
904
|
assert(tabY > 0, "tab line Y position recorded");
|
|
827
905
|
const tabRegions = (hdr as any).renderedTabRegions as Array<{ key: TabKey; startX: number; endX: number }>;
|
|
@@ -841,7 +919,7 @@ if (isMain(import.meta.url)) {
|
|
|
841
919
|
assert(renderRequested === true, "requestRender called when the tab switches");
|
|
842
920
|
|
|
843
921
|
const linesExt = hdr.render(100);
|
|
844
|
-
assert(stripAnsi(linesExt[
|
|
922
|
+
assert(stripAnsi(linesExt[5]!).includes(extRegion.key === "Extensions" ? "@bismawy/pi-agentrouter@1.6.1" : ""), "active tab data on the last info line");
|
|
845
923
|
|
|
846
924
|
// Click the Skills tab (measured at a width where every tab fits)
|
|
847
925
|
const wide = hdr.render(140);
|
|
@@ -860,11 +938,11 @@ if (isMain(import.meta.url)) {
|
|
|
860
938
|
|
|
861
939
|
const linesSkills = hdr.render(140);
|
|
862
940
|
assert(wide.every((l) => visibleWidth(l) === 140), "lines at 140 columns stay 140");
|
|
863
|
-
assert(wideRegions.length === 6, "width 140:
|
|
941
|
+
assert(wideRegions.length === 6, "width 140: all 6 tabs shown (Shortcut included)");
|
|
864
942
|
if (skillsRegion.key === "Skills") {
|
|
865
|
-
assert(linesSkills[
|
|
866
|
-
assert(!linesSkills[
|
|
867
|
-
assert(linesSkills[
|
|
943
|
+
assert(linesSkills[5]!.includes("agents-sdk, cloudflare"), "Skills tab data on the last info line");
|
|
944
|
+
assert(!linesSkills[5]!.includes("Gemini 3.8 Flash (Antigravity)"), "Model data not shown in the Skills tab content");
|
|
945
|
+
assert(linesSkills[3]!.includes("Model [Gemini 3.8 Flash]"), "tab menu still shown while another tab is active");
|
|
868
946
|
}
|
|
869
947
|
|
|
870
948
|
// The Y of a tab must point at the tab menu line, not the line above
|
|
@@ -879,13 +957,13 @@ if (isMain(import.meta.url)) {
|
|
|
879
957
|
((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
880
958
|
"width 170: Shortcut tab shown (not dropped)",
|
|
881
959
|
);
|
|
882
|
-
const scContent = stripAnsi(linesShortcut.slice(
|
|
960
|
+
const scContent = stripAnsi(linesShortcut.slice(5).join(" "));
|
|
883
961
|
assert(scContent.includes("[Esc] Interrupt"), "Shortcut tab: legend moved into the content");
|
|
884
962
|
assert(
|
|
885
963
|
scContent.includes(`[${SHORTCUT_RELOAD.label}] Reload`),
|
|
886
964
|
`Shortcut tab: reload shortcut present in the content (${SHORTCUT_RELOAD.label})`,
|
|
887
965
|
);
|
|
888
|
-
assert(stripAnsi(linesShortcut[
|
|
966
|
+
assert(stripAnsi(linesShortcut[1]!).includes("Arnative v"), "the Arnative brand stays on line 1 while the Shortcut tab is active");
|
|
889
967
|
hdr.activeTab = "Model";
|
|
890
968
|
hdr.render(140);
|
|
891
969
|
|
|
@@ -893,13 +971,13 @@ if (isMain(import.meta.url)) {
|
|
|
893
971
|
const narrow = hdr.render(60);
|
|
894
972
|
assert(narrow.every((l) => visibleWidth(l) === 60), "lines at 60 columns stay 60");
|
|
895
973
|
assert(stripAnsi(narrow[1]!).includes("Welcome back,"), "greeting stays intact at 60 columns");
|
|
896
|
-
assert(stripAnsi(narrow[
|
|
897
|
-
assert(stripAnsi(narrow[
|
|
974
|
+
assert(stripAnsi(narrow[1]!).includes("Arnative v"), "Arnative brand intact at 60 columns");
|
|
975
|
+
assert(stripAnsi(narrow[1]!).includes("Interrupt") === false, "legend does not come back to the top line");
|
|
898
976
|
assert(
|
|
899
977
|
!((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
900
978
|
"rightmost tab dropped at 60 columns",
|
|
901
979
|
);
|
|
902
|
-
assert(stripAnsi(narrow[
|
|
980
|
+
assert(stripAnsi(narrow[3]!).includes("Model ["), "Model tab still shown at 60 columns");
|
|
903
981
|
|
|
904
982
|
// Intercept addChild: capture the data and suppress the child
|
|
905
983
|
const proto = Object.getPrototypeOf(UserMessageComponent.prototype) as { addChild?: unknown };
|
|
@@ -917,13 +995,13 @@ if (isMain(import.meta.url)) {
|
|
|
917
995
|
// pi 0.99 ExpandableText shape: `build` callback + `state` object, no getters.
|
|
918
996
|
const modernSection = {
|
|
919
997
|
state: { expanded: false },
|
|
920
|
-
build: () => "[
|
|
998
|
+
build: () => "[Context]\n AGENTS.md, MEMORY.md",
|
|
921
999
|
setText: () => {},
|
|
922
1000
|
render: (_w: number) => ["should be hidden"],
|
|
923
1001
|
};
|
|
924
1002
|
container.addChild(modernSection);
|
|
925
1003
|
assert(modernSection.render(80).length === 0, "pi 0.99 section suppressed (build + state shape)");
|
|
926
|
-
assert(tabStore.get("
|
|
1004
|
+
assert(tabStore.get("Context")?.includes("MEMORY.md") === true, "pi 0.99 section data lands in tabStore");
|
|
927
1005
|
|
|
928
1006
|
// Startup help block is not a resource section and must survive untouched.
|
|
929
1007
|
const helpBlock = { build: () => "\u2580\u2580\u2588 v0.99.1\n\u2588\u2580 \u2588 escape interrupt", state: { expanded: false }, render: () => ["help"] };
|