@neilurk12/pi-clean-footer 0.1.0 → 0.1.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/README.md CHANGED
@@ -20,28 +20,22 @@ Shows a compact split footer:
20
20
 
21
21
  ## Install
22
22
 
23
- From local checkout:
23
+ From npm (recommended):
24
24
 
25
25
  ```bash
26
- pi install /absolute/path/to/pi-clean-footer
26
+ pi install @neilurk12/pi-clean-footer
27
27
  ```
28
28
 
29
- For project-local install, run from your project:
29
+ For project-local install:
30
30
 
31
31
  ```bash
32
- pi install -l /absolute/path/to/pi-clean-footer
32
+ pi install -l @neilurk12/pi-clean-footer
33
33
  ```
34
34
 
35
- For quick testing without installing:
35
+ Or from local checkout (development):
36
36
 
37
37
  ```bash
38
- pi -e /absolute/path/to/pi-clean-footer
39
- ```
40
-
41
- Then reload pi resources:
42
-
43
- ```text
44
- /reload
38
+ pi install /absolute/path/to/pi-clean-footer
45
39
  ```
46
40
 
47
41
  ## Usage
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neilurk12/pi-clean-footer",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Clean adaptive footer extension for pi coding agent.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -10,6 +10,11 @@
10
10
  "terminal"
11
11
  ],
12
12
  "license": "MIT",
13
+ "files": [
14
+ "src",
15
+ "README.md",
16
+ "example.png"
17
+ ],
13
18
  "pi": {
14
19
  "extensions": [
15
20
  "./src/index.ts"
@@ -1,546 +0,0 @@
1
- # Clean Footer Extension Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Build a project-local pi extension that replaces the built-in footer with a cleaner adaptive footer showing model/effort, directory, git state, context usage, and cumulative token/cache telemetry.
6
-
7
- **Architecture:** Implement one self-contained TypeScript extension at `.pi/extensions/clean-footer/index.ts`. Keep git state outside render and refresh it through debounced event handlers. Render computes lightweight session totals and adaptive layout from current width.
8
-
9
- **Tech Stack:** pi extension API, TypeScript, Node built-ins, `@earendil-works/pi-tui` width helpers, no external runtime deps.
10
-
11
- **Note:** Do not commit changes. Keep all work in workspace.
12
-
13
- ---
14
-
15
- ## File Structure
16
-
17
- - Create `.pi/extensions/clean-footer/index.ts`
18
- - Registers footer command.
19
- - Installs/removes custom footer.
20
- - Tracks enabled state, git state, debounced refresh, thinking level.
21
- - Formats model, effort, cwd basename, context, tokens, git state.
22
- - Renders adaptive footer tiers.
23
- - Use existing `docs/superpowers/specs/2026-05-09-clean-footer-extension-design.md` as requirements reference.
24
- - No tests file initially; verification is manual because extension footer behavior depends on interactive pi TUI.
25
-
26
- ---
27
-
28
- ### Task 1: Create extension skeleton and default-on footer
29
-
30
- **Files:**
31
-
32
- - Create: `.pi/extensions/clean-footer/index.ts`
33
-
34
- - [ ] **Step 1: Create extension with command and lifecycle wiring**
35
-
36
- Create `.pi/extensions/clean-footer/index.ts` with:
37
-
38
- ```typescript
39
- import type { AssistantMessage } from "@earendil-works/pi-ai";
40
- import type {
41
- ExtensionAPI,
42
- ExtensionContext,
43
- } from "@earendil-works/pi-coding-agent";
44
- import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
45
- import { execFile } from "node:child_process";
46
- import path from "node:path";
47
- import { promisify } from "node:util";
48
-
49
- const execFileAsync = promisify(execFile);
50
-
51
- type GitState = {
52
- inRepo: boolean;
53
- branch?: string;
54
- dirtyCount: number;
55
- };
56
-
57
- type Totals = {
58
- input: number;
59
- output: number;
60
- cacheRead: number;
61
- cacheWrite: number;
62
- };
63
-
64
- type FooterRuntime = {
65
- enabled: boolean;
66
- git: GitState;
67
- thinkingLevel?: string;
68
- refreshTimer?: NodeJS.Timeout;
69
- requestRender?: () => void;
70
- };
71
-
72
- const runtime: FooterRuntime = {
73
- enabled: true,
74
- git: { inRepo: false, dirtyCount: 0 },
75
- };
76
-
77
- export default function (pi: ExtensionAPI) {
78
- pi.registerCommand("footer", {
79
- description: "Toggle or refresh the clean footer",
80
- handler: async (args, ctx) => {
81
- const command = args.trim();
82
- if (command === "refresh") {
83
- await refreshGit(ctx, true);
84
- runtime.requestRender?.();
85
- if (ctx.hasUI) ctx.ui.notify("Footer refreshed", "info");
86
- return;
87
- }
88
-
89
- runtime.enabled = !runtime.enabled;
90
- if (!ctx.hasUI) return;
91
-
92
- if (runtime.enabled) {
93
- installFooter(ctx);
94
- await refreshGit(ctx, true);
95
- ctx.ui.notify("Clean footer enabled", "info");
96
- } else {
97
- ctx.ui.setFooter(undefined);
98
- ctx.ui.notify("Default footer restored", "info");
99
- }
100
- },
101
- });
102
-
103
- pi.on("session_start", async (_event, ctx) => {
104
- runtime.thinkingLevel = normalizeThinkingLevel(pi.getThinkingLevel?.());
105
- if (!ctx.hasUI || !runtime.enabled) return;
106
- installFooter(ctx);
107
- await refreshGit(ctx, true);
108
- });
109
-
110
- pi.on("session_shutdown", async (_event, ctx) => {
111
- if (runtime.refreshTimer) clearTimeout(runtime.refreshTimer);
112
- runtime.refreshTimer = undefined;
113
- runtime.requestRender = undefined;
114
- if (ctx.hasUI) ctx.ui.setFooter(undefined);
115
- });
116
-
117
- pi.on("thinking_level_select", (event) => {
118
- runtime.thinkingLevel = normalizeThinkingLevel(event.level);
119
- runtime.requestRender?.();
120
- });
121
-
122
- pi.on("model_select", () => {
123
- runtime.requestRender?.();
124
- });
125
-
126
- pi.on("message_end", (event) => {
127
- if (event.message.role === "assistant") runtime.requestRender?.();
128
- });
129
-
130
- pi.on("tool_execution_end", (event, ctx) => {
131
- if (["bash", "edit", "write"].includes(event.toolName))
132
- scheduleGitRefresh(ctx);
133
- runtime.requestRender?.();
134
- });
135
-
136
- pi.on("user_bash", (_event, ctx) => {
137
- scheduleGitRefresh(ctx);
138
- });
139
- }
140
- ```
141
-
142
- - [ ] **Step 2: Add no-op helper stubs so TypeScript names resolve**
143
-
144
- Append below the default export:
145
-
146
- ```typescript
147
- function installFooter(ctx: ExtensionContext) {
148
- if (!ctx.hasUI) return;
149
-
150
- ctx.ui.setFooter((tui, theme) => {
151
- runtime.requestRender = () => tui.requestRender();
152
- return {
153
- invalidate() {},
154
- render(width: number): string[] {
155
- return [
156
- truncateToWidth(theme.fg("dim", "clean footer loading"), width),
157
- ];
158
- },
159
- };
160
- });
161
- }
162
-
163
- function normalizeThinkingLevel(level: unknown): string | undefined {
164
- if (typeof level !== "string") return undefined;
165
- const normalized = level.toLowerCase();
166
- if (
167
- ["low", "med", "medium", "high", "xhigh", "extra-high"].includes(normalized)
168
- ) {
169
- if (normalized === "medium") return "med";
170
- if (normalized === "extra-high") return "xhigh";
171
- return normalized;
172
- }
173
- return undefined;
174
- }
175
-
176
- async function refreshGit(ctx: ExtensionContext, immediate = false) {
177
- runtime.git = { inRepo: false, dirtyCount: 0 };
178
- if (immediate) runtime.requestRender?.();
179
- }
180
-
181
- function scheduleGitRefresh(ctx: ExtensionContext) {
182
- if (runtime.refreshTimer) clearTimeout(runtime.refreshTimer);
183
- runtime.refreshTimer = setTimeout(() => {
184
- runtime.refreshTimer = undefined;
185
- void refreshGit(ctx, true);
186
- }, 500);
187
- }
188
- ```
189
-
190
- - [ ] **Step 3: Verify skeleton loads**
191
-
192
- Run pi command manually from interactive session:
193
-
194
- ```text
195
- /reload
196
- ```
197
-
198
- Expected: no extension load error; footer changes to `clean footer loading`. `/footer` toggles back to built-in footer and again to clean footer.
199
-
200
- ---
201
-
202
- ### Task 2: Implement formatting helpers and telemetry totals
203
-
204
- **Files:**
205
-
206
- - Modify: `.pi/extensions/clean-footer/index.ts`
207
-
208
- - [ ] **Step 1: Replace loading render with computed segments**
209
-
210
- Replace `installFooter` with:
211
-
212
- ```typescript
213
- function installFooter(ctx: ExtensionContext) {
214
- if (!ctx.hasUI) return;
215
-
216
- ctx.ui.setFooter((tui, theme) => {
217
- runtime.requestRender = () => tui.requestRender();
218
- return {
219
- invalidate() {},
220
- render(width: number): string[] {
221
- const model = formatModelName(ctx.model?.id ?? "no-model");
222
- const effort = runtime.thinkingLevel
223
- ? ` • ${runtime.thinkingLevel}`
224
- : "";
225
- const modelSegment = theme.fg("accent", `${model}${effort}`);
226
- const dirSegment = theme.fg("dim", path.basename(ctx.cwd));
227
- const gitSegment = formatGitSegment(theme);
228
- const ctxSegment = formatContextSegment(ctx, theme);
229
- const tokenSegment = theme.fg(
230
- "muted",
231
- formatTokenSegment(getTotals(ctx), "full"),
232
- );
233
-
234
- const leftParts = [modelSegment, dirSegment, gitSegment].filter(
235
- Boolean,
236
- );
237
- const left = leftParts.join(theme.fg("dim", " | "));
238
- const right = [ctxSegment, tokenSegment]
239
- .filter(Boolean)
240
- .join(theme.fg("dim", " | "));
241
- return [joinLeftRight(left, right, width)];
242
- },
243
- };
244
- });
245
- }
246
- ```
247
-
248
- - [ ] **Step 2: Add model/context/token helpers**
249
-
250
- Append helper functions:
251
-
252
- ```typescript
253
- function formatModelName(modelId: string): string {
254
- const lower = modelId.toLowerCase();
255
- const withoutProvider = lower.includes("/") ? lower.split("/").pop()! : lower;
256
-
257
- if (
258
- withoutProvider.includes("claude") &&
259
- withoutProvider.includes("sonnet")
260
- ) {
261
- if (withoutProvider.includes("4-5") || withoutProvider.includes("4.5"))
262
- return "sonnet-4.5";
263
- if (withoutProvider.includes("4")) return "sonnet-4";
264
- return "sonnet";
265
- }
266
- if (withoutProvider.includes("claude") && withoutProvider.includes("opus"))
267
- return "opus";
268
- if (withoutProvider.includes("gpt-5"))
269
- return withoutProvider.match(/gpt-5(?:[.-][a-z0-9]+)*/)?.[0] ?? "gpt-5";
270
- if (withoutProvider.includes("gpt-4"))
271
- return withoutProvider.match(/gpt-4(?:[.-][a-z0-9]+)*/)?.[0] ?? "gpt-4";
272
- if (withoutProvider.includes("gemini"))
273
- return withoutProvider.match(/gemini-[a-z0-9.-]+/)?.[0] ?? "gemini";
274
-
275
- return withoutProvider.length > 24
276
- ? `${withoutProvider.slice(0, 21)}…`
277
- : withoutProvider;
278
- }
279
-
280
- function getTotals(ctx: ExtensionContext): Totals {
281
- const totals: Totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
282
-
283
- for (const entry of ctx.sessionManager.getBranch()) {
284
- if (entry.type !== "message" || entry.message.role !== "assistant")
285
- continue;
286
- const message = entry.message as AssistantMessage;
287
- totals.input += message.usage?.input ?? 0;
288
- totals.output += message.usage?.output ?? 0;
289
- totals.cacheRead += message.usage?.cacheRead ?? 0;
290
- totals.cacheWrite += message.usage?.cacheWrite ?? 0;
291
- }
292
-
293
- return totals;
294
- }
295
-
296
- function formatTokenSegment(
297
- totals: Totals,
298
- mode: "full" | "no-cache" | "total-only",
299
- ): string {
300
- const total = totals.input + totals.output;
301
- if (mode === "total-only") return `Σ${formatCount(total)}`;
302
- const base = `↑${formatCount(totals.input)} ↓${formatCount(totals.output)} Σ${formatCount(total)}`;
303
- if (mode === "no-cache") return base;
304
- return `${base} ↯${formatCount(totals.cacheRead)} ↥${formatCount(totals.cacheWrite)}`;
305
- }
306
-
307
- function formatContextSegment(
308
- ctx: ExtensionContext,
309
- theme: ExtensionContext["ui"]["theme"],
310
- ): string {
311
- const usage = ctx.getContextUsage?.();
312
- const used = usage?.tokens ?? 0;
313
- const max = ctx.model?.contextWindow;
314
- const text = `ctx ${formatCount(used)}/${max ? formatCount(max) : "--"}`;
315
-
316
- if (!max || max <= 0) return theme.fg("dim", text);
317
- const ratio = used / max;
318
- if (ratio >= 0.85) return theme.fg("error", text);
319
- if (ratio >= 0.7) return theme.fg("warning", text);
320
- return theme.fg("success", text);
321
- }
322
-
323
- function formatCount(value: number): string {
324
- if (!Number.isFinite(value) || value <= 0) return "0";
325
- if (value < 1_000) return `${Math.round(value)}`;
326
- if (value < 1_000_000)
327
- return `${(value / 1_000).toFixed(value < 10_000 ? 1 : 0)}k`;
328
- return `${(value / 1_000_000).toFixed(1)}m`;
329
- }
330
- ```
331
-
332
- - [ ] **Step 3: Add join helper**
333
-
334
- Append:
335
-
336
- ```typescript
337
- function joinLeftRight(left: string, right: string, width: number): string {
338
- if (!right) return truncateToWidth(left, width);
339
- if (!left) return truncateToWidth(right, width);
340
-
341
- const gap = width - visibleWidth(left) - visibleWidth(right);
342
- if (gap >= 1) return truncateToWidth(left + " ".repeat(gap) + right, width);
343
-
344
- const half = Math.max(1, Math.floor((width - 1) / 2));
345
- return (
346
- truncateToWidth(left, half) + " " + truncateToWidth(right, width - half - 1)
347
- );
348
- }
349
- ```
350
-
351
- - [ ] **Step 4: Verify model/dir/context/tokens render**
352
-
353
- Run:
354
-
355
- ```text
356
- /reload
357
- ```
358
-
359
- Expected: footer shows smart model name, effort when known, `footer` as dir basename, `ctx used/max`, and token segment. No git yet.
360
-
361
- ---
362
-
363
- ### Task 3: Implement git refresh and git rendering
364
-
365
- **Files:**
366
-
367
- - Modify: `.pi/extensions/clean-footer/index.ts`
368
-
369
- - [ ] **Step 1: Replace git stubs with real git commands**
370
-
371
- Replace `refreshGit` with:
372
-
373
- ```typescript
374
- async function refreshGit(ctx: ExtensionContext, immediate = false) {
375
- try {
376
- const branchResult = await execFileAsync(
377
- "git",
378
- ["branch", "--show-current"],
379
- {
380
- cwd: ctx.cwd,
381
- timeout: 2_000,
382
- },
383
- );
384
- const statusResult = await execFileAsync("git", ["status", "--porcelain"], {
385
- cwd: ctx.cwd,
386
- timeout: 2_000,
387
- });
388
-
389
- const branch = branchResult.stdout.trim() || "detached";
390
- const dirtyCount = statusResult.stdout.split("\n").filter(Boolean).length;
391
- runtime.git = { inRepo: true, branch, dirtyCount };
392
- } catch {
393
- runtime.git = { inRepo: false, dirtyCount: 0 };
394
- }
395
-
396
- if (immediate) runtime.requestRender?.();
397
- }
398
- ```
399
-
400
- - [ ] **Step 2: Add git segment formatter**
401
-
402
- Append:
403
-
404
- ```typescript
405
- function formatGitSegment(
406
- theme: ExtensionContext["ui"]["theme"],
407
- ): string | undefined {
408
- if (!runtime.git.inRepo || !runtime.git.branch) return undefined;
409
- const branch = theme.fg("success", runtime.git.branch);
410
- if (runtime.git.dirtyCount <= 0) return branch;
411
- return `${branch} ${theme.fg("warning", `●${runtime.git.dirtyCount}`)}`;
412
- }
413
- ```
414
-
415
- - [ ] **Step 3: Verify git hidden/visible behavior**
416
-
417
- Manual checks:
418
-
419
- ```text
420
- /reload
421
- ```
422
-
423
- Expected inside repo: footer shows `main` or current branch. Modify any file, then wait after tool/write or run `/footer refresh`; expected dirty count updates, including untracked files. Outside repo, expected git segment absent.
424
-
425
- ---
426
-
427
- ### Task 4: Implement adaptive width tiers
428
-
429
- **Files:**
430
-
431
- - Modify: `.pi/extensions/clean-footer/index.ts`
432
-
433
- - [ ] **Step 1: Replace render body with tiered render**
434
-
435
- Inside `installFooter` render, replace current segment assembly with:
436
-
437
- ```typescript
438
- const model = formatModelName(ctx.model?.id ?? "no-model");
439
- const effort = runtime.thinkingLevel ? ` • ${runtime.thinkingLevel}` : "";
440
- const modelSegment = theme.fg("accent", `${model}${effort}`);
441
- const dirSegment = theme.fg("dim", path.basename(ctx.cwd));
442
- const gitSegment = formatGitSegment(theme);
443
- const ctxSegment = formatContextSegment(ctx, theme);
444
- const totals = getTotals(ctx);
445
-
446
- const leftFull = [modelSegment, dirSegment, gitSegment]
447
- .filter(Boolean)
448
- .join(theme.fg("dim", " | "));
449
- const leftMin = modelSegment;
450
-
451
- const full = joinLeftRight(
452
- leftFull,
453
- [ctxSegment, theme.fg("muted", formatTokenSegment(totals, "full"))].join(
454
- theme.fg("dim", " | "),
455
- ),
456
- width,
457
- );
458
- if (visibleWidth(full) <= width && width >= 100) return [full];
459
-
460
- const medium = joinLeftRight(
461
- leftFull,
462
- [ctxSegment, theme.fg("muted", formatTokenSegment(totals, "no-cache"))].join(
463
- theme.fg("dim", " | "),
464
- ),
465
- width,
466
- );
467
- if (visibleWidth(medium) <= width && width >= 80) return [medium];
468
-
469
- const small = joinLeftRight(
470
- leftFull,
471
- [
472
- ctxSegment,
473
- theme.fg("muted", formatTokenSegment(totals, "total-only")),
474
- ].join(theme.fg("dim", " | ")),
475
- width,
476
- );
477
- if (visibleWidth(small) <= width && width >= 60) return [small];
478
-
479
- const tiny = joinLeftRight(leftFull, ctxSegment, width);
480
- if (visibleWidth(tiny) <= width && width >= 40) return [tiny];
481
-
482
- return [joinLeftRight(leftMin, ctxSegment, width)];
483
- ```
484
-
485
- - [ ] **Step 2: Verify all tiers by resizing terminal**
486
-
487
- Manual expected results:
488
-
489
- - Wide terminal: cache fields visible.
490
- - Medium terminal: cache fields disappear first.
491
- - Small terminal: only total tokens remain.
492
- - Tiny terminal: tokens disappear, context remains.
493
- - Minimum terminal: only model and context remain.
494
-
495
- ---
496
-
497
- ### Task 5: Polish behavior and self-review
498
-
499
- **Files:**
500
-
501
- - Modify: `.pi/extensions/clean-footer/index.ts`
502
- - Modify if needed: `docs/superpowers/plans/2026-05-09-clean-footer-extension.md`
503
-
504
- - [ ] **Step 1: Verify command behavior**
505
-
506
- Run manually:
507
-
508
- ```text
509
- /footer
510
- /footer
511
- /footer refresh
512
- ```
513
-
514
- Expected:
515
-
516
- - First `/footer`: default footer restored.
517
- - Second `/footer`: clean footer restored.
518
- - `/footer refresh`: git state refreshes and notification appears in UI.
519
-
520
- - [ ] **Step 2: Verify no render-time git calls**
521
-
522
- Inspect `.pi/extensions/clean-footer/index.ts` and confirm `execFileAsync("git", ...)` occurs only in `refreshGit`, never in `render`.
523
-
524
- - [ ] **Step 3: Verify spec coverage**
525
-
526
- Checklist:
527
-
528
- - [ ] Project-local extension exists at `.pi/extensions/clean-footer/index.ts`.
529
- - [ ] Footer enabled by default.
530
- - [ ] Footer splits left/right.
531
- - [ ] Model smart-shortening exists.
532
- - [ ] Effort displays beside model as `low/med/high/xhigh` when available.
533
- - [ ] Directory displays basename only.
534
- - [ ] Git hides outside repo.
535
- - [ ] Git shows branch plus dirty count including untracked.
536
- - [ ] Git refresh is event-driven and debounced.
537
- - [ ] Context shows `used/max` with `--` fallback.
538
- - [ ] Context warning thresholds are implemented.
539
- - [ ] Tokens show cumulative input/output/total/cache read/cache write.
540
- - [ ] Adaptive tiers hide fields in requested order.
541
- - [ ] `/footer` and `/footer refresh` work.
542
- - [ ] Non-UI contexts avoid throwing.
543
-
544
- - [ ] **Step 4: Keep workspace uncommitted**
545
-
546
- Do not run `git commit`. Leave created/modified files in workspace for user review.
@@ -1,149 +0,0 @@
1
- # Clean Footer Extension Design
2
-
3
- ## Goal
4
-
5
- Create a project-local pi footer extension that replaces the built-in footer with a cleaner, adaptive footer focused on model identity, reasoning effort, project context, git state, context usage, and cumulative token/cache telemetry.
6
-
7
- ## Placement
8
-
9
- Use a project-local extension:
10
-
11
- ```text
12
- .pi/extensions/clean-footer/index.ts
13
- ```
14
-
15
- This keeps development local to the current repo and supports hot reload through `/reload`. The extension should have no runtime dependencies beyond pi's extension/TUI APIs unless implementation proves a helper is necessary.
16
-
17
- ## Footer Layout
18
-
19
- The footer is split into left and right regions with adaptive spacing.
20
-
21
- Left side:
22
-
23
- ```text
24
- model • effort | dir | branch ●N
25
- ```
26
-
27
- Right side:
28
-
29
- ```text
30
- ctx used/max | ↑input ↓output Σtotal ↯cacheRead ↥cacheWrite
31
- ```
32
-
33
- Example full-width render:
34
-
35
- ```text
36
- sonnet-4.5 • high | footer | main ●3 ctx 84k/200k | ↑12.4k ↓3.1k Σ15.5k ↯8.2k ↥1.0k
37
- ```
38
-
39
- ## Field Semantics
40
-
41
- ### Model and effort
42
-
43
- - Display a smart-short model name when known, with pattern-based shortening for common model IDs.
44
- - Fallback to truncated exact model ID when no smart rule matches.
45
- - Display reasoning effort directly beside model name as `low`, `med`, `high`, or `xhigh`.
46
- - Format: `model • effort`.
47
-
48
- ### Directory
49
-
50
- - Display only the basename of `ctx.cwd`.
51
- - Do not show parent path or full path.
52
-
53
- ### Git
54
-
55
- - Hide git segment entirely outside a git repository.
56
- - Inside a repo, display branch plus dirty count.
57
- - Dirty count is the number of lines from `git status --porcelain`, including untracked files.
58
- - Format: `branch ●N` when dirty, `branch` when clean.
59
- - Git refresh is event-driven and debounced, not run on every render.
60
-
61
- ### Context usage
62
-
63
- - Display used/max tokens: `ctx 84k/200k`.
64
- - If max context is unknown, display `ctx 84k/--`.
65
- - Color thresholds:
66
- - below 70%: normal
67
- - 70% through 84%: warning/yellow
68
- - 85% and above: danger/red
69
- - unknown max: dim/neutral
70
-
71
- ### Token and cache telemetry
72
-
73
- - Use cumulative totals across the active session branch.
74
- - Display input, output, total, cache read, and cache write.
75
- - Format: `↑12k ↓3k Σ15k ↯8k ↥1k`.
76
-
77
- ## Adaptive Width Tiers
78
-
79
- The footer must degrade gracefully as terminal width shrinks.
80
-
81
- 1. Full: model/effort, directory, git, context used/max, input/output/total/cache read/cache write.
82
- 2. Medium: hide cache read/write.
83
- 3. Small: hide input/output breakdown and show total only.
84
- 4. Tiny: show left side plus context only.
85
- 5. Minimum: show model and context only.
86
-
87
- Truncation should prefer preserving meaningful whole segments rather than cramming all fields.
88
-
89
- ## Refresh and Invalidation
90
-
91
- - Install footer on `session_start` when `ctx.hasUI` is true.
92
- - Request render on relevant events, without a periodic timer.
93
- - Refresh git state:
94
- - at startup/reload
95
- - after tool executions that may affect files: `bash`, `edit`, `write`
96
- - after user `!`/`!!` bash commands
97
- - Debounce git refresh by roughly 500 ms.
98
- - Do not execute git commands from footer `render()`.
99
-
100
- ## Commands
101
-
102
- Register `/footer` command behavior:
103
-
104
- - `/footer`: toggle custom footer on/off.
105
- - `/footer refresh`: force git refresh and re-render.
106
-
107
- The footer should be enabled by default when the extension loads in an interactive UI.
108
-
109
- ## Non-Interactive Behavior
110
-
111
- If `ctx.hasUI` is false, the extension should silently no-op for footer setup and avoid throwing. Commands should also avoid crashing in non-interactive, RPC, print, or JSON contexts.
112
-
113
- ## Style
114
-
115
- Use color-coded segments, but keep colors restrained and theme-driven:
116
-
117
- - model/effort: accent-like color
118
- - directory: dim/muted
119
- - git branch: success/green-ish; dirty marker/count: warning/accent
120
- - context: threshold-based normal/warning/danger
121
- - tokens/cache: muted/info style
122
-
123
- ## Error Handling
124
-
125
- - Git command failures should clear/hide git state rather than displaying errors in footer.
126
- - Missing model/context metadata should use documented fallbacks.
127
- - Extension reload should restore built-in footer when disabled or on cleanup if necessary.
128
-
129
- ## Testing Strategy
130
-
131
- Manual checks:
132
-
133
- 1. Load extension with `/reload` and verify footer replaces built-in footer.
134
- 2. Toggle with `/footer`.
135
- 3. Force refresh with `/footer refresh`.
136
- 4. Verify basename directory display.
137
- 5. Verify git branch and dirty count update after file edits and bash changes.
138
- 6. Verify git segment hides outside a repo.
139
- 7. Verify context/token fields update after assistant responses.
140
- 8. Resize terminal to exercise all width tiers.
141
- 9. Run in non-interactive mode or no-UI context if available and confirm no crash.
142
-
143
- ## Out of Scope
144
-
145
- - Persistent user config file.
146
- - NPM package publishing.
147
- - Provider-specific exhaustive model alias registry.
148
- - Polling-based live refresh.
149
- - Full git status counts or untracked split counts.
package/tok DELETED
@@ -1 +0,0 @@
1
- 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