pi-minimal-footer 0.7.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-10-28
11
+
12
+ ### Added
13
+
14
+ - Optional percentage context display, e.g. `24.7%/500k`, enabled with `minFooter.contextAsPercentage`
15
+
16
+ ### Changed
17
+
18
+ - Clearer default context counts, e.g. `0.78k/500k`, `1.0k/500k`, and `124k/500k`, with explicit units on both values
19
+
20
+ ## [0.7.1] - 2026-10-16
21
+
22
+ ### Fixed
23
+
24
+ - Use the correct Copilot account credentials when displaying quota usage
25
+ - Keep extension statuses on one footer line when they contain newlines or tabs
26
+
27
+ - Cover Pi 1.3.0 in compatibility checks
28
+
10
29
  ## [0.7.0] - 2026-10-13
11
30
 
12
31
  ### Fixed
@@ -254,7 +273,9 @@ First npm-ready release.
254
273
 
255
274
  - Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
256
275
 
257
- [Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.7.0...HEAD
276
+ [Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.8.0...HEAD
277
+ [0.8.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.7.1...v0.8.0
278
+ [0.7.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.7.0...v0.7.1
258
279
  [0.7.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.6.0...v0.7.0
259
280
  [0.6.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.3...v0.6.0
260
281
  [0.5.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.2...v0.5.3
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  An opinionated, compact, one-line status bar for [Pi](https://github.com/earendil-works/pi).
11
11
 
12
- ![Footer preview: ~/git/project   main  🧠 Karpathy  🪽 Icarus  model · 42/200k · [⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h](media/preview.png)
12
+ ![Footer preview: ~/git/project   main  🧠 Karpathy  🪽 Icarus  model · 42k/200k · [⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h](media/preview.png)
13
13
 
14
14
  **Left**
15
15
 
@@ -20,7 +20,7 @@ An opinionated, compact, one-line status bar for [Pi](https://github.com/earendi
20
20
 
21
21
  - Extension statuses: ` 🧠 Karpathy  🪽 Icarus`
22
22
  - Model: `model`
23
- - Context usage: `42/200k`
23
+ - Context usage: `42k/200k` (default), or `21.0%/200k` with `contextAsPercentage: true`
24
24
  - Subscription usage bar with reset time: `[⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h`
25
25
 
26
26
  ## Install
@@ -58,6 +58,7 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
58
58
  "showPath": true,
59
59
  "showModel": true,
60
60
  "showContext": true,
61
+ "contextAsPercentage": false,
61
62
  "powerlineSeparator": true,
62
63
  "gitRefreshSeconds": 91,
63
64
  "gitFetch": false,
@@ -82,7 +83,7 @@ Counts require a configured upstream. Unsynced branches use normal text color; s
82
83
 
83
84
  ### Context
84
85
 
85
- `42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
86
+ `42k/200k` means tokens used / context window. Set `"contextAsPercentage": true` under `minFooter` to show `21.0%/200k` instead. Unknown usage shows `?`.
86
87
 
87
88
  ### Subscription usage
88
89
 
package/VISION.md CHANGED
@@ -36,6 +36,13 @@ Two cells give 6.25% steps; ten give 1.25%. Fill rounds to the nearest eighth-ce
36
36
 
37
37
  The bar and reset label have no intervening space. Reset formatting uses local clock time through 24 hours, remaining whole days/hours above 24 hours, and whole days above 10 days. Exactly 24 hours stays clock time; exactly 10 days is `↻10d0h`. Missing reset metadata leaves the bar visible without a label.
38
38
 
39
+ ## Context display contract
40
+
41
+ - Default to tokens used / context window, with an explicit `k` suffix on both values. Never deduplicate units across the slash.
42
+ - Always use kilos, including for million-token windows (`1000k`, not `1M`). Below 10k, use two significant digits where one-token resolution permits, capped at three decimal places in kilos: `0.001k`, `0.012k`, `0.11k`, `0.78k`, `0.99k`, `1.0k`, `9.0k`. At and above 10k, round to whole kilos: `10k`, `124k`, `1000k`. Zero is `0k`; rounding can promote `9.999k` to `10k`.
43
+ - `minFooter.contextAsPercentage` defaults to `false`. Opting in replaces only the numerator with percentage used to one decimal place, e.g. `24.7%/500k`. Keep context distinct from the subscription usage bar.
44
+ - Unknown token usage is `?/500k` in either mode; absent context usage is `?`. Never present unknown usage as zero.
45
+
39
46
  ## Git contract
40
47
 
41
48
  - Pi supplies the branch name. Query `git rev-list --left-right --count HEAD...@{upstream}` for `↑N` local-only and `↓N` upstream-only commits. Use local refs; no remote fetch by default.
@@ -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 · 12k/128k · [⣿⣶⠀⠀⠀]↻16:40
5
5
  *
6
6
  * Settings are persisted in the agent directory (usually ~/.pi/agent)
7
7
  * settings.json under "minFooter".
@@ -31,6 +31,7 @@ interface Settings {
31
31
  showPath?: boolean;
32
32
  showModel?: boolean;
33
33
  showContext?: boolean;
34
+ contextAsPercentage?: boolean;
34
35
  powerlineSeparator?: boolean;
35
36
  gitRefreshSeconds?: number;
36
37
  gitFetch?: boolean;
@@ -52,6 +53,7 @@ const DEFAULT_SETTINGS: FooterSettings = {
52
53
  showPath: true,
53
54
  showModel: true,
54
55
  showContext: true,
56
+ contextAsPercentage: false,
55
57
  powerlineSeparator: true,
56
58
  gitRefreshSeconds: 91,
57
59
  gitFetch: false,
@@ -111,9 +113,11 @@ function writeEnabled(v: boolean): void {
111
113
 
112
114
  function formatSize(n: number | null | undefined): string {
113
115
  if (n == null) return "?";
114
- if (n >= 1_000_000) return `${Math.round(n / 1_000_000)}M`;
115
- if (n >= 1_000) return `${Math.round(n / 1_000)}k`;
116
- return String(n);
116
+ const kilos = n / 1_000;
117
+ if (n === 0 || n >= 10_000) return `${Math.round(kilos)}k`;
118
+ const rounded = Number(kilos.toPrecision(2));
119
+ const decimals = Math.min(3, Math.max(0, 1 - Math.floor(Math.log10(rounded))));
120
+ return `${rounded.toFixed(decimals)}k`;
117
121
  }
118
122
 
119
123
  /** Abbreviate only an exact home match or a boundary-prefixed child path. */
@@ -166,7 +170,7 @@ async function updateGitSync(state: FooterState): Promise<boolean> {
166
170
  return changed;
167
171
  }
168
172
 
169
- function updateState(ctx: ExtensionContext, state: FooterState): void {
173
+ function updateState(ctx: ExtensionContext, state: FooterState, config: FooterSettings): void {
170
174
  const cwd = ctx.cwd ?? process.cwd();
171
175
  if (state.cwd !== cwd) {
172
176
  state.gitSync = "";
@@ -177,16 +181,11 @@ function updateState(ctx: ExtensionContext, state: FooterState): void {
177
181
  state.model = ctx.model?.id ?? "no-model";
178
182
  const usage = ctx.getContextUsage();
179
183
  if (usage) {
180
- const fTokens = formatSize(usage.tokens);
184
+ const fTokens = config.contextAsPercentage === true
185
+ ? (usage.tokens == null || usage.contextWindow <= 0 ? "?" : `${(usage.tokens / usage.contextWindow * 100).toFixed(1)}%`)
186
+ : formatSize(usage.tokens);
181
187
  const fWindow = formatSize(usage.contextWindow);
182
- const lastT = fTokens[fTokens.length - 1];
183
- const lastW = fWindow[fWindow.length - 1];
184
- // Deduplicate k/M suffix: show suffix only on denominator when both use the same unit
185
- if ((lastT === "k" || lastT === "M") && lastT === lastW) {
186
- state.context = `${fTokens.slice(0, -1)}/${fWindow}`;
187
- } else {
188
- state.context = `${fTokens}/${fWindow}`;
189
- }
188
+ state.context = `${fTokens}/${fWindow}`;
190
189
  } else {
191
190
  state.context = "?";
192
191
  }
@@ -384,7 +383,7 @@ export default function (pi: ExtensionAPI) {
384
383
  return {
385
384
  render(width: number): string[] {
386
385
  const statuses = config.showSkills
387
- ? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
386
+ ? [...footerData.getExtensionStatuses().values()].map((s) => s.replace(/[\r\n\t]/g, " ")).filter((s) => s.trim())
388
387
  : [];
389
388
  const branch = config.showGitBranch ? footerData.getGitBranch() : null;
390
389
  const branchText = branch ? ` ${branch}${state.gitSync}` : "";
@@ -420,7 +419,7 @@ export default function (pi: ExtensionAPI) {
420
419
  }
421
420
 
422
421
  function refresh(ctx: ExtensionContext): void {
423
- updateState(ctx, state);
422
+ updateState(ctx, state, config);
424
423
  refreshFooter();
425
424
  }
426
425
 
@@ -431,7 +430,7 @@ export default function (pi: ExtensionAPI) {
431
430
  !("enabled" in data) || typeof data.enabled !== "boolean") return;
432
431
  if (enabled === data.enabled && (!enabled || disposeFooter !== null)) return;
433
432
  enabled = data.enabled;
434
- updateState(currentCtx, state);
433
+ updateState(currentCtx, state, config);
435
434
  install(currentCtx);
436
435
  refreshFooter();
437
436
  });
@@ -440,7 +439,7 @@ export default function (pi: ExtensionAPI) {
440
439
  currentCtx = ctx;
441
440
  enabled = readConfig().enabled !== false;
442
441
  usageLimits.stop();
443
- updateState(ctx, state);
442
+ updateState(ctx, state, config);
444
443
  install(ctx);
445
444
  refreshFooter();
446
445
  });
@@ -493,7 +492,7 @@ export default function (pi: ExtensionAPI) {
493
492
  else if (args === "off") enabled = false;
494
493
  else enabled = !enabled;
495
494
  writeEnabled(enabled);
496
- updateState(ctx, state);
495
+ updateState(ctx, state, config);
497
496
  install(ctx);
498
497
  refreshFooter();
499
498
  ctx.ui.notify(`Minimal footer ${enabled ? "enabled" : "disabled"}`, "info");
@@ -98,7 +98,9 @@ export async function usageRequest(ctx: ExtensionContext, model: NonNullable<Ext
98
98
  } else if (provider === "github-copilot") {
99
99
  // Quota API needs the GitHub login token, not the exchanged inference token.
100
100
  const refresh = storedOAuth(provider).refresh;
101
- token = auth.env?.COPILOT_GITHUB_TOKEN ?? process.env.COPILOT_GITHUB_TOKEN ?? (typeof refresh === "string" ? refresh : undefined);
101
+ if (ctx.modelRegistry.isUsingOAuth?.(model) ?? typeof refresh === "string") {
102
+ token = typeof refresh === "string" ? refresh : undefined;
103
+ }
102
104
  Object.assign(headers, { "Editor-Version": "vscode/1.96.2", "User-Agent": "GitHubCopilotChat/0.26.7", "X-Github-Api-Version": "2025-04-01" });
103
105
  } else if (provider === "google-gemini-cli" && token?.startsWith("{")) {
104
106
  const credentials = record(JSON.parse(token));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-minimal-footer",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "An opinionated minimal Pi footer - one-line status bar showing subscription usage, path, status, model and context.",
5
5
  "keywords": [
6
6
  "footer",