pi-minimal-footer 0.3.3 → 0.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.1] - 2026-10-07
11
+
12
+ ### Changed
13
+
14
+ - Restructure README with separate left/right feature lists, clearer context and subscription usage guidance, explicit adapter verification limits, and source installation under development
15
+ - Refresh the vision contract for the two-cell baseline, bracketed bars, and nine-second timeout
16
+ - Explain cache retention, refresh jitter, exact-model quota selection, and readability thresholds with rationale comments; update the source footer example to show brackets
17
+
18
+ ### Fixed
19
+
20
+ - Restore reset-label-only usage on terminals too narrow for a bracketed cell, with regression coverage for reset-only, hidden-usage, and one-cell width boundaries
21
+ - Align usage detection and regression expectations with bracketed bars, the two-cell baseline, bracket-aware width allocation, and ten-cell rounding for small percentages
22
+
23
+ ## [0.4.0] - 2026-10-07
24
+
25
+ ### Added
26
+
27
+ - `minFooter.maxUsageBarCells` (default `10`) to cap the subscription usage bar length; use a positive integer such as `4` for a compact bar, excluding brackets and reset time
28
+
29
+ ### Changed
30
+
31
+ - Reduce the compact subscription usage bar baseline from five to two cells before expanding into spare space up to the configured cap
32
+ - Add one space at each footer edge, reserving those columns when fitting narrow terminals
33
+ - Refresh README and package-gallery PNG previews with the current footer, edge padding, and live OpenAI Codex subscription usage
34
+
10
35
  ## [0.3.3] - 2026-10-07
11
36
 
12
37
  ### Changed
package/README.md CHANGED
@@ -9,18 +9,26 @@
9
9
 
10
10
  An opinionated, compact one-line footer for [Pi](https://github.com/earendil-works/pi).
11
11
 
12
- Path and branch on the left; extension statuses, model, context, and subscription usage bar on the right. Shrinks to fit narrow terminals. Interactive terminal UI only.
12
+ ![Footer preview: ~/git/project main  🧠 Karpathy 🪽 Icarus model · 42/200k · [⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h](media/github-preview.png)
13
13
 
14
- ![Footer preview: ~/git/project   main  🧠 Karpathy  🪽 Icarus  model · 42/200k · ⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀↻1d8h](media/github-preview.png)
14
+ **Left**
15
+
16
+ - Working directory: `~/git/project`
17
+ - Git branch: ` main`
18
+
19
+ **Right**
20
+
21
+ - Extension statuses: ` 🧠 Karpathy  🪽 Icarus`
22
+ - Model: `model`
23
+ - Context usage: `42/200k`
24
+ - Subscription usage bar with reset time: `[⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h`
15
25
 
16
26
  ## Install
17
27
 
18
- Requires Node.js >=22.19.0.
28
+ Requires Node.js >=22.19.0. Interactive terminal UI only.
19
29
 
20
30
  ```bash
21
31
  pi install npm:pi-minimal-footer
22
- # Or install from source:
23
- pi install git:github.com/Ryu-CZ/pi-minimal-footer
24
32
  ```
25
33
 
26
34
  ## Commands
@@ -44,27 +52,42 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
44
52
  "showPath": true,
45
53
  "showModel": true,
46
54
  "showContext": true,
47
- "powerlineSeparator": true
55
+ "powerlineSeparator": true,
56
+ "maxUsageBarCells": 10
48
57
  }
49
58
  }
50
59
  ```
51
60
 
52
61
  - `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
53
62
  - Set `powerlineSeparator` to `false` for plain spaces instead of `` / `` if your font lacks those glyphs. The git icon `` also needs a compatible font; hide it with `showGitBranch: false` if needed.
54
- - Context `42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
63
+ - `maxUsageBarCells` caps the Braille bar length (excluding brackets and reset time). Use a positive integer, e.g. `4` for a compact bar; invalid values fall back to `10`.
64
+
65
+ ## Reading the footer
66
+
67
+ ### Context
55
68
 
56
- ## Reading subscription usage
69
+ `42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
57
70
 
58
- `⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀↻1d8h` shows **subscription allowance used**, not context usage.
71
+ ### Subscription usage
59
72
 
60
- - Bar: shown as `[cells]` with 5–10 cells, expanding into spare space; green below 85%, amber from 85%, red from 92%. The brackets keep the bar’s beginning and end visible even when it is empty. Dim means cached after a refresh failure. Very narrow terminals may show fewer cells or only the reset label.
73
+ `[⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h` shows **subscription allowance used**, not context usage.
74
+
75
+ - **Bar:** expands into spare space, using 2–10 cells by default, capped by `maxUsageBarCells`. Smaller caps and very narrow terminals may show fewer cells or only the reset label.
76
+ - **Colors:** green below 85%, amber from 85%, red from 92%.
77
+ - **Cached data:** dim after a refresh failure.
61
78
  - Reset: local `↻HH:mm` through 24 hours; whole days/hours above 24 hours (`↻1d8h`); whole days above 10 days (`↻12d`). Remaining durations round down. No reset reported means bar only.
62
79
  - Shows the shortest available usage window for the selected provider. Missing credentials, unsupported/local models, or absent limits hide the usage bar. Custom proxy endpoints are not polled.
63
80
 
64
- Supported adapters: **OpenAI Codex, Claude OAuth, GitHub Copilot, Gemini CLI, MiniMax, Kimi Coding, and OpenCode Go**. Uses Pi's existing credentials; ordinary Claude API keys do not expose subscription usage. Provider availability varies; authenticated endpoints remain unverified live.
81
+ Implemented adapters: **OpenAI Codex, Claude OAuth, GitHub Copilot, Gemini CLI, MiniMax, Kimi Coding, and OpenCode Go**. Uses Pi's existing credentials; ordinary Claude API keys do not expose subscription usage. Authenticated endpoints remain unverified live; see [verification limits](VISION.md).
65
82
 
66
83
  ## Development & design
67
84
 
85
+ Install from source:
86
+
87
+ ```bash
88
+ pi install git:github.com/Ryu-CZ/pi-minimal-footer
89
+ ```
90
+
68
91
  See [VISION.md](VISION.md) for design constraints, local development, provider details, and verification limits. Release history: [CHANGELOG.md](CHANGELOG.md).
69
92
 
70
93
  ## Credits
package/VISION.md CHANGED
@@ -18,9 +18,9 @@ The [README](README.md) is the user manual. This document records current design
18
18
 
19
19
  Reserve the usage bar and model/context before allocating location and statuses. Statuses and location may shorten or disappear; on sufficiently narrow terminals, model/context can also shorten or disappear.
20
20
 
21
- Lay out other fields with a five-cell usage bar first. Grow the bar only into leftover columns, up to ten cells—never truncate another field solely to enlarge it. At extreme widths, shrink below five cells, show only the reset label, or hide the usage bar if the label cannot fit.
21
+ Lay out other fields with a two-cell usage bar first. Grow the bar only into leftover columns, up to ten cells—never truncate another field solely to enlarge it. At extreme widths, shrink to one cell, show only the reset label, or hide usage if neither can fit. Brackets keep an empty bar's edges visible and count toward its width, but not its cell cap.
22
22
 
23
- Five cells give 2.5% steps; ten give 1.25%. Fill rounds to the nearest eighth-cell step. Only the final partially filled cell uses an intermediate shade:
23
+ Two cells give 6.25% steps; ten give 1.25%. Fill rounds to the nearest eighth-cell step. Only the final partially filled cell uses an intermediate shade:
24
24
 
25
25
  | Cell | Fill |
26
26
  |---|---|
@@ -57,7 +57,7 @@ Reuse Pi's selected-provider credentials. Copilot uses the GitHub login token fr
57
57
  - Consume passive Codex response headers/stream events and Claude quota headers when available.
58
58
  - Otherwise fetch on startup/provider switch, then roughly every four minutes without a fresh response update. Sample independent uniform ±⅛ jitter (3.5–4.5 minutes) for each attempt or fresh displayed-window update to spread fallback requests across sessions. Keep that deadline across unrelated window updates; a reported reset time still triggers an earlier refresh.
59
59
  - Merge partial windows. A weekly-only signal must not replace a cached shorter window or postpone its refresh.
60
- - Bound authentication, fetch, and response-body parsing by a shared five-second timeout. Release stalled work so polling can recover.
60
+ - Bound authentication, fetch, and response-body parsing by a shared nine-second timeout. Release stalled work so polling can recover.
61
61
  - Provider switches clear usage immediately, cancel pending work, and invalidate late results. Gemini model switches also refresh model-specific usage.
62
62
  - Missing credentials and explicit empty/unlimited responses clear usage. Temporary authentication, network, or parsing failures retain only the current provider's cached usage, dimmed; without cache, hide it.
63
63
  - Invalid optional reset metadata must not discard otherwise valid usage. Zero usage is an empty bar, not an absent usage bar.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Minimal footer — replaces pi's default footer with a clean status line:
3
3
  *
4
- * ~/path/to/dir   main  status1  status2  sonnet · 12/128k · ⣿⣶⠀⠀⠀↻16:40
4
+ * ~/path/to/dir   main  status1  status2  sonnet · 12/128k · [⣿⣶⠀⠀⠀]↻16:40
5
5
  *
6
6
  * Settings are persisted in the agent directory (usually ~/.pi/agent)
7
7
  * settings.json under "minFooter".
@@ -30,6 +30,7 @@ interface Settings {
30
30
  showModel?: boolean;
31
31
  showContext?: boolean;
32
32
  powerlineSeparator?: boolean;
33
+ maxUsageBarCells?: number;
33
34
  };
34
35
  }
35
36
 
@@ -43,6 +44,7 @@ const DEFAULT_SETTINGS: FooterSettings = {
43
44
  showModel: true,
44
45
  showContext: true,
45
46
  powerlineSeparator: true,
47
+ maxUsageBarCells: MAX_BAR_CELLS,
46
48
  };
47
49
 
48
50
  function settingsPath(): string {
@@ -74,7 +76,11 @@ function writeSettings(patch: Partial<Settings>): void {
74
76
 
75
77
  function readConfig(): FooterSettings {
76
78
  const s = readSettings();
77
- return { ...DEFAULT_SETTINGS, ...s.minFooter };
79
+ const config = { ...DEFAULT_SETTINGS, ...s.minFooter };
80
+ if (typeof config.maxUsageBarCells !== "number" || !Number.isSafeInteger(config.maxUsageBarCells) || config.maxUsageBarCells < 1) {
81
+ config.maxUsageBarCells = MAX_BAR_CELLS;
82
+ }
83
+ return config;
78
84
  }
79
85
 
80
86
  function writeEnabled(v: boolean): void {
@@ -135,18 +141,21 @@ function updateState(ctx: ExtensionContext, state: FooterState): void {
135
141
  const MODEL_GAP = " · ";
136
142
  const USAGE_GAP = " · ";
137
143
  const LOCATION_GAP_WIDTH = 3;
144
+ // Below these budgets, ellipses dominate text and status tabs stop being useful at a glance.
138
145
  const MIN_TEXT_WIDTH = 4;
139
146
  const MIN_STATUS_WIDTH = 12;
140
147
 
141
148
  function buildLine(
142
149
  width: number, path: string, statuses: string, branch: string, model: string, context: string,
143
150
  usage: (available: number, maxCells?: number) => string | null,
144
- statusSeparator: string, locationSeparator: string,
151
+ statusSeparator: string, locationSeparator: string, maxUsageBarCells: number,
145
152
  ): string {
146
153
  if (width <= 0) return "";
154
+ if (width <= 2) return " ".repeat(width);
155
+ width -= 2;
147
156
  let core = [model, context].filter(Boolean).join(MODEL_GAP);
148
157
  // Budget the compact bar first; its expanded size must not drive truncation decisions.
149
- const usageBar = usage(width) ?? "";
158
+ const usageBar = usage(width, Math.min(BASE_BAR_CELLS, maxUsageBarCells)) ?? "";
150
159
  const usageGapWidth = usageBar && core ? USAGE_GAP.length : 0;
151
160
  const coreBudget = Math.max(0, width - visibleWidth(usageBar) - usageGapWidth);
152
161
  if (usageBar && coreBudget < MIN_TEXT_WIDTH) {
@@ -182,11 +191,11 @@ function buildLine(
182
191
  // A dropped left group needs no divider; those columns belong to the usage bar instead.
183
192
  const interGroupGapWidth = left && right ? LOCATION_GAP_WIDTH : 0;
184
193
  const spareWidth = Math.max(0, width - visibleWidth(left) - visibleWidth(right) - interGroupGapWidth);
185
- const expandedUsageBar = usage(visibleWidth(usageBar) + spareWidth, MAX_BAR_CELLS) ?? usageBar;
194
+ const expandedUsageBar = usage(visibleWidth(usageBar) + spareWidth, maxUsageBarCells) ?? usageBar;
186
195
  const expandedCore = [core, expandedUsageBar].filter(Boolean).join(USAGE_GAP);
187
196
  right = [fittedStatuses, expandedCore].filter(Boolean).join(statusSeparator);
188
197
  }
189
- return left + " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right))) + right;
198
+ return " " + left + " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right))) + right + " ";
190
199
  }
191
200
 
192
201
  // ── Extension ─────────────────────────────────────────────────────────
@@ -251,6 +260,7 @@ export default function (pi: ExtensionAPI) {
251
260
  (available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
252
261
  statusSeparator,
253
262
  config.powerlineSeparator ? theme.fg("dim", "  ") : " ",
263
+ config.maxUsageBarCells ?? MAX_BAR_CELLS,
254
264
  );
255
265
  return [line];
256
266
  },
@@ -14,7 +14,7 @@ const CELL_STEPS = 8;
14
14
  const PARTIAL_CELL_FILLS = ["", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣷"];
15
15
 
16
16
  // Start compact so extra precision never steals space from the other footer fields.
17
- export const BASE_BAR_CELLS = 5;
17
+ export const BASE_BAR_CELLS = 2;
18
18
  export const MAX_BAR_CELLS = 10;
19
19
 
20
20
  function formatResetLabel(resetAt: number | null, now: number): string {
@@ -186,6 +186,7 @@ export class UsageLimits {
186
186
  this.render();
187
187
  }
188
188
 
189
+ // Independent deadlines spread fallback requests across sessions sharing a subscription.
189
190
  private randomizeRefresh(): void {
190
191
  this.refreshDelay = REFRESH_MS + (Math.random() * 2 - 1) * REFRESH_JITTER_MS;
191
192
  }
@@ -246,8 +247,11 @@ export class UsageLimits {
246
247
  if (!this.ctx || !window) return null;
247
248
  const now = Date.now();
248
249
  const resetLabel = formatResetLabel(window.resetAt, now);
249
- // Reserve two columns for brackets so an empty bar still has clear edges.
250
- if (width < (resetLabel.length || 1) + 3) return null;
250
+ // Keep the reset useful when the terminal cannot fit even one bracketed cell.
251
+ if (width < resetLabel.length + 3) {
252
+ return resetLabel && width >= resetLabel.length ? theme.fg("dim", resetLabel) : null;
253
+ }
254
+ // Brackets keep the edges of a zero-usage bar visible.
251
255
  const cells = Math.min(maxCells, Math.max(1, width - resetLabel.length - 2));
252
256
  const steps = Math.round(window.used / 100 * cells * CELL_STEPS);
253
257
  const filled = "⣿".repeat(Math.floor(steps / CELL_STEPS)) + PARTIAL_CELL_FILLS[steps % CELL_STEPS];
@@ -136,6 +136,8 @@ export function parseUsageWindows(provider: string, model: string, payload: unkn
136
136
  if (window) windows[key] = window;
137
137
  else malformed = true;
138
138
  };
139
+ // Valid siblings remain useful; explicit absence clears cache, while malformed
140
+ // snapshots throw so a temporary provider failure retains dimmed cached usage.
139
141
  const finish = () => {
140
142
  if (Object.keys(windows).length) return windows;
141
143
  if (recognized && !malformed) return null;
@@ -247,6 +249,7 @@ export function parseUsageWindows(provider: string, model: string, payload: unkn
247
249
  if (!ok) malformed = true;
248
250
  return ok;
249
251
  });
252
+ // A malformed exact match must not substitute another model family's quota.
250
253
  const hasExact = buckets.some((b) => b.modelId === model);
251
254
  const family = model.toLowerCase().includes("flash") ? "flash" : model.toLowerCase().includes("pro") ? "pro" : null;
252
255
  const selected = valid.filter((b) => hasExact ? b.modelId === model : family && String(b.modelId).toLowerCase().includes(family));
Binary file
package/media/preview.png CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimal-footer",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "An opinionated minimal footer extension for Pi — clean status line with path, extension statuses, model, and context usage.",
5
5
  "keywords": [
6
6
  "pi-package",