pi-minimal-footer 0.2.0 → 0.3.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 +17 -1
- package/README.md +42 -116
- package/VISION.md +106 -0
- package/extensions/index.ts +26 -23
- package/extensions/lib/usage-limits.ts +15 -6
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `minFooter.powerlineSeparator` (default `true`); set to `false` for a single-space fallback when the terminal font lacks Powerline glyphs
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Keep README focused on installation and daily use; move design constraints, developer guidance, and provider details into `VISION.md`
|
|
19
|
+
- Separate directory and git branch with ``; render each extension status as its own `` tab, without footer-added parentheses or pipes
|
|
20
|
+
- Group model, context, and quota with spaced ` · ` separators
|
|
21
|
+
- Join the quota bar and reset label without an intervening space
|
|
22
|
+
- Show reset labels as local `↻HH:mm` through 24 hours, remaining whole days/hours above 24 hours (e.g., `↻1d8h`), and whole days above 10 days (e.g., `↻12d`)
|
|
23
|
+
- Grow the quota bar from five to ten cells using spare footer columns, improving steps from 2.5% to 1.25% without shortening other fields; retain the smaller fallback on very narrow terminals
|
|
24
|
+
|
|
10
25
|
## [0.2.0] - 2026-10-05
|
|
11
26
|
|
|
12
27
|
### Added
|
|
@@ -114,7 +129,8 @@ First npm-ready release.
|
|
|
114
129
|
|
|
115
130
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
116
131
|
|
|
117
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.
|
|
132
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.0...HEAD
|
|
133
|
+
[0.3.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.2.0...v0.3.0
|
|
118
134
|
[0.2.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.3...v0.2.0
|
|
119
135
|
[0.1.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.2...v0.1.3
|
|
120
136
|
[0.1.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.1...v0.1.2
|
package/README.md
CHANGED
|
@@ -1,142 +1,45 @@
|
|
|
1
1
|
# pi-minimal-footer
|
|
2
2
|
|
|
3
3
|
<p>
|
|
4
|
-
<a href="https://www.npmjs.com/package/pi-minimal-footer">
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<a href="https://
|
|
8
|
-
<img src="https://img.shields.io/npm/dt/pi-minimal-footer" alt="npm downloads">
|
|
9
|
-
</a>
|
|
10
|
-
<a href="LICENSE">
|
|
11
|
-
<img src="https://img.shields.io/npm/l/pi-minimal-footer" alt="license">
|
|
12
|
-
</a>
|
|
13
|
-
<a href="https://pi.dev/packages/pi-minimal-footer">
|
|
14
|
-
<img src="https://img.shields.io/badge/pi-package-1a1a2e" alt="pi package">
|
|
15
|
-
</a>
|
|
4
|
+
<a href="https://www.npmjs.com/package/pi-minimal-footer"><img src="https://img.shields.io/npm/v/pi-minimal-footer" alt="npm version"></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/pi-minimal-footer"><img src="https://img.shields.io/npm/dt/pi-minimal-footer" alt="npm downloads"></a>
|
|
6
|
+
<a href="LICENSE"><img src="https://img.shields.io/npm/l/pi-minimal-footer" alt="license"></a>
|
|
7
|
+
<a href="https://pi.dev/packages/pi-minimal-footer"><img src="https://img.shields.io/badge/pi-package-1a1a2e" alt="pi package"></a>
|
|
16
8
|
</p>
|
|
17
9
|
|
|
18
|
-
An opinionated,
|
|
10
|
+
An opinionated, compact one-line footer for [Pi](https://github.com/earendil-works/pi).
|
|
19
11
|
|
|
12
|
+
```text
|
|
13
|
+
~/git/project main 🧠 Karpathy 🪽 Icarus model · 42/200k · ⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀↻1d8h
|
|
20
14
|
```
|
|
21
|
-
~/git/pi-minimal-footer main (icarus | goal running) gpt-5.6-sol 12/128k ⣿⣶⠀⠀⠀ ↻16:40
|
|
22
|
-
```
|
|
23
15
|
|
|
24
|
-
|
|
16
|
+
Path and branch on the left; extension statuses, model, context, and provider quota on the right. Shrinks to fit narrow terminals. Interactive terminal UI only.
|
|
17
|
+
|
|
18
|
+

|
|
19
|
+
|
|
20
|
+
*Screenshot shows an earlier layout; the text preview reflects current source.*
|
|
25
21
|
|
|
26
22
|
## Install
|
|
27
23
|
|
|
28
|
-
|
|
24
|
+
Requires Node.js >=22.19.0.
|
|
29
25
|
|
|
30
26
|
```bash
|
|
31
|
-
# From npm (recommended)
|
|
32
27
|
pi install npm:pi-minimal-footer
|
|
33
|
-
|
|
34
|
-
# From git
|
|
28
|
+
# Or install from source:
|
|
35
29
|
pi install git:github.com/Ryu-CZ/pi-minimal-footer
|
|
36
|
-
|
|
37
|
-
# Manual — copy into your extensions directory
|
|
38
|
-
cp -r extensions/* ~/.pi/agent/extensions/
|
|
39
|
-
|
|
40
|
-
# Development — symlink for live edits
|
|
41
|
-
ln -s "$PWD/extensions" ~/.pi/agent/extensions/minimal-footer
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Development
|
|
45
|
-
|
|
46
|
-
Requires Node.js >=22.19.0. Run `npm ci`, `npm run check`, and `npm test`. Pi loads TypeScript directly; no build is needed. For an isolated local preview, run `pi --no-extensions -e ./extensions/index.ts`. After source edits, use `/reload` in Pi. Tests use Pi 1.0.3 and make no provider requests.
|
|
47
|
-
|
|
48
|
-
## Features
|
|
49
|
-
|
|
50
|
-
- **Working directory** — home abbreviated as `~`; paths outside home remain absolute
|
|
51
|
-
- **Extension statuses** — text reported by extensions through Pi's status API (`showSkills` retains its existing setting name; it does not discover installed skills)
|
|
52
|
-
- **Git branch** — current branch name
|
|
53
|
-
- **Model** — active model ID
|
|
54
|
-
- **Context usage** — tokens used / context window (e.g., `12/128k`). After compaction, Pi reports usage as unknown (`?`) until a subsequent model response.
|
|
55
|
-
- **Provider quota** — the right end shows the selected provider's shortest available quota as a Braille bar and local reset time in 24-hour `HH:mm` format, when reported.
|
|
56
|
-
|
|
57
|
-
The directory and git branch stay together on the left. Extension statuses, model, context usage, and quota stay on the right, with extra spacing before the quota. Statuses and location are muted; the model is bold.
|
|
58
|
-
|
|
59
|
-
On narrow terminals, the directory and statuses shorten or disappear, then the branch disappears. The quota bar shrinks before model/context is shortened, preserving the reset time. The footer always uses one line.
|
|
60
|
-
|
|
61
|
-
The footer appears only in Pi's interactive terminal UI, not in print, JSON, or RPC modes.
|
|
62
|
-
|
|
63
|
-
### Provider usage limits
|
|
64
|
-
|
|
65
|
-
The footer displays only the selected provider's shortest available quota window, typically five hours. If only a weekly or monthly quota is available, it displays that quota. This measures account quota used, separately from the model's context usage (`12/128k`).
|
|
66
|
-
|
|
67
|
-
| Provider | Quota selection |
|
|
68
|
-
|---|---|
|
|
69
|
-
| OpenAI Codex (`openai-codex`) | Shortest primary/secondary window |
|
|
70
|
-
| Claude (`anthropic`, OAuth) | Five-hour window, or weekly if that is all that is reported |
|
|
71
|
-
| GitHub Copilot (`github-copilot`) | Limited premium/chat quota; unlimited buckets are hidden |
|
|
72
|
-
| Gemini CLI (`google-gemini-cli`) | Selected model's quota, with Pro/Flash family fallback |
|
|
73
|
-
| MiniMax (`minimax`, `minimax-cn`) | General text bucket's shortest interval/weekly window |
|
|
74
|
-
| Kimi Coding (`kimi-coding`) | Shortest reported window, or weekly fallback |
|
|
75
|
-
| OpenCode Go (`opencode-go`) | Shortest rolling/weekly/monthly window |
|
|
76
|
-
|
|
77
|
-
The adapters reuse the selected provider's authentication from Pi. Claude subscription quota requires OAuth; ordinary Claude API keys do not expose it. Copilot's quota endpoint uses the GitHub login token from Pi's configured agent directory or `COPILOT_GITHUB_TOKEN`. Gemini CLI needs a configured provider/model; Pi 1.0.3 does not include that provider in its built-in catalog.
|
|
78
|
-
|
|
79
|
-
The usage segment contains only a five-cell Braille bar and its reset time:
|
|
80
|
-
|
|
81
|
-
```text
|
|
82
|
-
⣿⣶⠀⠀⠀ ↻16:40
|
|
83
30
|
```
|
|
84
31
|
|
|
85
|
-
This example shows 35% used. `↻16:40` means the quota resets at 16:40 in your local timezone: an absolute 24-hour time, not a countdown. No provider name, window label, percentage, or date is displayed.
|
|
86
|
-
|
|
87
|
-
If the provider does not report a reset time, only the bar is shown:
|
|
88
|
-
|
|
89
|
-
```text
|
|
90
|
-
⣿⣶⠀⠀⠀
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Each cell uses comic-style dot shading, starting from the bottom:
|
|
94
|
-
|
|
95
|
-
| Cell | Fill |
|
|
96
|
-
|---|---|
|
|
97
|
-
| `⠀` | Empty |
|
|
98
|
-
| `⡀` | One-eighth |
|
|
99
|
-
| `⣀` | Quarter |
|
|
100
|
-
| `⣄` | Three-eighths |
|
|
101
|
-
| `⣤` | Half |
|
|
102
|
-
| `⣦` | Five-eighths |
|
|
103
|
-
| `⣶` | Three-quarter |
|
|
104
|
-
| `⣷` | Seven-eighths |
|
|
105
|
-
| `⣿` | Full |
|
|
106
|
-
|
|
107
|
-
Each step adds one dot from the bottom upward. Five cells provide 2.5% steps, rounded to the nearest step; only the last partially filled cell uses an intermediate state. Filled cells are green below 85%, amber from 85%, and red from 92%. On narrow terminals the bar shrinks to preserve the reset time, so its steps become coarser.
|
|
108
|
-
|
|
109
|
-
Usage refreshes from Codex response headers/stream events and Claude quota response headers when available. Otherwise, the footer checks the selected provider's usage endpoint on startup or provider switch, then every four minutes without a fresh response update. It also checks when a reported reset time arrives. These checks make no model requests.
|
|
110
|
-
|
|
111
|
-
Partial response updates are merged with cached windows. A weekly-only update keeps the cached five-hour quota visible and does not postpone its refresh. Authentication, fetching, and response parsing share a five-second timeout; a stalled lookup or body read releases the request and allows the next scheduled attempt. Late results cannot restore a previous provider's quota.
|
|
112
|
-
|
|
113
|
-
Codex endpoint responses can supply either an absolute reset timestamp or a relative reset delay. Kimi quotas accept either used or remaining counts, including week/month durations. Invalid optional reset metadata leaves the bar visible without a time.
|
|
114
|
-
|
|
115
|
-
Switching providers immediately clears the previous quota, cancels its requests and timers, and ignores late results. Switching back fetches fresh data. Gemini model switches also refresh the model-specific quota. A previous provider's bar never remains visible while the new provider loads.
|
|
116
|
-
|
|
117
|
-
Local models, unsupported providers, and sessions without a selected model show no usage segment and make no usage requests. Missing credentials or a successful usage response without an applicable quota hide the segment, including any previously cached bar. Zero usage still displays an empty bar, with its reset time when available.
|
|
118
|
-
|
|
119
|
-
If a refresh fails temporarily or returns malformed quota data, the current provider's last known bar is dimmed until fresh data arrives; without cached data, the segment stays hidden. Explicit empty or unlimited quota responses clear the cached bar. Requests and timers also stop when the footer is disabled, disposed, or the session shuts down. Custom proxy endpoints are not polled. Live quota availability depends on each provider's endpoint; unavailable endpoints stay silent.
|
|
120
|
-
|
|
121
|
-
The adapters have automated parsing and lifecycle coverage, but authenticated provider endpoints have not been tested live. In particular, OpenCode Go's bearer-token response format and Copilot's public quota endpoint and enterprise compatibility remain unverified. The comparison with [mtrojnar/pi-usage](https://github.com/mtrojnar/pi-usage) informed the timeout, validation, and partial-update handling; its OpenCode implementation uses an authenticated dashboard instead.
|
|
122
|
-
|
|
123
|
-
### Credits
|
|
124
|
-
|
|
125
|
-
Honorable mention and thanks to **Can Celik (@ogulcancelik)**: the provider usage-fetching and quota-parsing logic was **copied and adapted from [@ogulcancelik/pi-minimal-footer](https://pi.dev/packages/@ogulcancelik/pi-minimal-footer)**. This footer keeps its minimal layout, five-cell bottom-up Braille display, and four-minute refresh behavior. The upstream MIT copyright and license are retained in [extensions/lib/LICENSE](extensions/lib/LICENSE).
|
|
126
|
-
|
|
127
32
|
## Commands
|
|
128
33
|
|
|
129
|
-
| Command |
|
|
34
|
+
| Command | Action |
|
|
130
35
|
|---|---|
|
|
131
|
-
| `/minfooter` | Toggle
|
|
36
|
+
| `/minfooter` | Toggle footer |
|
|
132
37
|
| `/minfooter on` | Enable |
|
|
133
38
|
| `/minfooter off` | Disable |
|
|
134
39
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## Configuration
|
|
40
|
+
## Settings
|
|
138
41
|
|
|
139
|
-
|
|
42
|
+
Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.json`). All defaults are shown below. Apply edits with `/reload` or `/minfooter on`.
|
|
140
43
|
|
|
141
44
|
```json
|
|
142
45
|
{
|
|
@@ -146,7 +49,30 @@ Settings live in Pi's configured agent directory, in `settings.json` under the `
|
|
|
146
49
|
"showSkills": true,
|
|
147
50
|
"showPath": true,
|
|
148
51
|
"showModel": true,
|
|
149
|
-
"showContext": true
|
|
52
|
+
"showContext": true,
|
|
53
|
+
"powerlineSeparator": true
|
|
150
54
|
}
|
|
151
55
|
}
|
|
152
56
|
```
|
|
57
|
+
|
|
58
|
+
- `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
|
|
59
|
+
- 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.
|
|
60
|
+
- Context `42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
|
|
61
|
+
|
|
62
|
+
## Reading quota
|
|
63
|
+
|
|
64
|
+
`⣿⣿⣿⣤⠀⠀⠀⠀⠀⠀↻1d8h` shows **account quota used**, not context usage.
|
|
65
|
+
|
|
66
|
+
- Bar: 5–10 cells, expanding into spare space; green below 85%, amber from 85%, red from 92%. Dim means cached after a refresh failure. Very narrow terminals may show fewer cells or only the reset label.
|
|
67
|
+
- 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.
|
|
68
|
+
- Shows the shortest available quota window for the selected provider. Missing credentials, unsupported/local models, or absent limits hide quota. Custom proxy endpoints are not polled.
|
|
69
|
+
|
|
70
|
+
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 quota. Provider availability varies; authenticated endpoints remain unverified live.
|
|
71
|
+
|
|
72
|
+
## Development & design
|
|
73
|
+
|
|
74
|
+
See [VISION.md](VISION.md) for design constraints, local development, provider details, and verification limits. Release history: [CHANGELOG.md](CHANGELOG.md).
|
|
75
|
+
|
|
76
|
+
## Credits
|
|
77
|
+
|
|
78
|
+
Provider usage-fetching and parsing logic **copied and adapted from [Can Celik (@ogulcancelik)'s pi-minimal-footer](https://pi.dev/packages/@ogulcancelik/pi-minimal-footer)**. Upstream MIT attribution is retained in [extensions/lib/LICENSE](extensions/lib/LICENSE).
|
package/VISION.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Vision & developer guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
An opinionated footer that answers three questions at a glance: where am I, what is running, and how close am I to a limit? Keep one line, avoid redundant labels, and never exceed the terminal width.
|
|
6
|
+
|
|
7
|
+
The [README](README.md) is the user manual. This document records current design and contributor guidance—not a speculative roadmap.
|
|
8
|
+
|
|
9
|
+
## Layout contract
|
|
10
|
+
|
|
11
|
+
- Directory and git branch form the left group, separated by ``.
|
|
12
|
+
- Each extension status is its own `` tab. Preserve the supplied text; do not invent status labels, parentheses, or pipes.
|
|
13
|
+
- Model, context, and quota form the rightmost group, separated by ` · `. Model identifies the running engine; context describes its usage; quota stays anchored at the right edge.
|
|
14
|
+
- Powerline separators are dim. `powerlineSeparator: false` replaces them with a single space. Font availability cannot be detected reliably.
|
|
15
|
+
- Measure terminal columns with `visibleWidth`, not string length. Preserve ANSI styling and wide-character accounting when truncating.
|
|
16
|
+
|
|
17
|
+
### Space allocation
|
|
18
|
+
|
|
19
|
+
Reserve quota 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
|
+
|
|
21
|
+
Lay out other fields with a five-cell quota 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 quota if the label cannot fit.
|
|
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:
|
|
24
|
+
|
|
25
|
+
| Cell | Fill |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `⠀` | Empty |
|
|
28
|
+
| `⡀` | One-eighth |
|
|
29
|
+
| `⣀` | Quarter |
|
|
30
|
+
| `⣄` | Three-eighths |
|
|
31
|
+
| `⣤` | Half |
|
|
32
|
+
| `⣦` | Five-eighths |
|
|
33
|
+
| `⣶` | Three-quarter |
|
|
34
|
+
| `⣷` | Seven-eighths |
|
|
35
|
+
| `⣿` | Full |
|
|
36
|
+
|
|
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
|
+
|
|
39
|
+
## Quota contract
|
|
40
|
+
|
|
41
|
+
Display only the selected provider's shortest applicable window. Account quota and model context usage are separate measurements.
|
|
42
|
+
|
|
43
|
+
| Provider | Selection / caveat |
|
|
44
|
+
|---|---|
|
|
45
|
+
| OpenAI Codex (`openai-codex`) | Shortest primary/secondary window; supports absolute reset timestamps and relative reset delays |
|
|
46
|
+
| Claude (`anthropic`, OAuth) | Five-hour window, or weekly fallback; ordinary API keys do not expose subscription quota |
|
|
47
|
+
| GitHub Copilot (`github-copilot`) | Most-used limited quota bucket; unlimited buckets hidden |
|
|
48
|
+
| Gemini CLI (`google-gemini-cli`) | Selected model, with Pro/Flash family fallback; needs a configured provider/model |
|
|
49
|
+
| MiniMax (`minimax`, `minimax-cn`) | Prefer general bucket, then active bucket, then first bucket; shortest interval/weekly window |
|
|
50
|
+
| Kimi Coding (`kimi-coding`) | Shortest available window; accepts used or remaining counts and week/month durations |
|
|
51
|
+
| OpenCode Go (`opencode-go`) | Shortest rolling/weekly/monthly window |
|
|
52
|
+
|
|
53
|
+
Reuse Pi's selected-provider credentials. Copilot uses the GitHub login token from Pi's configured agent directory or `COPILOT_GITHUB_TOKEN`. Pi 1.0.3 does not include Gemini CLI in its built-in model catalog. Do not poll custom proxy endpoints or make model requests to obtain usage.
|
|
54
|
+
|
|
55
|
+
### Refresh and failure behavior
|
|
56
|
+
|
|
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; also refresh at the reported reset time.
|
|
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.
|
|
61
|
+
- Provider switches clear quota immediately, cancel pending work, and invalidate late results. Gemini model switches also refresh model-specific quota.
|
|
62
|
+
- Missing credentials and explicit empty/unlimited responses clear quota. Temporary authentication, network, or parsing failures retain only the current provider's cached quota, dimmed; without cache, hide it.
|
|
63
|
+
- Invalid optional reset metadata must not discard otherwise valid usage. Zero usage is an empty bar, not an absent quota.
|
|
64
|
+
- Stop requests and timers on disable, disposal, and session shutdown.
|
|
65
|
+
|
|
66
|
+
### Verification limits
|
|
67
|
+
|
|
68
|
+
Parsing and lifecycle behavior have automated coverage. Authenticated provider endpoints have **not** been tested live. OpenCode Go's bearer-token response format and Copilot's public endpoint/enterprise compatibility remain unverified.
|
|
69
|
+
|
|
70
|
+
The comparison with [mtrojnar/pi-usage](https://github.com/mtrojnar/pi-usage) informed timeout, validation, and partial-update handling; its OpenCode implementation uses an authenticated dashboard rather than this adapter's bearer-token endpoint.
|
|
71
|
+
|
|
72
|
+
## Local development
|
|
73
|
+
|
|
74
|
+
Requires Node.js >=22.19.0. Tests currently use Pi 1.0.3 and mock provider requests.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npm ci
|
|
78
|
+
npm run check
|
|
79
|
+
npm test
|
|
80
|
+
|
|
81
|
+
# Isolated interactive preview; Pi loads TypeScript directly, no build needed:
|
|
82
|
+
pi --no-extensions -e ./extensions/index.ts
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use `/reload` after edits. For a persistent development install:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ln -s "$PWD/extensions" ~/.pi/agent/extensions/minimal-footer
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
For a manual install, copy `extensions/*` into `~/.pi/agent/extensions/`.
|
|
92
|
+
|
|
93
|
+
### Code map
|
|
94
|
+
|
|
95
|
+
- `extensions/index.ts`: settings, state refresh, layout, footer lifecycle, and `/minfooter`.
|
|
96
|
+
- `extensions/lib/usage-limits.ts`: quota refresh, cache, cancellation, and bar/reset rendering.
|
|
97
|
+
- `extensions/lib/quota-providers.ts`: authentication, endpoint selection, and response normalization.
|
|
98
|
+
- `tests/`: loader-based lifecycle/layout regression tests and quota parsing tests.
|
|
99
|
+
|
|
100
|
+
Settings live under `minFooter` in Pi's configured agent directory. Read them at session start or explicit toggle; ordinary refreshes use cached settings. Preserve unrelated settings on writes. Only install the footer in interactive terminal mode.
|
|
101
|
+
|
|
102
|
+
### Checking changes
|
|
103
|
+
|
|
104
|
+
For behavior changes, run `npm run check` and `npm test`. Layout tests should cover narrow widths, ANSI text, wide Unicode, field priority, and both separator modes. Quota changes should cover provider switches, timeouts, stale cache, partial updates, and malformed/missing metadata as applicable. Preview in a real terminal for font-dependent appearance; automated width checks cannot prove glyph availability.
|
|
105
|
+
|
|
106
|
+
Keep release notes in [CHANGELOG.md](CHANGELOG.md), and keep the README focused on installation and daily use. Preserve [upstream MIT attribution](extensions/lib/LICENSE) when changing adapted provider code.
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
|
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".
|
|
@@ -29,6 +29,7 @@ interface Settings {
|
|
|
29
29
|
showPath?: boolean;
|
|
30
30
|
showModel?: boolean;
|
|
31
31
|
showContext?: boolean;
|
|
32
|
+
powerlineSeparator?: boolean;
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -41,6 +42,7 @@ const DEFAULT_SETTINGS: FooterSettings = {
|
|
|
41
42
|
showPath: true,
|
|
42
43
|
showModel: true,
|
|
43
44
|
showContext: true,
|
|
45
|
+
powerlineSeparator: true,
|
|
44
46
|
};
|
|
45
47
|
|
|
46
48
|
function settingsPath(): string {
|
|
@@ -127,56 +129,54 @@ function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
|
127
129
|
// Keep quota and reset time together; shorten location/statuses before model/context.
|
|
128
130
|
// All segment measurements use visible widths, including ANSI and wide characters.
|
|
129
131
|
|
|
130
|
-
const
|
|
131
|
-
const QUOTA_GAP = "
|
|
132
|
+
const MODEL_GAP = " · ";
|
|
133
|
+
const QUOTA_GAP = " · ";
|
|
132
134
|
const LOCATION_GAP_WIDTH = 3;
|
|
133
|
-
const FULL_QUOTA_WIDTH = 12;
|
|
134
|
-
const COMPACT_QUOTA_WIDTH = 11;
|
|
135
135
|
const MIN_TEXT_WIDTH = 4;
|
|
136
136
|
const MIN_STATUS_WIDTH = 12;
|
|
137
137
|
|
|
138
138
|
function buildLine(width: number, path: string, statuses: string, branch: string, model: string, context: string,
|
|
139
|
-
usage: (available: number) => string | null): string {
|
|
139
|
+
usage: (available: number, maxCells?: number) => string | null, statusSeparator: string, locationSeparator: string): string {
|
|
140
140
|
if (width <= 0) return "";
|
|
141
|
-
let core = [model, context].filter(Boolean).join(
|
|
142
|
-
|
|
143
|
-
const availableQuotaWidth = width - visibleWidth(core) - QUOTA_GAP.length;
|
|
144
|
-
if (quota && visibleWidth(quota) > availableQuotaWidth) {
|
|
145
|
-
const compactWidth = Math.min(width, Math.max(COMPACT_QUOTA_WIDTH, availableQuotaWidth));
|
|
146
|
-
quota = usage(compactWidth) ?? "";
|
|
147
|
-
}
|
|
141
|
+
let core = [model, context].filter(Boolean).join(MODEL_GAP);
|
|
142
|
+
const quota = usage(width) ?? "";
|
|
148
143
|
const quotaGapWidth = quota && core ? QUOTA_GAP.length : 0;
|
|
149
144
|
const coreBudget = Math.max(0, width - visibleWidth(quota) - quotaGapWidth);
|
|
150
145
|
if (quota && coreBudget < MIN_TEXT_WIDTH) {
|
|
151
146
|
core = "";
|
|
152
|
-
quota = usage(Math.min(FULL_QUOTA_WIDTH, width)) ?? "";
|
|
153
147
|
} else if (visibleWidth(core) > coreBudget) {
|
|
154
|
-
const modelGapWidth = model && context ?
|
|
148
|
+
const modelGapWidth = model && context ? MODEL_GAP.length : 0;
|
|
155
149
|
const modelBudget = coreBudget - visibleWidth(context) - modelGapWidth;
|
|
156
150
|
if (modelBudget > 0) {
|
|
157
|
-
core = [truncateToWidth(model, modelBudget, "..."), context].filter(Boolean).join(
|
|
151
|
+
core = [truncateToWidth(model, modelBudget, "..."), context].filter(Boolean).join(MODEL_GAP);
|
|
158
152
|
} else {
|
|
159
153
|
core = truncateToWidth(context || model, coreBudget, "...");
|
|
160
154
|
}
|
|
161
155
|
}
|
|
162
156
|
const protectedRight = [core, quota].filter(Boolean).join(QUOTA_GAP);
|
|
163
157
|
const branchReservation = branch ? visibleWidth(branch) + LOCATION_GAP_WIDTH : 0;
|
|
164
|
-
const statusBudget = width - visibleWidth(protectedRight) - branchReservation -
|
|
158
|
+
const statusBudget = width - visibleWidth(protectedRight) - branchReservation - visibleWidth(statusSeparator);
|
|
165
159
|
let fittedStatuses = "";
|
|
166
160
|
if (statusBudget >= MIN_STATUS_WIDTH) {
|
|
167
161
|
fittedStatuses = statuses;
|
|
168
162
|
if (visibleWidth(statuses) > statusBudget) {
|
|
169
|
-
fittedStatuses = truncateToWidth(statuses, statusBudget
|
|
163
|
+
fittedStatuses = truncateToWidth(statuses, statusBudget, "...");
|
|
170
164
|
}
|
|
171
165
|
}
|
|
172
|
-
|
|
166
|
+
let right = [fittedStatuses, protectedRight].filter(Boolean).join(statusSeparator);
|
|
173
167
|
const locationGapWidth = right ? LOCATION_GAP_WIDTH : 0;
|
|
174
168
|
const leftBudget = Math.max(0, width - visibleWidth(right) - locationGapWidth);
|
|
175
169
|
const fittedBranch = visibleWidth(branch) <= leftBudget ? branch : "";
|
|
176
|
-
const pathGapWidth = fittedBranch && path ?
|
|
170
|
+
const pathGapWidth = fittedBranch && path ? visibleWidth(locationSeparator) : 0;
|
|
177
171
|
const pathBudget = leftBudget - visibleWidth(fittedBranch) - pathGapWidth;
|
|
178
172
|
const fittedPath = pathBudget >= MIN_TEXT_WIDTH ? truncateToWidth(path, pathBudget, "...") : "";
|
|
179
|
-
const left = [fittedPath, fittedBranch].filter(Boolean).join(
|
|
173
|
+
const left = [fittedPath, fittedBranch].filter(Boolean).join(locationSeparator);
|
|
174
|
+
if (quota) {
|
|
175
|
+
const spareWidth = Math.max(0, width - visibleWidth(left) - visibleWidth(right) - locationGapWidth);
|
|
176
|
+
const expandedQuota = usage(visibleWidth(quota) + spareWidth, 10) ?? quota;
|
|
177
|
+
const expandedCore = [core, expandedQuota].filter(Boolean).join(QUOTA_GAP);
|
|
178
|
+
right = [fittedStatuses, expandedCore].filter(Boolean).join(statusSeparator);
|
|
179
|
+
}
|
|
180
180
|
return left + " ".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right))) + right;
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -224,14 +224,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
224
224
|
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
225
225
|
: [];
|
|
226
226
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
227
|
+
const statusSeparator = config.powerlineSeparator ? theme.fg("dim", " ") : " ";
|
|
227
228
|
const line = buildLine(
|
|
228
229
|
width,
|
|
229
230
|
config.showPath ? theme.fg("dim", abbreviateHome(state.cwd, homedir())) : "",
|
|
230
|
-
skills.length ? theme.fg("dim",
|
|
231
|
+
skills.length ? statusSeparator + skills.map((s) => theme.fg("dim", s)).join(statusSeparator) : "",
|
|
231
232
|
branch ? theme.fg("dim", ` ${branch}`) : "",
|
|
232
233
|
config.showModel ? theme.bold(state.model) : "",
|
|
233
234
|
config.showContext ? theme.fg("dim", theme.bold(state.context)) : "",
|
|
234
|
-
(available) => usageLimits.line(available, theme),
|
|
235
|
+
(available, maxCells = 5) => usageLimits.line(available, theme, maxCells),
|
|
236
|
+
statusSeparator,
|
|
237
|
+
config.powerlineSeparator ? theme.fg("dim", " ") : " ",
|
|
235
238
|
);
|
|
236
239
|
return [line];
|
|
237
240
|
},
|
|
@@ -208,18 +208,27 @@ export class UsageLimits {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
line(width: number, theme: Theme): string | null {
|
|
211
|
+
line(width: number, theme: Theme, maxCells = 10): string | null {
|
|
212
212
|
const window = this.selectedWindow()?.[1];
|
|
213
|
-
if (!this.ctx || !window
|
|
213
|
+
if (!this.ctx || !window) return null;
|
|
214
|
+
const now = Date.now();
|
|
214
215
|
const date = window.resetAt === null ? null : new Date(window.resetAt);
|
|
215
|
-
const
|
|
216
|
-
const
|
|
216
|
+
const remaining = window.resetAt === null ? 0 : window.resetAt - now;
|
|
217
|
+
const day = 24 * 60 * 60 * 1000;
|
|
218
|
+
let time = date ? `↻${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}` : "";
|
|
219
|
+
if (remaining > 10 * day) {
|
|
220
|
+
time = `↻${Math.floor(remaining / day)}d`;
|
|
221
|
+
} else if (remaining > day) {
|
|
222
|
+
time = `↻${Math.floor(remaining / day)}d${Math.floor(remaining % day / (60 * 60 * 1000))}h`;
|
|
223
|
+
}
|
|
224
|
+
if (width < (time ? time.length : 1)) return null;
|
|
225
|
+
const cells = Math.min(maxCells, Math.max(0, width - time.length));
|
|
217
226
|
const steps = Math.round(window.used / 100 * cells * 8);
|
|
218
227
|
const filled = "⣿".repeat(Math.floor(steps / 8)) + ["", "⡀", "⣀", "⣄", "⣤", "⣦", "⣶", "⣷"][steps % 8];
|
|
219
228
|
const empty = "⠀".repeat(cells - Math.ceil(steps / 8));
|
|
220
|
-
const stale = this.stale || (window.resetAt !== null &&
|
|
229
|
+
const stale = this.stale || (window.resetAt !== null && now >= window.resetAt);
|
|
221
230
|
const color = stale ? "dim" : window.used >= 92 ? "error" : window.used >= 85 ? "warning" : "success";
|
|
222
|
-
const bar = cells ? theme.fg(color, filled) + theme.fg("dim", empty)
|
|
231
|
+
const bar = cells ? theme.fg(color, filled) + theme.fg("dim", empty) : "";
|
|
223
232
|
return bar + theme.fg("dim", time);
|
|
224
233
|
}
|
|
225
234
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-minimal-footer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"extensions/",
|
|
21
21
|
"LICENSE",
|
|
22
22
|
"README.md",
|
|
23
|
+
"VISION.md",
|
|
23
24
|
"package.json",
|
|
24
25
|
"media/"
|
|
25
26
|
],
|