@bismawy/pi-arnative 0.2.2 → 0.2.3
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/README.md +18 -2
- package/extensions/section-headers.ts +133 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A visual layer and tool styling tailored to your vibe coding.
|
|
|
11
11
|
pi-arnative replaces pi’s default terminal chrome with a unified, cohesive visual aesthetic.
|
|
12
12
|
|
|
13
13
|
- Warm Themes: Cohesive, contrast-checked color palettes generated without drift.
|
|
14
|
-
- Tabbed Header:
|
|
14
|
+
- Tabbed Header: Greeting, version line, and a compact box menu over Directory, Model, Context, Skills, Extensions, Themes, and Shortcut.
|
|
15
15
|
- Rounded Tool Boxes: Clear status, spinner, and duration for every tool call.
|
|
16
16
|
- Enhanced Footer: 2-line layout showing directory path, git branch, model, and token metrics.
|
|
17
17
|
- Transcript Clock: Timestamps for messages with clean bubble backgrounds.
|
|
@@ -36,11 +36,27 @@ pi --extension ./extensions/footer.ts
|
|
|
36
36
|
| Key / Action | Result |
|
|
37
37
|
| --- | --- |
|
|
38
38
|
| `/usage` | Show session usage & token metrics |
|
|
39
|
-
| `ctrl+alt+t` | Cycle header tabs (Directory → Model → Context → Skills → Extensions → Themes) |
|
|
39
|
+
| `ctrl+alt+t` | Cycle header tabs (Directory → Model → Context → Skills → Extensions → Themes → Shortcut) |
|
|
40
|
+
| `ctrl+alt+n` | Start a new session (`/new`) — see [Keybindings](#keybindings) |
|
|
40
41
|
| `ctrl+alt+r` | Quick reload runtime (`/reload`) |
|
|
41
42
|
| Click tab | Open selected header tab |
|
|
42
43
|
| Click tool / `ctrl+e` | Toggle tool output box |
|
|
43
44
|
|
|
45
|
+
Same cheatsheet in the header's **Shortcut** tab; on narrow terminals the rightmost tabs drop off first.
|
|
46
|
+
|
|
47
|
+
## Keybindings
|
|
48
|
+
|
|
49
|
+
`ctrl+alt+n` is a plain user keybinding, not a package shortcut — pi-arnative never registers it.
|
|
50
|
+
Add it once to `~/.pi/agent/keybindings.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"app.session.new": "ctrl+alt+n"
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Restart pi to pick it up.
|
|
59
|
+
|
|
44
60
|
## Architecture
|
|
45
61
|
|
|
46
62
|
<details>
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* Arnative header box & horizontal tab system:
|
|
3
3
|
* - Round-corner box, fixed height 5 lines: "pi" 3x6 logo on the left, a vertical
|
|
4
4
|
* divider, then 3 left-aligned info lines (version/shortcuts, tabs, data).
|
|
5
|
-
* - Info line 1:
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - Info line 1: "Welcome back, <device username>" (os.userInfo, env fallback). Line 2:
|
|
6
|
+
* pi + Arnative brand with versions. The tab box labels itself on its top border
|
|
7
|
+
* ("╭─ Menu ───╮").
|
|
8
|
+
* - Interactive tab menu (mouse click): Model + Context/Skills/Extensions/Themes/Shortcut
|
|
9
|
+
* with item counts. Tab icon = accent, tab text = tint. The key cheatsheet lives in
|
|
10
|
+
* the Shortcut tab, not in a header line.
|
|
8
11
|
* - Box divider '├────┤'.
|
|
9
12
|
* - Active tab content (left-aligned, neatly wrapped, tint, never stacked).
|
|
10
13
|
* - Hides pi's built-in stacked list in loadedResourcesContainer.
|
|
11
14
|
*/
|
|
12
15
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
13
|
-
import {
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { tmpdir, userInfo } from "node:os";
|
|
14
18
|
import { join } from "node:path";
|
|
15
19
|
import {
|
|
16
20
|
UserMessageComponent,
|
|
@@ -31,10 +35,10 @@ import {
|
|
|
31
35
|
import { renderBoxLines } from "../lib/box.ts";
|
|
32
36
|
import { formatTokens, getModelAllTimeUsage } from "../lib/usage-store.ts";
|
|
33
37
|
|
|
34
|
-
export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "Themes";
|
|
38
|
+
export type TabKey = "Model" | "Directory" | "Context" | "Skills" | "Extensions" | "Themes" | "Shortcut";
|
|
35
39
|
|
|
36
40
|
// Tab order for Ctrl+Alt+T and for the menu line.
|
|
37
|
-
export const TAB_ORDER: TabKey[] = ["Directory", "Model", "Context", "Skills", "Extensions", "Themes"];
|
|
41
|
+
export const TAB_ORDER: TabKey[] = ["Directory", "Model", "Context", "Skills", "Extensions", "Themes", "Shortcut"];
|
|
38
42
|
|
|
39
43
|
export const SECTION_ICONS: Record<TabKey, string> = {
|
|
40
44
|
Model: "\uf1b2",
|
|
@@ -43,6 +47,7 @@ export const SECTION_ICONS: Record<TabKey, string> = {
|
|
|
43
47
|
Skills: "\uec21",
|
|
44
48
|
Extensions: "\ueea8",
|
|
45
49
|
Themes: "\uee72",
|
|
50
|
+
Shortcut: "\uf11c", // nf-fa-keyboard_o
|
|
46
51
|
};
|
|
47
52
|
|
|
48
53
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
@@ -88,6 +93,21 @@ export function packageNameOf(label: string): string | null {
|
|
|
88
93
|
return /^[\w.@/-]+$/.test(base) ? base : null;
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
/**
|
|
97
|
+
* OS account name of whoever is running pi, for the header greeting. Falls back to the
|
|
98
|
+
* usual env vars, then to a neutral word so the line is never empty.
|
|
99
|
+
*/
|
|
100
|
+
export function deviceUser(): string {
|
|
101
|
+
try {
|
|
102
|
+
const u = userInfo().username;
|
|
103
|
+
if (u) return u;
|
|
104
|
+
} catch {
|
|
105
|
+
/* no passwd entry (some containers) — fall through to env */
|
|
106
|
+
}
|
|
107
|
+
const u = process.env.USER ?? process.env.USERNAME ?? "";
|
|
108
|
+
return u || "there";
|
|
109
|
+
}
|
|
110
|
+
|
|
91
111
|
const versionCache = new Map<string, string | null>();
|
|
92
112
|
|
|
93
113
|
export function readVersion(dir: string): string | null {
|
|
@@ -122,6 +142,11 @@ export function installedVersion(
|
|
|
122
142
|
return version;
|
|
123
143
|
}
|
|
124
144
|
|
|
145
|
+
// Arnative build actually running: package.json one level above this extension file.
|
|
146
|
+
// Reading the module's own package beats reading the npm-installed copy, which lags
|
|
147
|
+
// behind whenever the local clone (or a hand-copied extensions/) is what pi loaded.
|
|
148
|
+
const ARNATIVE_VERSION = readVersion(fileURLToPath(new URL("..", import.meta.url)));
|
|
149
|
+
|
|
125
150
|
export function extractItemsFromBody(body: string, isExtensions = false): string[] {
|
|
126
151
|
const plain = stripAnsi(body).trim();
|
|
127
152
|
if (!plain) return [];
|
|
@@ -145,16 +170,33 @@ export function extractItemsFromBody(body: string, isExtensions = false): string
|
|
|
145
170
|
});
|
|
146
171
|
}
|
|
147
172
|
|
|
173
|
+
// Cheatsheet for the Shortcut tab. Mirrors README "Shortcuts" plus the bindings the
|
|
174
|
+
// extensions themselves own (footer.ts). Kept as plain text so the data line can wrap it.
|
|
175
|
+
export const SHORTCUTS: string[] = [
|
|
176
|
+
"[Esc] Interrupt",
|
|
177
|
+
"[Ctrl+c/d] Exit",
|
|
178
|
+
"[/] Commands",
|
|
179
|
+
"[!] Bash",
|
|
180
|
+
"[Ctrl+o] More/expand",
|
|
181
|
+
"[Ctrl+alt+t] Next tab",
|
|
182
|
+
"[Ctrl+alt+n] New",
|
|
183
|
+
"[Ctrl+alt+r] Reload",
|
|
184
|
+
];
|
|
185
|
+
|
|
148
186
|
export function wrapCommaItems(items: string[], maxWidth: number): string[] {
|
|
187
|
+
return wrapTokens(items, maxWidth, ", ", ",");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function wrapTokens(items: string[], maxWidth: number, sep = " ", trailing = ""): string[] {
|
|
149
191
|
const lines: string[] = [];
|
|
150
192
|
let cur = "";
|
|
151
193
|
for (const item of items) {
|
|
152
194
|
if (!cur) {
|
|
153
195
|
cur = item;
|
|
154
|
-
} else if (visibleWidth(cur) +
|
|
155
|
-
cur +=
|
|
196
|
+
} else if (visibleWidth(cur) + visibleWidth(sep) + visibleWidth(item) <= maxWidth) {
|
|
197
|
+
cur += sep + item;
|
|
156
198
|
} else {
|
|
157
|
-
lines.push(cur +
|
|
199
|
+
lines.push(cur + trailing);
|
|
158
200
|
cur = item;
|
|
159
201
|
}
|
|
160
202
|
}
|
|
@@ -350,6 +392,13 @@ export class ArnativeHeader implements Component {
|
|
|
350
392
|
count: tabStore.get("Themes")?.length ?? 0,
|
|
351
393
|
label: `Themes [${tabStore.get("Themes")?.length ?? 0}]`,
|
|
352
394
|
},
|
|
395
|
+
{
|
|
396
|
+
key: "Shortcut",
|
|
397
|
+
name: "Shortcut",
|
|
398
|
+
icon: SECTION_ICONS.Shortcut,
|
|
399
|
+
count: SHORTCUTS.length,
|
|
400
|
+
label: `Shortcut [${SHORTCUTS.length}]`,
|
|
401
|
+
},
|
|
353
402
|
];
|
|
354
403
|
}
|
|
355
404
|
|
|
@@ -404,28 +453,17 @@ export class ArnativeHeader implements Component {
|
|
|
404
453
|
const infoLines: string[] = [];
|
|
405
454
|
// Inner box: the tab menu line is framed, content is (infoW - 2) columns.
|
|
406
455
|
const innerW = Math.max(1, infoW - 2);
|
|
456
|
+
const brand = (name: string) => fgFirst(th, ["accent", "tint"], `\x1b[1m${name}\x1b[22m`);
|
|
457
|
+
const dim = (text: string) => fgFirst(th, ["dim"], text);
|
|
458
|
+
|
|
459
|
+
// 1. Greeting: device username, accent like the brand names below it.
|
|
460
|
+
infoLines.push(`${dim("Welcome back,")} ${brand(deviceUser())}`);
|
|
407
461
|
|
|
408
|
-
//
|
|
462
|
+
// 2. Brand line: pi · Arnative, both in the theme accent, versions dim.
|
|
409
463
|
const versionStr = VERSION || "0.87.1";
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`${fgFirst(th, ["tint"], "[Esc]")} ${fgFirst(th, ["dim"], "Interrupt")}`,
|
|
414
|
-
`${fgFirst(th, ["tint"], "[Ctrl+c/d]")} ${fgFirst(th, ["dim"], "Exit")}`,
|
|
415
|
-
`${fgFirst(th, ["tint"], "[/]")} ${fgFirst(th, ["dim"], "Commands")}`,
|
|
416
|
-
`${fgFirst(th, ["tint"], "[!]")} ${fgFirst(th, ["dim"], "Bash")}`,
|
|
417
|
-
`${fgFirst(th, ["tint"], "[Ctrl+o]")} ${fgFirst(th, ["dim"], "More/expand")}`,
|
|
418
|
-
`${fgFirst(th, ["tint"], "[Ctrl+alt+t]")} ${fgFirst(th, ["dim"], "Next tab")}`,
|
|
419
|
-
`${fgFirst(th, ["tint"], "[Ctrl+alt+r]")} ${fgFirst(th, ["dim"], "Reload")}`,
|
|
420
|
-
];
|
|
421
|
-
// Shortcut list is trimmed from the right (longest prefix that still fits) so
|
|
422
|
-
// narrow screens never clip the tabs or the data.
|
|
423
|
-
let scText = "";
|
|
424
|
-
for (let i = 1; i <= shortcutItems.length; i++) {
|
|
425
|
-
const cand = shortcutItems.slice(0, i).join(" ");
|
|
426
|
-
if (visibleWidth(cand) <= infoW) scText = cand;
|
|
427
|
-
}
|
|
428
|
-
infoLines.push(scText);
|
|
464
|
+
infoLines.push(
|
|
465
|
+
`${brand("pi")} ${dim(`v${versionStr}`)}${dim(" · ")}${brand("Arnative")}${ARNATIVE_VERSION ? ` ${dim(`v${ARNATIVE_VERSION}`)}` : ""}`,
|
|
466
|
+
);
|
|
429
467
|
|
|
430
468
|
// 2. Tab menu: icon = accent, text = tint (bold when active)
|
|
431
469
|
const tabs = this.getTabsData(width);
|
|
@@ -470,8 +508,16 @@ export class ArnativeHeader implements Component {
|
|
|
470
508
|
const t = truncateToWidth(text, tabInnerW);
|
|
471
509
|
return `${side}${t}${" ".repeat(tabInnerW - visibleWidth(t))}${side}`;
|
|
472
510
|
};
|
|
473
|
-
const tabRule = (l: string, r: string): string =>
|
|
474
|
-
|
|
511
|
+
const tabRule = (l: string, r: string, label = ""): string => {
|
|
512
|
+
// Label rides on the top border ("╭─ Menu ───╮") so no extra text line is spent; the
|
|
513
|
+
// word is tint so it reads as a label instead of a dimmer stretch of border.
|
|
514
|
+
const lead = label.length + 1;
|
|
515
|
+
if (label && tabInnerW - lead > 1) {
|
|
516
|
+
return `${fgFirst(th, ["dim"], `${l}─`)}${fgFirst(th, ["tint", "text"], label)}${fgFirst(th, ["dim"], `${"─".repeat(tabInnerW - lead)}${r}`)}`;
|
|
517
|
+
}
|
|
518
|
+
return fgFirst(th, ["dim"], `${l}${"─".repeat(tabInnerW)}${r}`);
|
|
519
|
+
};
|
|
520
|
+
infoLines.push(tabRule("╭", "╮", " Menu "));
|
|
475
521
|
// One space from the left border, then " │ " between tabs.
|
|
476
522
|
infoLines.push(tabRow(tabText));
|
|
477
523
|
// Tab line = top border (index 0) + the infoLines above + this line.
|
|
@@ -516,6 +562,10 @@ export class ArnativeHeader implements Component {
|
|
|
516
562
|
for (const line of wrapPath(getCwd(this.ctx), infoW)) {
|
|
517
563
|
infoLines.push(fgFirst(th, ["tint", "text"], line));
|
|
518
564
|
}
|
|
565
|
+
} else if (this.activeTab === "Shortcut") {
|
|
566
|
+
for (const line of wrapTokens(SHORTCUTS, infoW)) {
|
|
567
|
+
infoLines.push(fgFirst(th, ["tint", "text"], line));
|
|
568
|
+
}
|
|
519
569
|
} else if (activeItems.length === 0) {
|
|
520
570
|
infoLines.push(fgFirst(th, ["tint"], "(kosong)"));
|
|
521
571
|
} else if (this.activeTab === "Model") {
|
|
@@ -728,7 +778,27 @@ if (isMain) {
|
|
|
728
778
|
lines100.slice(1, 7).every((l, i) => stripAnsi(l).slice(2, 15) === logoPlain[i]),
|
|
729
779
|
"ascii Pi asli di kolom kiri, 6 baris",
|
|
730
780
|
);
|
|
731
|
-
assert(
|
|
781
|
+
assert(
|
|
782
|
+
/Welcome back, \S/.test(stripAnsi(lines100[1]!)) && !stripAnsi(lines100[1]!).includes("pi v"),
|
|
783
|
+
"baris 1: sapaan pakai username device",
|
|
784
|
+
);
|
|
785
|
+
assert(
|
|
786
|
+
stripAnsi(lines100[2]!).includes("pi v") && /Arnative v\d+\.\d+\.\d+/.test(stripAnsi(lines100[2]!)),
|
|
787
|
+
"baris 2: pi + Arnative + versi build Arnative",
|
|
788
|
+
);
|
|
789
|
+
assert(/╭─ Menu ─+╮/.test(stripAnsi(lines100[3]!)), "border atas kotak tab memuat label Menu");
|
|
790
|
+
assert(!stripAnsi(lines100[1]!).includes("Interrupt"), "legend pintasan tidak lagi di baris atas");
|
|
791
|
+
// Both brand names use the theme accent, not only "pi".
|
|
792
|
+
const accentTheme = {
|
|
793
|
+
fg: (c: string, t: string) => (c === "accent" ? `<A>${t}</A>` : c === "dim" ? `<D>${t}</D>` : t),
|
|
794
|
+
};
|
|
795
|
+
const accentHeader = new ArnativeHeader(fakeTui, accentTheme as any, fakeCtx);
|
|
796
|
+
const accentLine = accentHeader.render(100)[2]!;
|
|
797
|
+
assert(accentLine.includes("<A>\x1b[1mpi"), "nama pi pakai aksen tema");
|
|
798
|
+
assert(accentLine.includes("<A>\x1b[1mArnative"), "nama Arnative pakai aksen tema yang sama");
|
|
799
|
+
const greetLine = accentHeader.render(100)[1]!;
|
|
800
|
+
assert(greetLine.includes("<D>Welcome back,"), "kata sapaan pakai warna dim");
|
|
801
|
+
assert(greetLine.includes(`<A>\x1b[1m${deviceUser()}`), "username device pakai aksen tema");
|
|
732
802
|
assert(stripAnsi(lines100[4]!).includes("Model [Gemini 3.8 Flash]"), "menu tab Model tampil (Model [nama])");
|
|
733
803
|
// Directory tab: name only in the menu, whole path in the content line below
|
|
734
804
|
hdr.activeTab = "Directory";
|
|
@@ -747,10 +817,13 @@ if (isMain) {
|
|
|
747
817
|
assert(dirContent.includes("pi-arnative"), "path lengkap tab Directory (akhir) tampil — tanpa truncate");
|
|
748
818
|
hdr.activeTab = "Model";
|
|
749
819
|
hdr.render(100);
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
820
|
+
// Inner box rules: bottom line is one fully dimmed block (the "─" runs are colored too, not
|
|
821
|
+
// just the corners); the top one is the same except it carries the tinted " Menu " label.
|
|
822
|
+
assert((lines100[5]!.match(/\x1b\[/g) ?? []).length === 8, 'garis kotak bawah (baris 5) = satu blok dim penuh');
|
|
823
|
+
assert(
|
|
824
|
+
lines100[3]!.includes("\x1b[2m╭─\x1b[22m Menu \x1b[2m─") && lines100[3]!.includes("╮\x1b[22m"),
|
|
825
|
+
'border atas kotak tab = dua blok dim dengan label " Menu " di antaranya',
|
|
826
|
+
);
|
|
754
827
|
|
|
755
828
|
// Tab regions: Model, Context, Skills, Extensions, Themes
|
|
756
829
|
const tabY = (hdr as any).renderedTabLineY;
|
|
@@ -791,23 +864,42 @@ if (isMain) {
|
|
|
791
864
|
|
|
792
865
|
const linesSkills = hdr.render(140);
|
|
793
866
|
assert(wide.every((l) => visibleWidth(l) === 140), "baris 140 kolom tetap 140");
|
|
794
|
-
assert(wideRegions.length === 6, "lebar 140:
|
|
867
|
+
assert(wideRegions.length === 6, "lebar 140: 6 tab pertama tampil (Shortcut di paling kanan ikut terbuang)");
|
|
795
868
|
if (skillsRegion.key === "Skills") {
|
|
796
869
|
assert(linesSkills[6]!.includes("agents-sdk, cloudflare"), "data tab Skills tampil di baris info terakhir");
|
|
797
870
|
assert(!linesSkills[6]!.includes("Gemini 3.8 Flash (Antigravity)"), "data Model tidak tampil di konten tab Skills");
|
|
798
871
|
assert(linesSkills[4]!.includes("Model [Gemini 3.8 Flash]"), "menu tab tetap tampil saat tab lain aktif");
|
|
799
872
|
}
|
|
800
873
|
|
|
801
|
-
// The Y of a tab must point at the tab menu line, not the
|
|
874
|
+
// The Y of a tab must point at the tab menu line, not the line above
|
|
802
875
|
const rowsPlain = lines100.map((l) => stripAnsi(l));
|
|
803
876
|
assert(rowsPlain[tabY]!.includes("Model ["), "renderedTabLineY menunjuk baris menu tab");
|
|
804
877
|
assert(tabRegions.every((r) => rowsPlain[tabY]!.slice(r.startX, r.endX + 1).includes(r.key === "Model" ? "Model [" : r.key)), "region tab rata dengan teks di baris itu");
|
|
805
878
|
|
|
806
|
-
//
|
|
879
|
+
// Shortcut tab: the key cheatsheet lives in the tab content, not in a header line
|
|
880
|
+
hdr.activeTab = "Shortcut";
|
|
881
|
+
const linesShortcut = hdr.render(170);
|
|
882
|
+
assert(
|
|
883
|
+
((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
884
|
+
"lebar 170: tab Shortcut tampil (tidak terbuang)",
|
|
885
|
+
);
|
|
886
|
+
const scContent = stripAnsi(linesShortcut.slice(6).join(" "));
|
|
887
|
+
assert(scContent.includes("[Esc] Interrupt"), "tab Shortcut: legend pindah ke konten");
|
|
888
|
+
assert(scContent.includes("[Ctrl+alt+r] Reload"), "tab Shortcut: pintasan reload ada di konten");
|
|
889
|
+
assert(stripAnsi(linesShortcut[2]!).includes("Arnative v"), "baris 2 tetap brand Arnative saat tab Shortcut aktif");
|
|
890
|
+
hdr.activeTab = "Model";
|
|
891
|
+
hdr.render(140);
|
|
892
|
+
|
|
893
|
+
// Narrow screen: brand lines + tabs + data stay intact, rightmost tabs are dropped
|
|
807
894
|
const narrow = hdr.render(60);
|
|
808
895
|
assert(narrow.every((l) => visibleWidth(l) === 60), "baris 60 kolom tetap 60");
|
|
809
|
-
assert(stripAnsi(narrow[1]!).includes("
|
|
810
|
-
assert(stripAnsi(narrow[
|
|
896
|
+
assert(stripAnsi(narrow[1]!).includes("Welcome back,"), "sapaan tetap utuh di 60 kolom");
|
|
897
|
+
assert(stripAnsi(narrow[2]!).includes("Arnative v"), "brand pi + Arnative utuh di 60 kolom");
|
|
898
|
+
assert(stripAnsi(narrow[2]!).includes("Interrupt") === false, "legend tidak balik ke baris atas");
|
|
899
|
+
assert(
|
|
900
|
+
!((hdr as any).renderedTabRegions as Array<{ key: TabKey }>).some((r) => r.key === "Shortcut"),
|
|
901
|
+
"tab paling kanan dibuang di 60 kolom",
|
|
902
|
+
);
|
|
811
903
|
assert(stripAnsi(narrow[4]!).includes("Model ["), "tab Model tetap tampil di 60 kolom");
|
|
812
904
|
|
|
813
905
|
// Intercept addChild: capture the data and suppress the child
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Arnative look for Pi: 11 warm-neutral themes, rounded tool boxes, branded header with tabs, 2-line footer with git/model/status, transcript clock, and a /usage dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|