@bismawy/pi-arnative 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +88 -0
- package/extensions/footer.ts +48 -67
- package/extensions/section-headers.ts +87 -112
- package/extensions/timestamps.ts +75 -31
- package/extensions/tools.ts +157 -147
- package/extensions/ui-render-tweaks.ts +101 -119
- package/extensions/usage.ts +17 -25
- package/lib/ansi.ts +22 -9
- package/lib/box.ts +8 -13
- package/lib/check.ts +12 -0
- package/lib/format.ts +33 -0
- package/lib/usage-store.ts +3 -1
- package/package.json +2 -1
- package/themes/gen-themes.mjs +38 -38
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.5] - 2026-09-28
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- `timestamps.ts`: missing `themeOf` import crashed `session_start` with "themeOf is not defined" on `/reload` and session resume (0.2.4 regression).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.2.4] - 2026-09-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `lib/check.ts` and `lib/format.ts`: shared self-check helpers (`assert`, `isMain`) and shared display formatting (model names, capitalize, expand-hint key) — footer, header tabs and tool boxes now agree by construction.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- A crashing third-party tool renderer (e.g. pi-web-access `get_content` with missing args) no longer exits pi: every external renderer call is wrapped in a safe fallback, matching pi's own `updateDisplay` semantics.
|
|
17
|
+
- Restored / `--continue` sessions now show the real send time on user messages (timestamps prefilled from session history). A message with no recorded time shows no clock instead of a fake "now".
|
|
18
|
+
- The spinner animates at the documented 500 ms cadence (was 150 ms); footer status stripping no longer carries a broken ANSI-regex leftover.
|
|
19
|
+
- The usage log prefilter no longer silently undercounts if the JSONL key order/shape changes.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Self-check boilerplate, ANSI stripping and model-name formatting were deduplicated into `lib/`; dead compatibility exports removed. Behavior unchanged, `npm test` green.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## [0.2.3] - 2026-09-27
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `Shortcut` header tab (rightmost, after `Themes`) holding the key cheatsheet: `Esc`, `Ctrl+c/d`, `/`, `!`, `Ctrl+o`, `Ctrl+alt+t`, `Ctrl+alt+n`, `Ctrl+alt+r`. Tab order is now `Directory → Model → Context → Skills → Extensions → Themes → Shortcut`.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- The header no longer spends a line on the shortcut legend. Line 1 greets the device username (`Welcome back, <user>`), line 2 shows `pi vX.Y.Z · Arnative vX.Y.Z` (names in the theme accent, versions dim). The tab box labels itself on its top border (`╭─ Menu ───╮`) instead of spending a text line. The Arnative version is read from the package.json of the running build, so an outdated npm install can no longer claim to be the executing code.
|
|
33
|
+
- The `Shortcut` tab is dropped first on narrow terminals (~<146 columns), like every rightmost tab.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## [0.2.2] - 2026-09-27
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- `ctrl+alt+r` reload legend in the header shortcut line (`[Ctrl+alt+r] Reload`), shown from 142 columns on.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
- `Directory` is now the first header tab (and the default tab on open), so the working directory shows immediately. Tab order: `Directory → Model → Context → Skills → Extensions → Themes`.
|
|
44
|
+
- The `Directory` tab label is just `Directory`; the full path moved to the tab content line, shown on click.
|
|
45
|
+
- Model tab label uses brackets: `Model [Deepseek V4.1 Flash]` instead of `Model: Deepseek V4.1 Flash`.
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Click hit-areas for header tabs were off by 2 columns (tab box border + leading space were not counted), so the leftmost tab looked dead on its last 2 columns.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## [0.2.1] - 2026-09-27
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- Fixed `ctrl+alt+r` reload shortcut by publishing missing extension logic to NPM registry.
|
|
56
|
+
- Hoisted reload shortcut key id to typed `Key.ctrlAlt("r")` with encoding assertions for both legacy ESC (`\x1b\x12`) and Kitty CSI-u (`\x1b[114;7u`).
|
|
57
|
+
|
|
58
|
+
### Added
|
|
59
|
+
- Added `npm run dev` script for conflict-free local live TUI testing.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## [0.2.0] - 2026-09-27
|
|
64
|
+
|
|
65
|
+
### Added
|
|
66
|
+
- Interactive header box with clickable resource tabs (`Model`, `Directory`, `Context`, `Skills`, `Extensions`, `Themes`).
|
|
67
|
+
- Full working directory view under the `Directory` tab (wrapped at path segments, never truncated).
|
|
68
|
+
- Keyboard shortcut `ctrl+alt+t` to cycle through header tabs.
|
|
69
|
+
- Keyboard shortcut `ctrl+alt+r` for instant runtime reload (`/reload`).
|
|
70
|
+
- Full `/usage` interactive dashboard modal with session/all-time token metrics and estimated costs.
|
|
71
|
+
- 10 expressive palette variants generated from base Arnative (`sun`, `zinc`, `violet`, `emerald`, `matrix`, `cyberpunk`, `synthwave`, `gruvbox`, `nord`, `dracula`).
|
|
72
|
+
- Visual rounded boxes with animated status spinners for built-in and MCP tool executions.
|
|
73
|
+
- Transcript timestamp clock and message bubble background repair.
|
|
74
|
+
|
|
75
|
+
### Changed
|
|
76
|
+
- Shortened footer working directory display to tail segments.
|
|
77
|
+
- Compacted compaction and status notices into single-line dimmed highlights.
|
|
78
|
+
- Unified theme token contrast check across all variants via automated test suite.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## [0.1.0] - 2026-09-24
|
|
83
|
+
|
|
84
|
+
### Added
|
|
85
|
+
- Initial release of `@bismawy/pi-arnative`.
|
|
86
|
+
- Warm-neutral base Arnative theme with cyan accents.
|
|
87
|
+
- Fixed 2-line status footer with real-time streaming speed (tok/s), Git branch/tag status, and token metrics.
|
|
88
|
+
- Boxed editor input with dim ` > ` prompt.
|
package/extensions/footer.ts
CHANGED
|
@@ -11,6 +11,9 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
|
11
11
|
import { CustomEditor, FooterComponent, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
|
+
import { stripAnsi } from "../lib/ansi.ts";
|
|
15
|
+
import { assert, isMain } from "../lib/check.ts";
|
|
16
|
+
import { capitalize, modelDisplayParts } from "../lib/format.ts";
|
|
14
17
|
import { formatTokens } from "../lib/usage-store.ts";
|
|
15
18
|
|
|
16
19
|
// The TUI drops key-release events before handleInput, so press+release cannot
|
|
@@ -99,19 +102,9 @@ function formatModelName(
|
|
|
99
102
|
dim: (t: string) => string,
|
|
100
103
|
): string {
|
|
101
104
|
if (!model) return dim("No Model");
|
|
102
|
-
|
|
103
|
-
const provider = model.provider ? model.provider.charAt(0).toUpperCase() + model.provider.slice(1) : "";
|
|
104
|
-
if (name === model.id) {
|
|
105
|
-
name = name
|
|
106
|
-
.split(/[-_]/)
|
|
107
|
-
.map((w) => (w.length > 0 ? w.charAt(0).toUpperCase() + w.slice(1) : ""))
|
|
108
|
-
.join(" ");
|
|
109
|
-
}
|
|
110
|
-
if (provider && name.toLowerCase().endsWith(`(${provider.toLowerCase()})`)) {
|
|
111
|
-
name = name.slice(0, name.lastIndexOf("(")).trim();
|
|
112
|
-
}
|
|
105
|
+
const { name, provider } = modelDisplayParts({ id: model.id, name: model.name, provider: model.provider });
|
|
113
106
|
const provStr = provider ? ` ${dim(`(${provider})`)}` : "";
|
|
114
|
-
const capThinking = thinkingLevel && thinkingLevel !== "off" ?
|
|
107
|
+
const capThinking = thinkingLevel && thinkingLevel !== "off" ? capitalize(thinkingLevel) : "";
|
|
115
108
|
const thinkStr = capThinking ? `${acc("\udb80\udf35")} ${tint(capThinking)} ${dim("·")} ` : "";
|
|
116
109
|
return `${thinkStr}${acc(name)}${provStr}`;
|
|
117
110
|
}
|
|
@@ -140,15 +133,12 @@ export function formatDuration(ms: number): string {
|
|
|
140
133
|
return `${s}s`;
|
|
141
134
|
}
|
|
142
135
|
|
|
143
|
-
// Full box editor
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// `visible` = editor content line count (private `renderedVisibleLineCount`);
|
|
150
|
-
// autocomplete lines come AFTER the bottom border and are deliberately outside the box.
|
|
151
|
-
// JetBrainsMono Nerd Font has the round corner glyphs (verified via fontconfig).
|
|
136
|
+
// Full box editor: pi draws only the top/bottom lines (pi-tui editor.js: "no side
|
|
137
|
+
// borders, just horizontal lines above and below") and no prompt char, so the `│`
|
|
138
|
+
// sides, the round ╭╮╰╯ corners and the `> ` prompt are added here. `lines` are
|
|
139
|
+
// already rendered at width - 5 (2 sides + 3 prompt) and the border is patched with
|
|
140
|
+
// dashes to match. `visible` = content line count (private `renderedVisibleLineCount`);
|
|
141
|
+
// autocomplete lines stay outside the box. JetBrainsMono NF has the round glyphs.
|
|
152
142
|
export function boxEditorLines(
|
|
153
143
|
lines: readonly string[],
|
|
154
144
|
visible: number,
|
|
@@ -234,6 +224,14 @@ function poke(): void {
|
|
|
234
224
|
* Tool name → active progress text (gerund).
|
|
235
225
|
* Fallback when unregistered: "Working".
|
|
236
226
|
*/
|
|
227
|
+
// Verb for an MCP sub-action (shared by the `mcp` and `mcp__*` tool names).
|
|
228
|
+
function mcpStatusOf(sub: string): string {
|
|
229
|
+
if (sub.includes("search") || sub.includes("find")) return "Searching";
|
|
230
|
+
if (sub.includes("fetch") || sub.includes("get") || sub.includes("read")) return "Fetching";
|
|
231
|
+
if (sub.includes("edit") || sub.includes("write") || sub.includes("patch")) return "Editing";
|
|
232
|
+
return "Executing";
|
|
233
|
+
}
|
|
234
|
+
|
|
237
235
|
export function getToolWorkingMessage(toolName: string, args?: any): string {
|
|
238
236
|
const n = (toolName ?? "").trim();
|
|
239
237
|
if (!n) return "Working";
|
|
@@ -257,21 +255,11 @@ export function getToolWorkingMessage(toolName: string, args?: any): string {
|
|
|
257
255
|
if (n === "mcpScript") return "Running script";
|
|
258
256
|
if (n === "mcp") {
|
|
259
257
|
const sub = typeof args === "object" && args ? (args.tool || args.search || args.describe || args.action) : "";
|
|
260
|
-
|
|
261
|
-
const s = String(sub).toLowerCase();
|
|
262
|
-
if (s.includes("search") || s.includes("find")) return "Searching";
|
|
263
|
-
if (s.includes("fetch") || s.includes("get") || s.includes("read")) return "Fetching";
|
|
264
|
-
if (s.includes("edit") || s.includes("write") || s.includes("patch")) return "Editing";
|
|
265
|
-
return "Executing";
|
|
266
|
-
}
|
|
267
|
-
return "Executing";
|
|
258
|
+
return sub ? mcpStatusOf(String(sub).toLowerCase()) : "Executing";
|
|
268
259
|
}
|
|
269
260
|
if (n.startsWith("mcp__")) {
|
|
270
261
|
const sub = typeof args === "object" && args?.tool ? String(args.tool).toLowerCase() : "";
|
|
271
|
-
|
|
272
|
-
if (sub.includes("fetch") || sub.includes("get") || sub.includes("read")) return "Fetching";
|
|
273
|
-
if (sub.includes("edit") || sub.includes("write") || sub.includes("patch")) return "Editing";
|
|
274
|
-
return "Executing";
|
|
262
|
+
return mcpStatusOf(sub);
|
|
275
263
|
}
|
|
276
264
|
|
|
277
265
|
return "Working";
|
|
@@ -367,7 +355,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
367
355
|
const segs: string[] = [];
|
|
368
356
|
const cleanStatus = (s: string) => {
|
|
369
357
|
if (s.includes("MCP:")) {
|
|
370
|
-
let clean = s
|
|
358
|
+
let clean = stripAnsi(s);
|
|
371
359
|
const idx = clean.indexOf("MCP:");
|
|
372
360
|
let rest = (idx >= 0 ? clean.slice(idx) : clean).replace(/\uFFFD/g, "").trim();
|
|
373
361
|
rest = rest.replace(/[\p{Extended_Pictographic}\uFE0F\u200D\u2800-\u28FF]/gu, "").replace(/\s{2,}/g, " ").trim();
|
|
@@ -397,7 +385,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
397
385
|
const label = isOff ? "OFF" : isReview ? "REVIEW" : "ON";
|
|
398
386
|
return `${acc("\uedcf")} ${tint("Jev:")} ${bullet} ${tint(label)}`;
|
|
399
387
|
}
|
|
400
|
-
let clean = s
|
|
388
|
+
let clean = stripAnsi(s);
|
|
401
389
|
clean = clean.replace(/\uFFFD/g, "").replace(/\?{1,2}\s*/g, "");
|
|
402
390
|
return tint(clean.trim());
|
|
403
391
|
};
|
|
@@ -612,37 +600,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
612
600
|
}
|
|
613
601
|
|
|
614
602
|
// Self-check: `node extensions/footer.ts`
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
assert(
|
|
624
|
-
assert(
|
|
625
|
-
assert(
|
|
626
|
-
assert(shortenCwd("D:\\Pi\\pi-arnative") === "D:\\Pi\\pi-arnative", "path Windows dangkal tak diubah");
|
|
627
|
-
assert(shortenCwd("/a/x/y/z") === "\u2026/y/z", "path jauh -> tail");
|
|
628
|
-
assert(shortenCwd("") === "", "cwd kosong");
|
|
629
|
-
assert(formatDuration(0) === "-", "durasi kosong = -");
|
|
630
|
-
assert(formatDuration(5_000) === "5s", "detik saja");
|
|
631
|
-
assert(formatDuration(65_000) === "1m 5s", "menit + detik");
|
|
632
|
-
assert(formatDuration(3_700_000) === "1h 1m", "jam + menit");
|
|
603
|
+
if (isMain(import.meta.url)) {
|
|
604
|
+
assert(shortenCwd("/run/media/bisma/DATA/Pi/pi-arnative") === "\u2026/Pi/pi-arnative", "cwd inside -> last 2 segments");
|
|
605
|
+
assert(shortenCwd("/home/bisma") === "/home/bisma", "shallow cwd left unchanged");
|
|
606
|
+
assert(shortenCwd("D:\\Pi\\a\\pi-arnative") === "D:/\u2026/a/pi-arnative", "long Windows path: drive kept");
|
|
607
|
+
assert(shortenCwd("D:\\Pi\\pi-arnative") === "D:\\Pi\\pi-arnative", "shallow Windows path left unchanged");
|
|
608
|
+
assert(shortenCwd("/a/x/y/z") === "\u2026/y/z", "long path -> tail");
|
|
609
|
+
assert(shortenCwd("") === "", "empty cwd");
|
|
610
|
+
assert(formatDuration(0) === "-", "empty duration = -");
|
|
611
|
+
assert(formatDuration(5_000) === "5s", "seconds only");
|
|
612
|
+
assert(formatDuration(65_000) === "1m 5s", "minutes + seconds");
|
|
613
|
+
assert(formatDuration(3_700_000) === "1h 1m", "hours + minutes");
|
|
633
614
|
|
|
634
615
|
const plain = (s: string) => s;
|
|
635
|
-
const D = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"; // 8
|
|
616
|
+
const D = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"; // 8 columns = content width in this test
|
|
636
617
|
const boxed = boxEditorLines([D, " hi ", " lo ", D, " /mo "], 2, plain);
|
|
637
|
-
assert(boxed[0] === `\u256d${D}\u2500\u2500\u2500\u256e`, "
|
|
638
|
-
assert(boxed[1] === "\u2502 > hi \u2502", "
|
|
639
|
-
assert(boxed[2] === "\u2502 lo \u2502", "
|
|
640
|
-
assert(boxed[3] === `\u2570${D}\u2500\u2500\u2500\u256f`, "
|
|
641
|
-
assert(boxed[4] === " /mo ", "
|
|
618
|
+
assert(boxed[0] === `\u256d${D}\u2500\u2500\u2500\u256e`, "editor box: top corner spans the content");
|
|
619
|
+
assert(boxed[1] === "\u2502 > hi \u2502", "editor box: ' > ' prompt on the first line");
|
|
620
|
+
assert(boxed[2] === "\u2502 lo \u2502", "editor box: continuation indented, no second >");
|
|
621
|
+
assert(boxed[3] === `\u2570${D}\u2500\u2500\u2500\u256f`, "editor box: bottom corner");
|
|
622
|
+
assert(boxed[4] === " /mo ", "autocomplete lines stay outside the box");
|
|
642
623
|
const dimP = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
643
624
|
const boxedDim = boxEditorLines([D, " hi ", D], 1, plain, dimP(" > "));
|
|
644
|
-
assert(visibleWidth(boxedDim[1]!) === visibleWidth(D) + 5, "
|
|
645
|
-
assert(boxedDim[1]!.startsWith(`\u2502${dimP(" > ")}`), "
|
|
625
|
+
assert(visibleWidth(boxedDim[1]!) === visibleWidth(D) + 5, "ANSI (dim) prompt does not shift the width");
|
|
626
|
+
assert(boxedDim[1]!.startsWith(`\u2502${dimP(" > ")}`), "dim prompt kept as-is");
|
|
646
627
|
|
|
647
628
|
// Self-check: getToolWorkingMessage
|
|
648
629
|
assert(getToolWorkingMessage("edit") === "Editing", "edit -> Editing");
|
|
@@ -656,14 +637,14 @@ if (isMain) {
|
|
|
656
637
|
assert(getToolWorkingMessage("todo") === "Updating tasks", "todo -> Updating tasks");
|
|
657
638
|
assert(getToolWorkingMessage("unknown_tool") === "Working", "unknown -> Working");
|
|
658
639
|
assert(getToolWorkingMessage("mcp", { tool: "fetch_repo" }) === "Fetching", "mcp fetch -> Fetching");
|
|
659
|
-
assert(boxed.slice(0, 4).every((l) => visibleWidth(l) === visibleWidth(D) + 5), "
|
|
660
|
-
assert(boxEditorLines(["\u2500\u2500\u2500\u2500"], 1, plain).length === 1, "
|
|
640
|
+
assert(boxed.slice(0, 4).every((l) => visibleWidth(l) === visibleWidth(D) + 5), "uniform box width (sides + prompt)");
|
|
641
|
+
assert(boxEditorLines(["\u2500\u2500\u2500\u2500"], 1, plain).length === 1, "too narrow: left unchanged");
|
|
661
642
|
|
|
662
643
|
// Guard: the reload shortcut must match every encoding a terminal can deliver,
|
|
663
644
|
// so a typo in the key id fails here instead of silently doing nothing.
|
|
664
|
-
assert(matchesKey("\x1b\x12", RELOAD_SHORTCUT), "shortcut
|
|
665
|
-
assert(matchesKey("\x1b[114;7u", RELOAD_SHORTCUT), "shortcut
|
|
666
|
-
assert(!matchesKey("\x12", RELOAD_SHORTCUT), "ctrl+r
|
|
667
|
-
assert(!matchesKey("r", RELOAD_SHORTCUT), "
|
|
645
|
+
assert(matchesKey("\x1b\x12", RELOAD_SHORTCUT), "shortcut matches legacy ctrl+alt+r (ESC + 0x12)");
|
|
646
|
+
assert(matchesKey("\x1b[114;7u", RELOAD_SHORTCUT), "shortcut matches Kitty CSI-u ctrl+alt+r");
|
|
647
|
+
assert(!matchesKey("\x12", RELOAD_SHORTCUT), "plain ctrl+r does not trigger reload");
|
|
648
|
+
assert(!matchesKey("r", RELOAD_SHORTCUT), "bare letter r does not trigger reload");
|
|
668
649
|
console.log("footer.ts self-check OK");
|
|
669
650
|
}
|