@bismawy/pi-arnative 0.2.11 → 0.2.12
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 +5 -0
- package/README.md +2 -2
- package/extensions/footer.ts +80 -82
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.12] - 2026-10-02
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- The footer is a fixed 3-row grid: row 1 pairs the cwd line with the model, rows 2-3 pair the extension status (mcp first) with the metrics (tok/s · cache · token) — each side wraps inside its half of the terminal, the right cells stay right-aligned, and a row where both sides ran out is dropped. Replaces the adaptive 2→3 reflow; no click toggle (`extensions/footer.ts`, `layoutFooterGrid()` + `pairRow()`).
|
|
7
|
+
|
|
3
8
|
## [0.2.11] - 2026-10-01
|
|
4
9
|
|
|
5
10
|
### Changed
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ pi-arnative replaces pi’s default terminal chrome with a unified, cohesive vis
|
|
|
15
15
|
- Warm Themes: Cohesive, contrast-checked color palettes generated without drift.
|
|
16
16
|
- Tabbed Header: Greeting, version line, and a compact box menu over Directory, Model, Context, Skills, Extensions, Themes, and Shortcut.
|
|
17
17
|
- Rounded Tool Boxes: Clear status, spinner, and duration for every tool call.
|
|
18
|
-
- Enhanced Footer:
|
|
18
|
+
- Enhanced Footer: fixed 3-row grid — directory/git line, then extension status paired with token metrics.
|
|
19
19
|
- Transcript Clock: Timestamps for messages with clean bubble backgrounds.
|
|
20
20
|
- Usage Dashboard: /usage command to track tokens and session costs.
|
|
21
21
|
|
|
@@ -68,7 +68,7 @@ Add to `~/.pi/agent/keybindings.json` to bind `app.session.new`:
|
|
|
68
68
|
| --- | --- |
|
|
69
69
|
| `extensions/tools.ts` | Rounded tool boxes, spinners, and duration |
|
|
70
70
|
| `extensions/section-headers.ts` | Tabbed header and resource box |
|
|
71
|
-
| `extensions/footer.ts` |
|
|
71
|
+
| `extensions/footer.ts` | 3-row status grid footer and boxed editor |
|
|
72
72
|
| `extensions/timestamps.ts` | Message clock and bubble background fixes |
|
|
73
73
|
| `extensions/ui-render-tweaks.ts` | Contrast tweaks, selection style, and UI polish |
|
|
74
74
|
| `extensions/usage.ts` | `/usage` token dashboard |
|
package/extensions/footer.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Arnative footer (
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Arnative footer (fixed 3-row grid, no click toggle).
|
|
3
|
+
* Row 1: cwd | duration | branch/tag/status ...... model + thinking level.
|
|
4
|
+
* Rows 2-3: extension status (mcp first) ......... tok/s · cache · token,
|
|
5
|
+
* each side wrapped inside its half of the width and paired row by row.
|
|
5
6
|
*
|
|
6
7
|
* Transcript clock & bubble bg: extensions/timestamps.ts
|
|
7
8
|
* /new header: extensions/section-headers.ts
|
|
@@ -177,59 +178,64 @@ export function boxEditorLines(
|
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* of crowding the statuses. `maxRows` caps the growth — a group wider than the
|
|
187
|
-
* terminal is never split, the caller truncates it.
|
|
181
|
+
* The footer as a fixed 3-row grid. Row 1 pairs the head cells (cwd line, model);
|
|
182
|
+
* rows 2-3 pair the wrapped left cells (extension status, mcp first) with the
|
|
183
|
+
* wrapped right cells (tok/s · cache · token), each side wrapping inside its half
|
|
184
|
+
* of the terminal so the columns never collide. Right cells stay right-aligned.
|
|
185
|
+
* A side that runs out of cells leaves its half blank, and a row where both sides
|
|
186
|
+
* ran out is dropped, so a quiet footer stays 1-3 rows tall.
|
|
188
187
|
*/
|
|
189
|
-
export function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
export function layoutFooterGrid(
|
|
189
|
+
headLeft: string,
|
|
190
|
+
headRight: string,
|
|
191
|
+
left: readonly string[],
|
|
192
|
+
right: readonly string[],
|
|
193
|
+
leftSep: string,
|
|
194
|
+
rightSep: string,
|
|
193
195
|
width: number,
|
|
194
|
-
rows: number,
|
|
195
|
-
maxRows = Number.MAX_SAFE_INTEGER,
|
|
196
196
|
vw: (s: string) => number = visibleWidth,
|
|
197
197
|
): string[] {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
198
|
+
const half = Math.max(1, Math.floor(width / 2) - 1);
|
|
199
|
+
// Wrap each side at half width; the first break happens at most once (rows 2-3),
|
|
200
|
+
// after that the rest stays on the last row and the pairing truncates it.
|
|
201
|
+
const wrap = (cells: readonly string[], sep: string): string[] => {
|
|
202
|
+
const rows: string[] = [];
|
|
203
|
+
let line = "";
|
|
204
|
+
for (const cell of cells) {
|
|
205
|
+
const candidate = line ? line + sep + cell : cell;
|
|
206
|
+
if (line && rows.length === 0 && vw(candidate) > half) {
|
|
207
|
+
rows.push(line);
|
|
208
|
+
line = cell;
|
|
209
|
+
} else {
|
|
210
|
+
line = candidate;
|
|
211
|
+
}
|
|
211
212
|
}
|
|
213
|
+
if (line) rows.push(line);
|
|
214
|
+
return rows;
|
|
215
|
+
};
|
|
216
|
+
const leftRows = wrap(left, leftSep);
|
|
217
|
+
const rightRows = wrap(right, rightSep);
|
|
218
|
+
const out = [pairRow(headLeft, headRight, width, vw)];
|
|
219
|
+
for (let i = 0; i < Math.max(leftRows.length, rightRows.length); i++) {
|
|
220
|
+
const l = leftRows[i];
|
|
221
|
+
const r = rightRows[i];
|
|
222
|
+
if (l === undefined && r === undefined) break;
|
|
223
|
+
out.push(pairRow(l ?? "", r ?? "", width, vw));
|
|
212
224
|
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
213
227
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} else if (out.length < budget) {
|
|
222
|
-
// `budget` is the only thing `rows` controls: 2 lets the metrics take a
|
|
223
|
-
// line of their own instead of crowding the groups, 1 packs everything.
|
|
224
|
-
out.push(line);
|
|
225
|
-
line = pad("");
|
|
226
|
-
} else {
|
|
227
|
-
line = line + sep + metrics;
|
|
228
|
-
}
|
|
228
|
+
/** One grid row: `left` at the start, `right` flush to the right edge. */
|
|
229
|
+
function pairRow(left: string, right: string, width: number, vw: (s: string) => number): string {
|
|
230
|
+
if (!right) return truncateToWidth(left, width);
|
|
231
|
+
const rightWidth = Math.min(vw(right), width);
|
|
232
|
+
if (!left) return " ".repeat(width - rightWidth) + truncateToWidth(right, width);
|
|
233
|
+
if (vw(left) + 1 + rightWidth <= width) {
|
|
234
|
+
return left + " ".repeat(width - vw(left) - rightWidth) + right;
|
|
229
235
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return
|
|
236
|
+
// Too wide together: the right cell keeps its columns, the left one is cut.
|
|
237
|
+
const room = Math.max(1, width - rightWidth - 1);
|
|
238
|
+
return truncateToWidth(left, room) + " " + truncateToWidth(right, width - room - 1);
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
function parseOptimizer(raw: string | undefined): string | null {
|
|
@@ -458,8 +464,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
458
464
|
left1 = `${acc("\uf07b")} ${dim(cwdShort)}${sep}${pDuration}${sep}${pBranch}${sep}${pTag}${sep}${pState}`;
|
|
459
465
|
}
|
|
460
466
|
const right1 = formatModelName(currentModel, currentThinkingLevel, acc, tint, dim);
|
|
461
|
-
const pad1 = " ".repeat(Math.max(1, width - visibleWidth(left1) - visibleWidth(right1)));
|
|
462
|
-
const lines = [truncateToWidth(left1 + pad1 + right1, width)];
|
|
463
467
|
|
|
464
468
|
const statuses: ReadonlyMap<string, string> = (() => {
|
|
465
469
|
try {
|
|
@@ -521,19 +525,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
521
525
|
// fallback
|
|
522
526
|
}
|
|
523
527
|
const speedStr = latestSpeed !== null && latestSpeed > 0 ? `${acc("\udb81\udcc5")} ${tint(`${latestSpeed.toFixed(1)} tok/s`)}` : "";
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
for (let rows = 2; ; rows++) {
|
|
530
|
-
const wrapped = layoutStatusLines(groups, metrics, sep, width, rows, MAX_ROWS);
|
|
531
|
-
if (rows >= MAX_ROWS || wrapped.every((l) => visibleWidth(l) <= width)) {
|
|
532
|
-
for (const l of wrapped) lines.push(truncateToWidth(l, width));
|
|
533
|
-
break;
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
return lines;
|
|
528
|
+
const metricCells = [speedStr, opt ? `${acc("\udb80\udf5b")} ${tint(opt)}` : "", usageStr].filter(Boolean);
|
|
529
|
+
|
|
530
|
+
// Fixed 3-row grid: head (cwd/model), then statuses and metrics each
|
|
531
|
+
// wrapped inside their half of the width — no click toggle.
|
|
532
|
+
return layoutFooterGrid(left1, right1, groups, metricCells, sep, ` ${dim("·")} `, width);
|
|
537
533
|
},
|
|
538
534
|
};
|
|
539
535
|
});
|
|
@@ -724,26 +720,28 @@ if (isMain(import.meta.url)) {
|
|
|
724
720
|
assert(formatDuration(65_000) === "1m 5s", "minutes + seconds");
|
|
725
721
|
assert(formatDuration(3_700_000) === "1h 1m", "hours + minutes");
|
|
726
722
|
|
|
727
|
-
// Self-check: footer
|
|
728
|
-
|
|
723
|
+
// Self-check: the footer grid — head row, then statuses/metrics paired per row.
|
|
724
|
+
// truncateToWidth appends ANSI resets, so measure through stripAnsi (the real
|
|
725
|
+
// renderer measures with visibleWidth, which ignores them the same way).
|
|
726
|
+
const vw = (s: string) => stripAnsi(s).length;
|
|
729
727
|
const g = (n: number) => "g".repeat(n);
|
|
730
|
-
//
|
|
731
|
-
const
|
|
732
|
-
assert(
|
|
733
|
-
assert(
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
assert(
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
assert(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
);
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
assert(
|
|
728
|
+
// Head row: cwd line left, model right-aligned; groups and metrics pair below.
|
|
729
|
+
const grid = layoutFooterGrid("H".repeat(10), "M".repeat(6), [g(10), g(10), g(10)], ["s".repeat(10), "u".repeat(10)], " | ", " · ", 40, vw);
|
|
730
|
+
assert(grid.length === 3, "grid: head + two paired rows");
|
|
731
|
+
assert(grid[0]!.startsWith("H".repeat(10)) && grid[0]!.endsWith("M".repeat(6)), "grid: head pairs cwd and model");
|
|
732
|
+
assert(grid[0]!.length === 40, "grid: head right-aligns the model");
|
|
733
|
+
assert(grid[1]!.includes("g") && grid[1]!.endsWith("s".repeat(10)), "grid: row 2 pairs statuses and metrics");
|
|
734
|
+
assert(grid.every((l) => vw(l) <= 40), "grid: every row fits the terminal");
|
|
735
|
+
// Each side wraps inside its half: 3 groups + 2 metrics at width 20 -> 3 rows.
|
|
736
|
+
const tight = layoutFooterGrid("h", "m", [g(6), g(6), g(6)], [g(6), g(6)], " | ", " · ", 20, vw);
|
|
737
|
+
assert(tight.length === 3, "grid: wraps inside the half width");
|
|
738
|
+
assert(tight.every((l) => vw(l) <= 20), "grid: wrapped rows still fit");
|
|
739
|
+
// The right cell survives an oversized left cell (the left one is truncated).
|
|
740
|
+
const cut = layoutFooterGrid("h", "m", [g(30)], ["R".repeat(4)], " | ", " · ", 12, vw);
|
|
741
|
+
assert(cut[1]!.endsWith("R".repeat(4)) && vw(cut[1]!) === 12, "grid: right cell survives a wide left cell");
|
|
742
|
+
// A side that runs out leaves its half blank; rows where both ran out drop.
|
|
743
|
+
const blank = layoutFooterGrid("h", "m", [], [], " | ", " · ", 20, vw);
|
|
744
|
+
assert(blank.length === 1, "grid: empty sides drop their rows");
|
|
747
745
|
|
|
748
746
|
const plain = (s: string) => s;
|
|
749
747
|
const D = "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"; // 8 columns = content width in this test
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Arnative look for Pi: warm-neutral themes, rounded tool boxes, tabbed header, compact footer, transcript clock, and a /usage dashboard.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|