pi-minimal-footer 0.6.0 → 0.7.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 +32 -1
- package/README.md +18 -2
- package/VISION.md +15 -0
- package/extensions/index.ts +15 -2
- package/extensions/lib/usage-limits.ts +1 -1
- package/extensions/lib/usage-providers.ts +7 -3
- package/package.json +9 -5
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.7.1] - 2026-10-16
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Use the correct Copilot account credentials when displaying quota usage
|
|
15
|
+
- Keep extension statuses on one footer line when they contain newlines or tabs
|
|
16
|
+
|
|
17
|
+
- Cover Pi 1.3.0 in compatibility checks
|
|
18
|
+
|
|
19
|
+
## [0.7.0] - 2026-10-13
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Remove an unused, completed implementation plan from the documentation
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Programmatically enable or disable the footer for the current session without changing saved settings:
|
|
28
|
+
```typescript
|
|
29
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: false }); // disable
|
|
30
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: true }); // enable
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Strengthen TypeScript checks and make provider endpoint lookups explicit
|
|
36
|
+
- Refine the package description and discovery keywords
|
|
37
|
+
- Shorten the README introduction and use a thinner footer preview; keep the Pi package preview unchanged
|
|
38
|
+
|
|
10
39
|
## [0.6.0] - 2026-10-08
|
|
11
40
|
|
|
12
41
|
### Fixed
|
|
@@ -234,7 +263,9 @@ First npm-ready release.
|
|
|
234
263
|
|
|
235
264
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
236
265
|
|
|
237
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.
|
|
266
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.7.1...HEAD
|
|
267
|
+
[0.7.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.7.0...v0.7.1
|
|
268
|
+
[0.7.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.6.0...v0.7.0
|
|
238
269
|
[0.6.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.3...v0.6.0
|
|
239
270
|
[0.5.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.2...v0.5.3
|
|
240
271
|
[0.5.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.1...v0.5.2
|
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<a href="https://pi.dev/packages/pi-minimal-footer"><img src="https://img.shields.io/badge/pi-package-1a1a2e" alt="pi package"></a>
|
|
8
8
|
</p>
|
|
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/
|
|
12
|
+
![Footer preview: ~/git/project main 🧠 Karpathy 🪽 Icarus model · 42/200k · [⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀]↻1d8h](media/preview.png)
|
|
13
13
|
|
|
14
14
|
**Left**
|
|
15
15
|
|
|
@@ -116,6 +116,22 @@ Provided by other extensions; hidden with `showSkills: false`. Narrow terminals
|
|
|
116
116
|
|
|
117
117
|
Only one footer extension can be active. For plugin integration, see [VISION.md](VISION.md#plugin-integration).
|
|
118
118
|
|
|
119
|
+
## Programmatic control
|
|
120
|
+
|
|
121
|
+
Other Pi extensions can enable or disable the footer for the current session without changing saved settings:
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: false }); // disable
|
|
125
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: true }); // enable
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- Events work only while this extension's session is active.
|
|
129
|
+
- The latest command or event wins.
|
|
130
|
+
- `/minfooter on/off` saves your preference into config; events don't.
|
|
131
|
+
- Enabling may replace another footer.
|
|
132
|
+
- Disabling restores Pi's default footer if this footer is active; it leaves another extension's active footer untouched.
|
|
133
|
+
- New sessions and `/reload` use your saved preference.
|
|
134
|
+
|
|
119
135
|
## Development & design
|
|
120
136
|
|
|
121
137
|
Install from source:
|
package/VISION.md
CHANGED
|
@@ -113,6 +113,21 @@ Use a unique key. Pi requests a redraw when statuses change; no footer-specific
|
|
|
113
113
|
|
|
114
114
|
Footer ownership is exclusive: another extension calling `ctx.ui.setFooter()` replaces this footer. Usage requests, new Git queries, and footer-owned timers stop on disposal; an in-flight Git query may complete, but lifecycle state refreshes alone never start one.
|
|
115
115
|
|
|
116
|
+
#### Session-only footer control
|
|
117
|
+
|
|
118
|
+
Extensions can temporarily override footer visibility without changing the user's saved setting:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: false });
|
|
122
|
+
pi.events.emit("pi-minimal-footer:set-enabled", { enabled: true });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Emit after this extension's `session_start` handler has run; events before startup or after shutdown are ignored. The payload must contain a boolean `enabled`; invalid payloads are ignored. Changes are silent and do not write settings.
|
|
126
|
+
|
|
127
|
+
The latest explicit action wins: `/minfooter on/off` changes the current footer and saves the preference, while this event changes only the current session. Normal refreshes never undo the choice. A new session or extension reload starts from the saved preference.
|
|
128
|
+
|
|
129
|
+
Disabling restores Pi's default footer only if we still own the active footer; otherwise it leaves the active footer untouched. Enabling explicitly installs our footer and may replace another extension's footer; repeated enables while we own it do nothing. Neither action restores a previous extension's footer. A coordinating extension must choose the active footer—there is no priority stack or automatic reactivation.
|
|
130
|
+
|
|
116
131
|
### Code map
|
|
117
132
|
|
|
118
133
|
- `extensions/index.ts`: settings, plain state updates, active-footer refresh gate, layout, footer lifecycle, and `/minfooter`.
|
package/extensions/index.ts
CHANGED
|
@@ -262,6 +262,7 @@ function buildLine(
|
|
|
262
262
|
export default function (pi: ExtensionAPI) {
|
|
263
263
|
let config = readConfig();
|
|
264
264
|
let enabled = config.enabled !== false;
|
|
265
|
+
let currentCtx: ExtensionContext | undefined;
|
|
265
266
|
const state: FooterState = {
|
|
266
267
|
cwd: process.cwd(), model: "no-model", context: "?", gitSync: "",
|
|
267
268
|
gitInitialized: false, gitHighlighted: false, gitRequest: 0,
|
|
@@ -294,7 +295,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
294
295
|
|
|
295
296
|
function install(ctx: ExtensionContext): void {
|
|
296
297
|
config = readConfig();
|
|
297
|
-
enabled = config.enabled !== false;
|
|
298
298
|
if (ctx.mode !== "tui") { usageLimits.stop(); return; }
|
|
299
299
|
|
|
300
300
|
if (!enabled) {
|
|
@@ -384,7 +384,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
384
384
|
return {
|
|
385
385
|
render(width: number): string[] {
|
|
386
386
|
const statuses = config.showSkills
|
|
387
|
-
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
387
|
+
? [...footerData.getExtensionStatuses().values()].map((s) => s.replace(/[\r\n\t]/g, " ")).filter((s) => s.trim())
|
|
388
388
|
: [];
|
|
389
389
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
390
390
|
const branchText = branch ? ` ${branch}${state.gitSync}` : "";
|
|
@@ -426,7 +426,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
426
426
|
|
|
427
427
|
// ── Lifecycle events (passive; no footer re-install) ────────────────
|
|
428
428
|
|
|
429
|
+
pi.events.on("pi-minimal-footer:set-enabled", (data) => {
|
|
430
|
+
if (!currentCtx || typeof data !== "object" || data === null ||
|
|
431
|
+
!("enabled" in data) || typeof data.enabled !== "boolean") return;
|
|
432
|
+
if (enabled === data.enabled && (!enabled || disposeFooter !== null)) return;
|
|
433
|
+
enabled = data.enabled;
|
|
434
|
+
updateState(currentCtx, state);
|
|
435
|
+
install(currentCtx);
|
|
436
|
+
refreshFooter();
|
|
437
|
+
});
|
|
438
|
+
|
|
429
439
|
pi.on("session_start", async (_event, ctx) => {
|
|
440
|
+
currentCtx = ctx;
|
|
441
|
+
enabled = readConfig().enabled !== false;
|
|
430
442
|
usageLimits.stop();
|
|
431
443
|
updateState(ctx, state);
|
|
432
444
|
install(ctx);
|
|
@@ -467,6 +479,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
467
479
|
});
|
|
468
480
|
|
|
469
481
|
pi.on("session_shutdown", async () => {
|
|
482
|
+
currentCtx = undefined;
|
|
470
483
|
usageLimits.stop();
|
|
471
484
|
disposeFooter?.();
|
|
472
485
|
});
|
|
@@ -197,7 +197,7 @@ export class UsageLimits {
|
|
|
197
197
|
if (!this.ctx || this.request) return;
|
|
198
198
|
const selected = this.selectedWindow();
|
|
199
199
|
// Keep the sampled deadline when unrelated windows reschedule the timer.
|
|
200
|
-
let next = Math.max(this.attemptedAt, selected ? this.updatedAt[selected[0]] : 0) + this.refreshDelay;
|
|
200
|
+
let next = Math.max(this.attemptedAt, selected ? this.updatedAt[selected[0]] ?? 0 : 0) + this.refreshDelay;
|
|
201
201
|
const resetAt = selected?.[1].resetAt;
|
|
202
202
|
if (resetAt != null && resetAt > this.attemptedAt) next = Math.min(next, resetAt);
|
|
203
203
|
this.timer = setTimeout(() => { this.timer = null; void this.poll(); }, Math.max(1, next - Date.now()));
|
|
@@ -73,6 +73,8 @@ function storedOAuth(provider: string): Record<string, unknown> {
|
|
|
73
73
|
|
|
74
74
|
export async function usageRequest(ctx: ExtensionContext, model: NonNullable<ExtensionContext["model"]>): Promise<{ url: string; init: RequestInit } | null> {
|
|
75
75
|
const provider = model.provider;
|
|
76
|
+
const url = ENDPOINTS[provider];
|
|
77
|
+
if (!url) return null;
|
|
76
78
|
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
77
79
|
if (!auth.ok) {
|
|
78
80
|
// Pi exposes missing credentials as a message, alongside transient OAuth errors.
|
|
@@ -96,7 +98,9 @@ export async function usageRequest(ctx: ExtensionContext, model: NonNullable<Ext
|
|
|
96
98
|
} else if (provider === "github-copilot") {
|
|
97
99
|
// Quota API needs the GitHub login token, not the exchanged inference token.
|
|
98
100
|
const refresh = storedOAuth(provider).refresh;
|
|
99
|
-
|
|
101
|
+
if (ctx.modelRegistry.isUsingOAuth?.(model) ?? typeof refresh === "string") {
|
|
102
|
+
token = typeof refresh === "string" ? refresh : undefined;
|
|
103
|
+
}
|
|
100
104
|
Object.assign(headers, { "Editor-Version": "vscode/1.96.2", "User-Agent": "GitHubCopilotChat/0.26.7", "X-Github-Api-Version": "2025-04-01" });
|
|
101
105
|
} else if (provider === "google-gemini-cli" && token?.startsWith("{")) {
|
|
102
106
|
const credentials = record(JSON.parse(token));
|
|
@@ -110,7 +114,7 @@ export async function usageRequest(ctx: ExtensionContext, model: NonNullable<Ext
|
|
|
110
114
|
init.body = "{}";
|
|
111
115
|
headers["Content-Type"] = "application/json";
|
|
112
116
|
}
|
|
113
|
-
return { url
|
|
117
|
+
return { url, init };
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
function isObject(value: unknown): value is Record<string, unknown> {
|
|
@@ -148,7 +152,7 @@ export function parseUsageWindows(provider: string, model: string, payload: unkn
|
|
|
148
152
|
recognized = true;
|
|
149
153
|
if (data.rate_limit !== null && !isObject(data.rate_limit)) malformed = true;
|
|
150
154
|
const limits = record(data.rate_limit);
|
|
151
|
-
for (const [key, name] of [["primary", "primary_window"], ["secondary", "secondary_window"]]) {
|
|
155
|
+
for (const [key, name] of [["primary", "primary_window"], ["secondary", "secondary_window"]] as const) {
|
|
152
156
|
add(key, limits[name], (w) => windowFrom(number(w.used_percent),
|
|
153
157
|
Object.hasOwn(w, "limit_window_seconds") ? number(w.limit_window_seconds) ?? NaN : undefined,
|
|
154
158
|
resetSeconds(w.reset_at) ?? relativeReset(w.reset_after_seconds)));
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-minimal-footer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "An opinionated minimal footer
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "An opinionated minimal Pi footer - one-line status bar showing subscription usage, path, status, model and context.",
|
|
5
5
|
"keywords": [
|
|
6
|
+
"footer",
|
|
7
|
+
"minimal",
|
|
8
|
+
"usage",
|
|
9
|
+
"limits",
|
|
10
|
+
"status-bar",
|
|
11
|
+
"one-line",
|
|
6
12
|
"pi-package",
|
|
7
13
|
"pi-extension",
|
|
8
|
-
"pi"
|
|
9
|
-
"footer",
|
|
10
|
-
"minimal"
|
|
14
|
+
"pi"
|
|
11
15
|
],
|
|
12
16
|
"homepage": "https://github.com/Ryu-CZ/pi-minimal-footer#readme",
|
|
13
17
|
"bugs": {
|