pi-minimal-footer 0.3.2 → 0.4.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 +22 -1
- package/README.md +7 -5
- package/VISION.md +1 -1
- package/extensions/index.ts +14 -5
- package/extensions/lib/usage-limits.ts +16 -6
- package/media/github-preview.png +0 -0
- package/media/preview.png +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `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
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Reduce the compact subscription usage bar baseline from five to two cells before expanding into spare space up to the configured cap
|
|
19
|
+
- Add one space at each footer edge, reserving those columns when fitting narrow terminals
|
|
20
|
+
- Refresh README and package-gallery PNG previews with the current footer, edge padding, and live OpenAI Codex subscription usage
|
|
21
|
+
|
|
22
|
+
## [0.3.3] - 2026-10-07
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Add square brackets around the subscription usage bar so its beginning and end remain visible when empty
|
|
27
|
+
- Randomize subscription usage fallback refreshes by ±⅛ of the four-minute interval (3.5–4.5 minutes), preserving reset-time checks and deadlines across unrelated window updates
|
|
28
|
+
- Raise the subscription usage request timeout from five to nine seconds before aborting stalled authentication, fetch, or response-body waits
|
|
29
|
+
|
|
10
30
|
## [0.3.2] - 2026-10-06
|
|
11
31
|
|
|
12
32
|
### Changed
|
|
@@ -147,7 +167,8 @@ First npm-ready release.
|
|
|
147
167
|
|
|
148
168
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
149
169
|
|
|
150
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.
|
|
170
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.3...HEAD
|
|
171
|
+
[0.3.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.2...v0.3.3
|
|
151
172
|
[0.3.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.1...v0.3.2
|
|
152
173
|
[0.3.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.0...v0.3.1
|
|
153
174
|
[0.3.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.2.0...v0.3.0
|
package/README.md
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
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
|
+
Path and branch on the left; extension statuses, model, context, and subscription usage bar on the right. Shrinks to fit narrow terminals, with one space of padding at each edge. Interactive terminal UI only.
|
|
13
13
|
|
|
14
|
-
![Footer preview: ~/git/project main 🧠 Karpathy 🪽 Icarus model · 42/200k ·
|
|
14
|
+
![Footer preview: ~/git/project main 🧠 Karpathy 🪽 Icarus model · 42/200k · [⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h](media/github-preview.png)
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -44,20 +44,22 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
44
44
|
"showPath": true,
|
|
45
45
|
"showModel": true,
|
|
46
46
|
"showContext": true,
|
|
47
|
-
"powerlineSeparator": true
|
|
47
|
+
"powerlineSeparator": true,
|
|
48
|
+
"maxUsageBarCells": 10
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
- `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
|
|
53
54
|
- 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.
|
|
55
|
+
- `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`.
|
|
54
56
|
- Context `42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
|
|
55
57
|
|
|
56
58
|
## Reading subscription usage
|
|
57
59
|
|
|
58
|
-
|
|
60
|
+
`[⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h` shows **subscription allowance used**, not context usage.
|
|
59
61
|
|
|
60
|
-
- Bar:
|
|
62
|
+
- Bar: shown as `[cells]` with 2–10 cells by default, expanding into spare space up to `maxUsageBarCells` (or fewer when the cap is below 2); 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.
|
|
61
63
|
- 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
64
|
- 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
65
|
|
package/VISION.md
CHANGED
|
@@ -55,7 +55,7 @@ Reuse Pi's selected-provider credentials. Copilot uses the GitHub login token fr
|
|
|
55
55
|
### Refresh and failure behavior
|
|
56
56
|
|
|
57
57
|
- Consume passive Codex response headers/stream events and Claude quota headers when available.
|
|
58
|
-
- Otherwise fetch on startup/provider switch, then every four minutes without a fresh response update
|
|
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
60
|
- Bound authentication, fetch, and response-body parsing by a shared five-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.
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
@@ -141,12 +147,14 @@ const MIN_STATUS_WIDTH = 12;
|
|
|
141
147
|
function buildLine(
|
|
142
148
|
width: number, path: string, statuses: string, branch: string, model: string, context: string,
|
|
143
149
|
usage: (available: number, maxCells?: number) => string | null,
|
|
144
|
-
statusSeparator: string, locationSeparator: string,
|
|
150
|
+
statusSeparator: string, locationSeparator: string, maxUsageBarCells: number,
|
|
145
151
|
): string {
|
|
146
152
|
if (width <= 0) return "";
|
|
153
|
+
if (width <= 2) return " ".repeat(width);
|
|
154
|
+
width -= 2;
|
|
147
155
|
let core = [model, context].filter(Boolean).join(MODEL_GAP);
|
|
148
156
|
// Budget the compact bar first; its expanded size must not drive truncation decisions.
|
|
149
|
-
const usageBar = usage(width) ?? "";
|
|
157
|
+
const usageBar = usage(width, Math.min(BASE_BAR_CELLS, maxUsageBarCells)) ?? "";
|
|
150
158
|
const usageGapWidth = usageBar && core ? USAGE_GAP.length : 0;
|
|
151
159
|
const coreBudget = Math.max(0, width - visibleWidth(usageBar) - usageGapWidth);
|
|
152
160
|
if (usageBar && coreBudget < MIN_TEXT_WIDTH) {
|
|
@@ -182,11 +190,11 @@ function buildLine(
|
|
|
182
190
|
// A dropped left group needs no divider; those columns belong to the usage bar instead.
|
|
183
191
|
const interGroupGapWidth = left && right ? LOCATION_GAP_WIDTH : 0;
|
|
184
192
|
const spareWidth = Math.max(0, width - visibleWidth(left) - visibleWidth(right) - interGroupGapWidth);
|
|
185
|
-
const expandedUsageBar = usage(visibleWidth(usageBar) + spareWidth,
|
|
193
|
+
const expandedUsageBar = usage(visibleWidth(usageBar) + spareWidth, maxUsageBarCells) ?? usageBar;
|
|
186
194
|
const expandedCore = [core, expandedUsageBar].filter(Boolean).join(USAGE_GAP);
|
|
187
195
|
right = [fittedStatuses, expandedCore].filter(Boolean).join(statusSeparator);
|
|
188
196
|
}
|
|
189
|
-
return left + " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right))) + right;
|
|
197
|
+
return " " + left + " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right))) + right + " ";
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
// ── Extension ─────────────────────────────────────────────────────────
|
|
@@ -251,6 +259,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
251
259
|
(available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
|
|
252
260
|
statusSeparator,
|
|
253
261
|
config.powerlineSeparator ? theme.fg("dim", " ") : " ",
|
|
262
|
+
config.maxUsageBarCells ?? MAX_BAR_CELLS,
|
|
254
263
|
);
|
|
255
264
|
return [line];
|
|
256
265
|
},
|
|
@@ -6,14 +6,15 @@ import { parseUsageWindows, record, shortest, supportedOrigin, usageRequest, win
|
|
|
6
6
|
import type { UsageWindow } from "./usage-providers.js";
|
|
7
7
|
|
|
8
8
|
const REFRESH_MS = 4 * 60_000;
|
|
9
|
-
const
|
|
9
|
+
const REFRESH_JITTER_MS = REFRESH_MS / 8;
|
|
10
|
+
const REQUEST_TIMEOUT_MS = 9000;
|
|
10
11
|
const HOUR_MS = 60 * 60_000;
|
|
11
12
|
const DAY_MS = 24 * HOUR_MS;
|
|
12
13
|
const CELL_STEPS = 8;
|
|
13
14
|
const PARTIAL_CELL_FILLS = ["", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣷"];
|
|
14
15
|
|
|
15
16
|
// Start compact so extra precision never steals space from the other footer fields.
|
|
16
|
-
export const BASE_BAR_CELLS =
|
|
17
|
+
export const BASE_BAR_CELLS = 2;
|
|
17
18
|
export const MAX_BAR_CELLS = 10;
|
|
18
19
|
|
|
19
20
|
function formatResetLabel(resetAt: number | null, now: number): string {
|
|
@@ -115,6 +116,7 @@ export class UsageLimits {
|
|
|
115
116
|
private updatedAt: Record<string, number> = {};
|
|
116
117
|
private stale = false;
|
|
117
118
|
private attemptedAt = 0;
|
|
119
|
+
private refreshDelay = REFRESH_MS;
|
|
118
120
|
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
119
121
|
private request: AbortController | null = null;
|
|
120
122
|
|
|
@@ -178,17 +180,23 @@ export class UsageLimits {
|
|
|
178
180
|
// Only a response for the displayed window supersedes its background refresh.
|
|
179
181
|
this.cancelRequest();
|
|
180
182
|
this.stale = false;
|
|
183
|
+
this.randomizeRefresh();
|
|
181
184
|
}
|
|
182
185
|
this.schedule();
|
|
183
186
|
this.render();
|
|
184
187
|
}
|
|
185
188
|
|
|
189
|
+
private randomizeRefresh(): void {
|
|
190
|
+
this.refreshDelay = REFRESH_MS + (Math.random() * 2 - 1) * REFRESH_JITTER_MS;
|
|
191
|
+
}
|
|
192
|
+
|
|
186
193
|
private schedule(): void {
|
|
187
194
|
if (this.timer) clearTimeout(this.timer);
|
|
188
195
|
this.timer = null;
|
|
189
196
|
if (!this.ctx || this.request) return;
|
|
190
197
|
const selected = this.selectedWindow();
|
|
191
|
-
|
|
198
|
+
// Keep the sampled deadline when unrelated windows reschedule the timer.
|
|
199
|
+
let next = Math.max(this.attemptedAt, selected ? this.updatedAt[selected[0]] : 0) + this.refreshDelay;
|
|
192
200
|
const resetAt = selected?.[1].resetAt;
|
|
193
201
|
if (resetAt != null && resetAt > this.attemptedAt) next = Math.min(next, resetAt);
|
|
194
202
|
this.timer = setTimeout(() => { this.timer = null; void this.poll(); }, Math.max(1, next - Date.now()));
|
|
@@ -205,6 +213,7 @@ export class UsageLimits {
|
|
|
205
213
|
// Identity checks below keep late responses from restoring a replaced provider's usage.
|
|
206
214
|
this.request = controller;
|
|
207
215
|
this.attemptedAt = Date.now();
|
|
216
|
+
this.randomizeRefresh();
|
|
208
217
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
209
218
|
timeout.unref();
|
|
210
219
|
try {
|
|
@@ -237,14 +246,15 @@ export class UsageLimits {
|
|
|
237
246
|
if (!this.ctx || !window) return null;
|
|
238
247
|
const now = Date.now();
|
|
239
248
|
const resetLabel = formatResetLabel(window.resetAt, now);
|
|
240
|
-
|
|
241
|
-
|
|
249
|
+
// Reserve two columns for brackets so an empty bar still has clear edges.
|
|
250
|
+
if (width < (resetLabel.length || 1) + 3) return null;
|
|
251
|
+
const cells = Math.min(maxCells, Math.max(1, width - resetLabel.length - 2));
|
|
242
252
|
const steps = Math.round(window.used / 100 * cells * CELL_STEPS);
|
|
243
253
|
const filled = "⣿".repeat(Math.floor(steps / CELL_STEPS)) + PARTIAL_CELL_FILLS[steps % CELL_STEPS];
|
|
244
254
|
const empty = "⠀".repeat(cells - Math.ceil(steps / CELL_STEPS));
|
|
245
255
|
const stale = this.stale || (window.resetAt !== null && now >= window.resetAt);
|
|
246
256
|
const color = stale ? "dim" : window.used >= 92 ? "error" : window.used >= 85 ? "warning" : "success";
|
|
247
257
|
const bar = cells ? theme.fg(color, filled) + theme.fg("dim", empty) : "";
|
|
248
|
-
return bar + theme.fg("dim", resetLabel);
|
|
258
|
+
return theme.fg("dim", "[") + bar + theme.fg("dim", "]") + theme.fg("dim", resetLabel);
|
|
249
259
|
}
|
|
250
260
|
}
|
package/media/github-preview.png
CHANGED
|
Binary file
|
package/media/preview.png
CHANGED
|
Binary file
|
package/package.json
CHANGED