@bismawy/pi-arnative 0.2.12 → 0.3.0

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.
@@ -4,17 +4,21 @@
4
4
  * Rows 2-3: extension status (mcp first) ......... tok/s · cache · token,
5
5
  * each side wrapped inside its half of the width and paired row by row.
6
6
  *
7
+ * Preset (`/arnative footers`): "Arnative (Full)" (default) or "Pi" (built-in footer), live.
7
8
  * Transcript clock & bubble bg: extensions/timestamps.ts
8
9
  * /new header: extensions/section-headers.ts
9
10
  * Selection + box reload: extensions/ui-render-tweaks.ts
10
11
  */
11
12
  import type { AssistantMessage } from "@earendil-works/pi-ai";
12
- import { CustomEditor, FooterComponent, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
+ import { CustomEditor, FooterComponent, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
13
14
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
14
15
  import { execFile } from "node:child_process";
16
+ import { mkdtempSync, rmSync } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
15
19
  import { stripAnsi } from "../lib/ansi.ts";
16
20
  import { assert, isMain } from "../lib/check.ts";
17
- import { capitalize, modelDisplayParts } from "../lib/format.ts";
21
+ import { capitalize, markedLabels, modelDisplayParts } from "../lib/format.ts";
18
22
  import { IS_WINDOWS_LIKE, SHORTCUT_RELOAD } from "../lib/shortcuts.ts";
19
23
  import { formatTokens } from "../lib/usage-store.ts";
20
24
 
@@ -22,12 +26,49 @@ import { formatTokens } from "../lib/usage-store.ts";
22
26
  // The TUI drops key-release events before handleInput, so press+release cannot
23
27
  // fire this twice even when the terminal reports event types (Kitty flag 2).
24
28
 
29
+ // Footer preset chosen through `/arnative footers`. "Arnative (Full)" = this extension's
30
+ // 3-row grid footer; "Pi" = pi's built-in footer (the boxed editor stays arnative in both —
31
+ // pi's `setEditorComponent()` would close the picker mid-preview). The picker lives in
32
+ // arnative.ts, so the choice is shared through globalThis like the header preset.
33
+ export const FOOTER_PRESETS = ["Arnative (Full)", "Pi"] as const;
34
+ export type FooterPreset = (typeof FOOTER_PRESETS)[number];
35
+ export const DEFAULT_FOOTER_PRESET: FooterPreset = "Arnative (Full)";
36
+ const FOOTER_PRESET_KEY = Symbol.for("pi-arnative.footerPreset");
37
+ const FOOTER_REGISTRAR_KEY = Symbol.for("pi-arnative.footerRegistrar");
38
+
39
+ export function activeFooterPreset(): FooterPreset {
40
+ const stored = (globalThis as Record<symbol, unknown>)[FOOTER_PRESET_KEY];
41
+ return FOOTER_PRESETS.includes(stored as FooterPreset) ? (stored as FooterPreset) : DEFAULT_FOOTER_PRESET;
42
+ }
43
+
44
+ /**
45
+ * Hand the footer slot to the active preset: arnative's grid, or pi's built-in footer.
46
+ * The editor is deliberately never touched here — pi's `setEditorComponent()` calls
47
+ * `disposeActiveSelector()`, which would close the `/arnative` picker mid-preview.
48
+ */
49
+ export function applyFooterPreset(ctx: ExtensionContext): void {
50
+ if (activeFooterPreset() === "Pi") {
51
+ ctx.ui?.setFooter?.(undefined);
52
+ return;
53
+ }
54
+ const register = (globalThis as Record<symbol, ((ctx: ExtensionContext) => void) | undefined>)[FOOTER_REGISTRAR_KEY];
55
+ register?.(ctx);
56
+ }
57
+
58
+ /** Select a preset and apply it immediately (the footer swaps on the spot). */
59
+ export function setFooterPreset(name: FooterPreset, ctx: ExtensionContext): void {
60
+ (globalThis as Record<symbol, unknown>)[FOOTER_PRESET_KEY] = name;
61
+ applyFooterPreset(ctx);
62
+ }
63
+
25
64
  // Intercept the built-in FooterComponent to force a pure 2-line footer
26
65
  const PATCHED_KEY = Symbol.for("pi-arnative.footer2LinesPatched");
27
66
  if (FooterComponent?.prototype?.render && !(globalThis as Record<symbol, boolean>)[PATCHED_KEY]) {
28
67
  (globalThis as Record<symbol, boolean>)[PATCHED_KEY] = true;
29
68
  const origRender = FooterComponent.prototype.render;
30
69
  FooterComponent.prototype.render = function (width: number): string[] {
70
+ // "Pi" preset = pi's own footer, so keep its stock line count.
71
+ if (activeFooterPreset() === "Pi") return origRender.call(this, width);
31
72
  const lines = origRender.call(this, width);
32
73
  return lines.length > 2 ? lines.slice(0, 2) : lines;
33
74
  };
@@ -84,47 +125,61 @@ function run(cmd: string, args: string[], cwd: string): Promise<string> {
84
125
  });
85
126
  }
86
127
 
128
+ // A refresh arriving while one is in flight is queued (last requested cwd wins) and
129
+ // re-run afterwards — it used to be dropped, leaving the footer's git state stale
130
+ // until the next trigger.
131
+ let gitRefreshQueuedCwd: string | null = null;
87
132
  async function refreshGit(cwd: string): Promise<void> {
88
- if (gitRefreshInFlight) return;
133
+ if (gitRefreshInFlight) {
134
+ gitRefreshQueuedCwd = cwd;
135
+ return;
136
+ }
89
137
  gitRefreshInFlight = true;
90
- const branch = await run("git", ["branch", "--show-current"], cwd);
91
- if (!branch) {
92
- git = null;
138
+ try {
139
+ const branch = await run("git", ["branch", "--show-current"], cwd);
140
+ if (!branch) {
141
+ git = null;
142
+ return;
143
+ }
144
+ const [tag, status, sync] = await Promise.all([
145
+ run("git", ["describe", "--tags", "--abbrev=0"], cwd),
146
+ run("git", ["status", "--porcelain"], cwd),
147
+ run("git", ["rev-list", "--left-right", "--count", "@{u}...HEAD"], cwd),
148
+ ]);
149
+ const uncommitted = status ? status.split("\n").filter((l) => l.trim().length > 0).length : 0;
150
+ const parts = sync.trim() === "" ? [] : sync.trim().split(/\s+/).map(Number);
151
+ const behind = parts.length > 0 && parts[0] > 0 ? parts[0] : 0;
152
+ const ahead = parts.length > 1 && parts[1] > 0 ? parts[1] : 0;
153
+ git = {
154
+ branch,
155
+ tag: tag || "-",
156
+ uncommitted,
157
+ ahead,
158
+ behind,
159
+ };
160
+ (globalThis as Record<symbol, any>)[GIT_KEY] = git;
161
+ } finally {
93
162
  gitRefreshInFlight = false;
94
- return;
163
+ if (gitRefreshQueuedCwd !== null) {
164
+ const queued = gitRefreshQueuedCwd;
165
+ gitRefreshQueuedCwd = null;
166
+ void refreshGit(queued).then(() => poke());
167
+ }
95
168
  }
96
- const [tag, status, sync] = await Promise.all([
97
- run("git", ["describe", "--tags", "--abbrev=0"], cwd),
98
- run("git", ["status", "--porcelain"], cwd),
99
- run("git", ["rev-list", "--left-right", "--count", "@{u}...HEAD"], cwd),
100
- ]);
101
- const uncommitted = status ? status.split("\n").filter((l) => l.trim().length > 0).length : 0;
102
- const parts = sync.trim() === "" ? [] : sync.trim().split(/\s+/).map(Number);
103
- const behind = parts.length > 0 && parts[0] > 0 ? parts[0] : 0;
104
- const ahead = parts.length > 1 && parts[1] > 0 ? parts[1] : 0;
105
- git = {
106
- branch,
107
- tag: tag || "-",
108
- uncommitted,
109
- ahead,
110
- behind,
111
- };
112
- (globalThis as Record<symbol, any>)[GIT_KEY] = git;
113
- gitRefreshInFlight = false;
114
169
  }
115
170
 
116
171
  function formatModelName(
117
172
  model: { id: string; name?: string; provider?: string } | undefined,
118
173
  thinkingLevel: string | undefined,
119
174
  acc: (t: string) => string,
120
- tint: (t: string) => string,
175
+ soft: (t: string) => string,
121
176
  dim: (t: string) => string,
122
177
  ): string {
123
178
  if (!model) return dim("No Model");
124
179
  const { name, provider } = modelDisplayParts({ id: model.id, name: model.name, provider: model.provider });
125
180
  const provStr = provider ? ` ${dim(`(${provider})`)}` : "";
126
181
  const capThinking = thinkingLevel && thinkingLevel !== "off" ? capitalize(thinkingLevel) : "";
127
- const thinkStr = capThinking ? `${acc("\udb80\udf35")} ${tint(capThinking)} ${dim("·")} ` : "";
182
+ const thinkStr = capThinking ? `${acc("\udb80\udf35")} ${soft(capThinking)} ${dim("·")} ` : "";
128
183
  return `${thinkStr}${acc(name)}${provStr}`;
129
184
  }
130
185
 
@@ -248,22 +303,36 @@ function parseOptimizer(raw: string | undefined): string | null {
248
303
  let usageScanAt = 0;
249
304
  let usageScanLen = -1;
250
305
  let usageScanNums = { inp: 0, out: 0, read: 0 };
306
+ // Usage of the branch's last entry at scan time: a stream tick mutates that message
307
+ // in place (branch length unchanged), so the delta is applied on top of the cached
308
+ // sums — O(1) per frame, numbers always fresh, still no O(N) rescan per frame.
309
+ let usageScanLast = { inp: 0, out: 0, read: 0 };
310
+
311
+ const lastUsageOf = (branch: readonly unknown[]): { inp: number; out: number; read: number } => {
312
+ const last = branch[branch.length - 1] as
313
+ | { type?: string; message?: { role?: string; usage?: { input?: number; output?: number; cacheRead?: number } } }
314
+ | undefined;
315
+ const u = last?.type === "message" && last.message?.role === "assistant" ? last.message.usage : undefined;
316
+ return u ? { inp: u.input || 0, out: u.output || 0, read: u.cacheRead || 0 } : { inp: 0, out: 0, read: 0 };
317
+ };
251
318
 
252
319
  // 2s cache + branch length: the footer renders on every poke/stream tick, so
253
320
  // without this every frame rescans the whole branch (O(N) per frame).
254
- function getUsage(
321
+ export function getUsage(
255
322
  ctx: { sessionManager: { getBranch(): readonly unknown[] }; getContextUsage(): { tokens: number | null; contextWindow: number; percent: number | null } | undefined },
256
323
  acc: (t: string) => string,
257
- tint: (t: string) => string,
324
+ soft: (t: string) => string,
258
325
  ): string {
259
326
  const branch = ctx.sessionManager.getBranch();
327
+ const last = lastUsageOf(branch);
260
328
  let inp = 0;
261
329
  let out = 0;
262
330
  let read = 0;
263
331
  if (Date.now() - usageScanAt < 2000 && branch.length === usageScanLen) {
264
- inp = usageScanNums.inp;
265
- out = usageScanNums.out;
266
- read = usageScanNums.read;
332
+ // Cached sums + the streaming delta of the last message (mutated in place).
333
+ inp = usageScanNums.inp + last.inp - usageScanLast.inp;
334
+ out = usageScanNums.out + last.out - usageScanLast.out;
335
+ read = usageScanNums.read + last.read - usageScanLast.read;
267
336
  } else {
268
337
  for (const e of branch) {
269
338
  if (e && typeof e === "object" && "type" in e && e.type === "message") {
@@ -279,16 +348,17 @@ function getUsage(
279
348
  }
280
349
  }
281
350
  usageScanNums = { inp, out, read };
351
+ usageScanLast = last;
282
352
  usageScanAt = Date.now();
283
353
  usageScanLen = branch.length;
284
354
  }
285
355
  const parts: string[] = [];
286
- if (inp > 0) parts.push(`${acc("↑")}${tint(formatTokens(inp))}`);
287
- if (out > 0) parts.push(`${acc("↓")}${tint(formatTokens(out))}`);
288
- if (read > 0) parts.push(`${acc("\uf49b")} ${tint(formatTokens(read))}`);
356
+ if (inp > 0) parts.push(`${acc("↑")}${soft(formatTokens(inp))}`);
357
+ if (out > 0) parts.push(`${acc("↓")}${soft(formatTokens(out))}`);
358
+ if (read > 0) parts.push(`${acc("\uf49b")} ${soft(formatTokens(read))}`);
289
359
  const u = ctx.getContextUsage();
290
360
  if (u && u.percent !== null && u.tokens !== null) {
291
- parts.push(`${acc("\udb81\udfaf")} ${tint(`${u.percent.toFixed(1)}%/${formatTokens(u.contextWindow)}`)}`);
361
+ parts.push(`${acc("\udb81\udfaf")} ${soft(`${u.percent.toFixed(1)}%/${formatTokens(u.contextWindow)}`)}`);
292
362
  }
293
363
  return parts.join(" ");
294
364
  }
@@ -425,115 +495,119 @@ export default function (pi: ExtensionAPI) {
425
495
  (globalThis as Record<symbol, number>)[GEN_KEY] = footerGen;
426
496
 
427
497
  try {
428
- ctx.ui.setFooter((tui, theme, footerData) => {
429
- activeThemeProxy = theme;
430
- rerender = () => tui.requestRender();
431
- const unsub = footerData.onBranchChange(() => {
432
- void refreshGit(cwd).then(() => tui.requestRender());
433
- });
434
- return {
435
- dispose() {
436
- rerender = null;
437
- unsub();
438
- },
439
- invalidate() {},
440
- render(width: number): string[] {
441
- const acc = (text: string) => theme.fg("accent", text);
442
- const dim = (text: string) => theme.fg("dim", text);
443
- const fgAny = theme.fg.bind(theme) as (color: string, text: string) => string;
444
- let tintName = "accent";
445
- try {
446
- fgAny("tint", "");
447
- tintName = "tint";
448
- } catch {
449
- // fallback
450
- }
451
- const tint = (text: string) => fgAny(tintName, text);
452
- const sep = dim(" | ");
453
-
454
- const durationStr = formatDuration(Date.now() - sessionStartMs);
455
- const pDuration = `${acc("\uf017")} ${tint(durationStr)}`;
456
- const cwdShort = shortenCwd(cwd);
457
- let left1 = `${acc("\uf07b")} ${dim(cwdShort)}${sep}${pDuration}`;
458
- if (git) {
459
- const gitIcon = acc("\uf172");
460
- const gitText = git.uncommitted > 0 ? tint(`~${git.uncommitted}`) : tint("clean");
461
- const pBranch = `${acc("\uf126")} ${tint(git.branch)}${git.ahead > 0 ? ` ${tint(`↑${git.ahead}`)}` : ""}${git.behind > 0 ? ` ${tint(`↓${git.behind}`)}` : ""}`;
462
- const pTag = `${acc("\uf02b")} ${tint(git.tag)}`;
463
- const pState = `${gitIcon} ${gitText}`;
464
- left1 = `${acc("\uf07b")} ${dim(cwdShort)}${sep}${pDuration}${sep}${pBranch}${sep}${pTag}${sep}${pState}`;
465
- }
466
- const right1 = formatModelName(currentModel, currentThinkingLevel, acc, tint, dim);
467
-
468
- const statuses: ReadonlyMap<string, string> = (() => {
498
+ const registerArnative = (c: ExtensionContext) => {
499
+ c.ui.setFooter((tui, theme, footerData) => {
500
+ activeThemeProxy = theme;
501
+ rerender = () => tui.requestRender();
502
+ const unsub = footerData.onBranchChange(() => {
503
+ void refreshGit(cwd).then(() => tui.requestRender());
504
+ });
505
+ return {
506
+ dispose() {
507
+ rerender = null;
508
+ unsub();
509
+ },
510
+ invalidate() {},
511
+ render(width: number): string[] {
512
+ const acc = (text: string) => theme.fg("accent", text);
513
+ const dim = (text: string) => theme.fg("dim", text);
514
+ const fgAny = theme.fg.bind(theme) as (color: string, text: string) => string;
515
+ let softName = "accent";
469
516
  try {
470
- return footerData.getExtensionStatuses();
517
+ fgAny("accentSoft", "");
518
+ softName = "accentSoft";
471
519
  } catch {
472
- return new Map<string, string>();
520
+ // fallback
473
521
  }
474
- })();
475
- const rawCache = statuses.get("pi-cache-stats");
476
- const cleanStatus = (s: string) => {
477
- if (s.includes("MCP:")) {
478
- let clean = stripAnsi(s);
479
- const idx = clean.indexOf("MCP:");
480
- let rest = (idx >= 0 ? clean.slice(idx) : clean).replace(/\uFFFD/g, "").trim();
481
- rest = rest.replace(/[\p{Extended_Pictographic}\uFE0F\u200D\u2800-\u28FF]/gu, "").replace(/\s{2,}/g, " ").trim();
482
- const m = rest.match(/MCP:\s*\d+\s*servers?\s*enabled/i);
483
- return `${acc("\uf233")} ${tint(m ? m[0] : rest || "MCP")}`;
484
- }
485
- if (s.includes("ponytail")) {
486
- const isActive = s.includes("●");
487
- const bullet = isActive ? acc("●") : dim("○");
488
- let mode = "FULL";
489
- if (/LITE/i.test(s)) mode = "LITE";
490
- else if (/ULTRA/i.test(s)) mode = "ULTRA";
491
- else if (/FULL/i.test(s)) mode = "FULL";
492
- return `${acc("\uef04")} ${tint("ponytail:")} ${bullet} ${tint(mode)}`;
522
+ const soft = (text: string) => fgAny(softName, text);
523
+ const sep = dim(" | ");
524
+
525
+ const durationStr = formatDuration(Date.now() - sessionStartMs);
526
+ const pDuration = `${acc("\uf017")} ${soft(durationStr)}`;
527
+ const cwdShort = shortenCwd(cwd);
528
+ let left1 = `${acc("\uf07b")} ${dim(cwdShort)}${sep}${pDuration}`;
529
+ if (git) {
530
+ const gitIcon = acc("\uf172");
531
+ const gitText = git.uncommitted > 0 ? soft(`~${git.uncommitted}`) : soft("clean");
532
+ const pBranch = `${acc("\uf126")} ${soft(git.branch)}${git.ahead > 0 ? ` ${soft(`↑${git.ahead}`)}` : ""}${git.behind > 0 ? ` ${soft(`↓${git.behind}`)}` : ""}`;
533
+ const pTag = `${acc("\uf02b")} ${soft(git.tag)}`;
534
+ const pState = `${gitIcon} ${gitText}`;
535
+ left1 = `${acc("\uf07b")} ${dim(cwdShort)}${sep}${pDuration}${sep}${pBranch}${sep}${pTag}${sep}${pState}`;
493
536
  }
494
- if (s.includes("jev-eye")) {
495
- const isOff = s.includes("OFF") || s.includes("○");
496
- const isReview = /REVIEW/i.test(s);
497
- let bullet = isOff ? dim("○") : acc("●");
498
- if (isReview) {
499
- try {
500
- bullet = theme.fg("warning", "●");
501
- } catch {
502
- bullet = acc("●");
537
+ const right1 = formatModelName(currentModel, currentThinkingLevel, acc, soft, dim);
538
+
539
+ const statuses: ReadonlyMap<string, string> = (() => {
540
+ try {
541
+ return footerData.getExtensionStatuses();
542
+ } catch {
543
+ return new Map<string, string>();
544
+ }
545
+ })();
546
+ const rawCache = statuses.get("pi-cache-stats");
547
+ const cleanStatus = (s: string) => {
548
+ if (s.includes("MCP:")) {
549
+ let clean = stripAnsi(s);
550
+ const idx = clean.indexOf("MCP:");
551
+ let rest = (idx >= 0 ? clean.slice(idx) : clean).replace(/\uFFFD/g, "").trim();
552
+ rest = rest.replace(/[\p{Extended_Pictographic}\uFE0F\u200D\u2800-\u28FF]/gu, "").replace(/\s{2,}/g, " ").trim();
553
+ const m = rest.match(/MCP:\s*\d+\s*servers?\s*enabled/i);
554
+ return `${acc("\uf233")} ${soft(m ? m[0] : rest || "MCP")}`;
555
+ }
556
+ if (s.includes("ponytail")) {
557
+ const isActive = s.includes("●");
558
+ const bullet = isActive ? acc("●") : dim("○");
559
+ let mode = "FULL";
560
+ if (/LITE/i.test(s)) mode = "LITE";
561
+ else if (/ULTRA/i.test(s)) mode = "ULTRA";
562
+ else if (/FULL/i.test(s)) mode = "FULL";
563
+ return `${acc("\uef04")} ${soft("ponytail:")} ${bullet} ${soft(mode)}`;
564
+ }
565
+ if (s.includes("jev-eye")) {
566
+ const isOff = s.includes("OFF") || s.includes("○");
567
+ const isReview = /REVIEW/i.test(s);
568
+ let bullet = isOff ? dim("○") : acc("●");
569
+ if (isReview) {
570
+ try {
571
+ bullet = theme.fg("warning", "●");
572
+ } catch {
573
+ bullet = acc("●");
574
+ }
503
575
  }
576
+ const label = isOff ? "OFF" : isReview ? "REVIEW" : "ON";
577
+ return `${acc("\uedcf")} ${soft("Jev:")} ${bullet} ${soft(label)}`;
504
578
  }
505
- const label = isOff ? "OFF" : isReview ? "REVIEW" : "ON";
506
- return `${acc("\uedcf")} ${tint("Jev:")} ${bullet} ${tint(label)}`;
579
+ let clean = stripAnsi(s);
580
+ clean = clean.replace(/\uFFFD/g, "").replace(/\?{1,2}\s*/g, "");
581
+ return soft(clean.trim());
582
+ };
583
+
584
+ const groups: string[] = [];
585
+ const mcp = statuses.get("mcp");
586
+ if (mcp !== undefined) groups.push(cleanStatus(mcp));
587
+ for (const [k, s] of statuses) {
588
+ if (k !== "mcp" && k !== "pi-cache-stats") groups.push(cleanStatus(s));
507
589
  }
508
- let clean = stripAnsi(s);
509
- clean = clean.replace(/\uFFFD/g, "").replace(/\?{1,2}\s*/g, "");
510
- return tint(clean.trim());
511
- };
512
-
513
- const groups: string[] = [];
514
- const mcp = statuses.get("mcp");
515
- if (mcp !== undefined) groups.push(cleanStatus(mcp));
516
- for (const [k, s] of statuses) {
517
- if (k !== "mcp" && k !== "pi-cache-stats") groups.push(cleanStatus(s));
518
- }
519
590
 
520
- const opt = parseOptimizer(rawCache);
521
- let usageStr = "";
522
- try {
523
- usageStr = getUsage(ctx, acc, tint);
524
- } catch {
525
- // fallback
526
- }
527
- const speedStr = latestSpeed !== null && latestSpeed > 0 ? `${acc("\udb81\udcc5")} ${tint(`${latestSpeed.toFixed(1)} tok/s`)}` : "";
528
- const metricCells = [speedStr, opt ? `${acc("\udb80\udf5b")} ${tint(opt)}` : "", usageStr].filter(Boolean);
591
+ const opt = parseOptimizer(rawCache);
592
+ let usageStr = "";
593
+ try {
594
+ usageStr = getUsage(ctx, acc, soft);
595
+ } catch {
596
+ // fallback
597
+ }
598
+ const speedStr = latestSpeed !== null && latestSpeed > 0 ? `${acc("\udb81\udcc5")} ${soft(`${latestSpeed.toFixed(1)} tok/s`)}` : "";
599
+ const metricCells = [speedStr, opt ? `${acc("\udb80\udf5b")} ${soft(opt)}` : "", usageStr].filter(Boolean);
529
600
 
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);
533
- },
534
- };
535
- });
601
+ // Fixed 3-row grid: head (cwd/model), then statuses and metrics each
602
+ // wrapped inside their half of the width — no click toggle.
603
+ return layoutFooterGrid(left1, right1, groups, metricCells, sep, ` ${dim("·")} `, width);
604
+ },
605
+ };
606
+ });
536
607
 
608
+ };
609
+ (globalThis as Record<symbol, (ctx: ExtensionContext) => void>)[FOOTER_REGISTRAR_KEY] = registerArnative;
610
+ applyFooterPreset(ctx);
537
611
  class ArnativeEditor extends CustomEditor {
538
612
  constructor(tui: any, editorTheme: any, keybindings: any, options?: any) {
539
613
  super(tui, editorTheme, keybindings, { ...options, embedWorkingStatus: true });
@@ -558,7 +632,7 @@ export default function (pi: ExtensionAPI) {
558
632
  const th = this.getActiveTheme();
559
633
  if (!th) return text;
560
634
  try {
561
- return th.fg("tint", text);
635
+ return th.fg("accentSoft", text);
562
636
  } catch {
563
637
  return th.fg("muted", text);
564
638
  }
@@ -791,5 +865,52 @@ if (isMain(import.meta.url)) {
791
865
  assert(SHORTCUT_RELOAD.matches("\x1br") === IS_WINDOWS_LIKE, "ESC + r triggers reload only where alt+r is bound");
792
866
  assert(!SHORTCUT_RELOAD.matches("\x12"), "plain ctrl+r does not trigger reload");
793
867
  assert(!SHORTCUT_RELOAD.matches("r"), "bare letter r does not trigger reload");
868
+
869
+ // getUsage cache: a stream tick mutates the last assistant message in place —
870
+ // the delta is applied on top of the cached sums (fresh numbers, no O(N) rescan)
871
+ const usageBranch: Array<{ type: string; message: { role: string; usage?: { input?: number; output?: number; cacheRead?: number } } }> = [
872
+ { type: "message", message: { role: "user", content: "hi" } as never },
873
+ { type: "message", message: { role: "assistant", usage: { input: 100, output: 50, cacheRead: 10 } } },
874
+ ];
875
+ const usageCtx = { sessionManager: { getBranch: () => usageBranch }, getContextUsage: () => undefined } as Parameters<typeof getUsage>[0];
876
+ const id = (t: string) => t;
877
+ const s1 = getUsage(usageCtx, id, id);
878
+ assert(s1.includes("↑100") && s1.includes("↓50"), "getUsage scans the branch");
879
+ usageBranch[1]!.message.usage = { input: 100, output: 80, cacheRead: 10 }; // stream tick in place
880
+ const s2 = getUsage(usageCtx, id, id);
881
+ assert(s2.includes("↓80") && !s2.includes("↓50"), "stream tick: delta applied without a rescan (length unchanged)");
882
+ usageBranch.push({ type: "message", message: { role: "assistant", usage: { input: 1000, output: 2500, cacheRead: 0 } } });
883
+ const s3 = getUsage(usageCtx, id, id);
884
+ assert(s3.includes("↑1.1k") && s3.includes("↓2.6k"), "branch length changed: full rescan");
885
+
886
+ // regression: a refresh arriving while one is in flight is re-run with the newest
887
+ // cwd, not dropped (the footer's git state no longer stays stale until the next trigger)
888
+ git = null;
889
+ const noGit = mkdtempSync(join(tmpdir(), "pi-arnative-nogit-"));
890
+ void refreshGit(noGit); // in flight (deterministic: sync up to the first await), resolves git = null
891
+ await refreshGit(process.cwd()); // queued behind it with the newest cwd
892
+ for (let i = 0; i < 100 && git === null; i++) await new Promise((r) => setTimeout(r, 20));
893
+ assert(git !== null && git.branch.length > 0, "queued git refresh re-ran on the newest cwd (not dropped)");
894
+ rmSync(noGit, { recursive: true, force: true });
895
+
896
+ // Footer presets: Arnative (Full) is the default; Pi hands the footer slot back to pi (the editor stays arnative).
897
+ assert(DEFAULT_FOOTER_PRESET === "Arnative (Full)" && FOOTER_PRESETS.length === 2, "Arnative (Full) is the default footer preset");
898
+ assert(markedLabels(FOOTER_PRESETS, "Pi").join(" | ") === " Arnative (Full) | ● Pi", "footer preset labels mark the active one");
899
+ let footerCleared = 0;
900
+ let editorTouched = 0;
901
+ const presetCtx: any = {
902
+ ui: {
903
+ setFooter: (f: unknown) => { if (f === undefined) footerCleared++; },
904
+ setEditorComponent: () => { editorTouched++; },
905
+ },
906
+ };
907
+ setFooterPreset("Pi", presetCtx);
908
+ assert(footerCleared === 1 && editorTouched === 0 && activeFooterPreset() === "Pi", "Pi clears the footer only (an editor swap would close the picker)");
909
+ let registered = 0;
910
+ (globalThis as Record<symbol, (ctx: ExtensionContext) => void>)[FOOTER_REGISTRAR_KEY] = () => { registered++; };
911
+ setFooterPreset("Arnative (Full)", presetCtx);
912
+ assert(registered === 1 && activeFooterPreset() === "Arnative (Full)", "Arnative (Full) re-runs the arnative registrar");
913
+ delete (globalThis as Record<symbol, unknown>)[FOOTER_REGISTRAR_KEY];
914
+
794
915
  console.log("footer.ts self-check OK");
795
916
  }