pi-minimal-footer 0.4.2 → 0.5.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 +20 -1
- package/README.md +6 -2
- package/VISION.md +17 -3
- package/extensions/index.ts +67 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Keep the whole Git section in normal text foreground while commits remain to push or pull, returning to dim when synced
|
|
15
|
+
- Bold changed Git divergence arrows/counts for two user submissions; further changes restart the highlight, while tool turns and extension prompts do not consume it
|
|
16
|
+
|
|
17
|
+
## [0.5.0] - 2026-10-07
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Show Git upstream divergence next to the branch, for example ` main ↑2 ↓1`; `↑` is local commits to push and `↓` is upstream commits to pull
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Query Git divergence asynchronously and omit it safely when the repository has no upstream or Git is unavailable
|
|
26
|
+
|
|
10
27
|
## [0.4.2] - 2026-10-07
|
|
11
28
|
|
|
12
29
|
### Fixed
|
|
@@ -186,7 +203,9 @@ First npm-ready release.
|
|
|
186
203
|
|
|
187
204
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
188
205
|
|
|
189
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.
|
|
206
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.1...HEAD
|
|
207
|
+
[0.5.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.5.0...v0.5.1
|
|
208
|
+
[0.5.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.4.2...v0.5.0
|
|
190
209
|
[0.4.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.4.1...v0.4.2
|
|
191
210
|
[0.4.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.4.0...v0.4.1
|
|
192
211
|
[0.4.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.3.3...v0.4.0
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ An opinionated, compact one-line footer for [Pi](https://github.com/earendil-wor
|
|
|
14
14
|
**Left**
|
|
15
15
|
|
|
16
16
|
- Working directory: `~/git/project`
|
|
17
|
-
- Git branch: ` main`
|
|
17
|
+
- Git branch: ` main ↑2 ↓1` (`↑` to push, `↓` to pull)
|
|
18
18
|
|
|
19
19
|
**Right**
|
|
20
20
|
|
|
@@ -59,11 +59,15 @@ Edit `minFooter` in Pi's agent `settings.json` (normally `~/.pi/agent/settings.j
|
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
- `showSkills` shows **extension status text**, not installed skills. The footer does not add the example statuses itself.
|
|
62
|
-
- Set `powerlineSeparator` to `false` for plain spaces instead of `` / `` if your font lacks those glyphs. The git icon `` also
|
|
62
|
+
- Set `powerlineSeparator` to `false` for plain spaces instead of `` / `` if your font lacks those glyphs. The git icon `` and sync arrows also need compatible glyphs; hide Git data with `showGitBranch: false` if needed.
|
|
63
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
64
|
|
|
65
65
|
## Reading the footer
|
|
66
66
|
|
|
67
|
+
### Git
|
|
68
|
+
|
|
69
|
+
Counts require a configured upstream. Unsynced branches use the theme's normal foreground; synced branches are dim. Changed counts stay bold until two subsequent user messages; further changes restart the highlight.
|
|
70
|
+
|
|
67
71
|
### Context
|
|
68
72
|
|
|
69
73
|
`42/200k` means tokens used / context window. `?` means Pi has not reported usage yet, including immediately after compaction.
|
package/VISION.md
CHANGED
|
@@ -8,7 +8,7 @@ The [README](README.md) is the user manual. This document records current design
|
|
|
8
8
|
|
|
9
9
|
## Layout contract
|
|
10
10
|
|
|
11
|
-
- Directory and
|
|
11
|
+
- Directory and Git branch form the left group, separated by ``.
|
|
12
12
|
- Each extension status is its own `` tab. Preserve the supplied text; do not invent status labels, parentheses, or pipes.
|
|
13
13
|
- Model, context, and subscription usage form the rightmost group, separated by ` · `. Model identifies the running engine; context describes its usage; the usage bar stays anchored at the right edge.
|
|
14
14
|
- Powerline separators are dim. `powerlineSeparator: false` replaces them with a single space. Font availability cannot be detected reliably.
|
|
@@ -36,6 +36,14 @@ 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
|
+
## Git contract
|
|
40
|
+
|
|
41
|
+
- 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; never fetch automatically.
|
|
42
|
+
- Query outside rendering with a one-second timeout so Git cannot block the TUI. Missing upstreams and command failures omit counts.
|
|
43
|
+
- Refresh on branch-change signals and footer state updates. Ignore older query results to prevent stale counts from restarting highlights.
|
|
44
|
+
- Use the theme's normal text foreground while unsynced, otherwise dim. A count change bolds the arrows/counts until two subsequent user submissions; further changes restart the window. Tool turns and extension prompts do not consume it.
|
|
45
|
+
- Initial loading and working-directory changes establish an unhighlighted baseline. Zero counts hide the arrows entirely.
|
|
46
|
+
|
|
39
47
|
## Subscription usage contract
|
|
40
48
|
|
|
41
49
|
Display only the selected provider's shortest applicable window. Subscription usage and model context usage are separate measurements.
|
|
@@ -95,12 +103,18 @@ For a manual install, copy `extensions/*` into `~/.pi/agent/extensions/`.
|
|
|
95
103
|
- `extensions/index.ts`: settings, state refresh, layout, footer lifecycle, and `/minfooter`.
|
|
96
104
|
- `extensions/lib/usage-limits.ts`: usage refresh, cache, cancellation, and bar/reset rendering.
|
|
97
105
|
- `extensions/lib/usage-providers.ts`: authentication, endpoint selection, and response normalization.
|
|
98
|
-
- `tests/`:
|
|
106
|
+
- `tests/`: lifecycle/layout regressions, Git highlighting with a temporary repository, and usage parsing tests.
|
|
99
107
|
|
|
100
108
|
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
109
|
|
|
102
110
|
### Checking changes
|
|
103
111
|
|
|
104
|
-
|
|
112
|
+
Run `npm run check` and `npm test` for behavior changes. Cover relevant cases:
|
|
113
|
+
|
|
114
|
+
- Layout: narrow widths, ANSI/wide Unicode, field priority, and separator modes.
|
|
115
|
+
- Git: divergence, synced styling, highlight expiry/restart, and user submissions versus tool turns.
|
|
116
|
+
- Subscription usage: provider switches, timeouts, stale cache, partial updates, and malformed/missing metadata.
|
|
117
|
+
|
|
118
|
+
Preview font-dependent appearance in a real terminal; width checks cannot prove glyph availability.
|
|
105
119
|
|
|
106
120
|
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
|
@@ -17,6 +17,8 @@ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
|
17
17
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
18
18
|
import { join, dirname, sep } from "node:path";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
|
+
import { promisify } from "node:util";
|
|
21
|
+
import { execFile } from "node:child_process";
|
|
20
22
|
import { BASE_BAR_CELLS, MAX_BAR_CELLS, UsageLimits } from "./lib/usage-limits.js";
|
|
21
23
|
|
|
22
24
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
@@ -36,6 +38,8 @@ interface Settings {
|
|
|
36
38
|
|
|
37
39
|
type FooterSettings = NonNullable<Settings["minFooter"]>;
|
|
38
40
|
|
|
41
|
+
const execFileAsync = promisify(execFile);
|
|
42
|
+
|
|
39
43
|
const DEFAULT_SETTINGS: FooterSettings = {
|
|
40
44
|
enabled: true,
|
|
41
45
|
showGitBranch: true,
|
|
@@ -111,10 +115,50 @@ interface FooterState {
|
|
|
111
115
|
cwd: string;
|
|
112
116
|
model: string;
|
|
113
117
|
context: string;
|
|
118
|
+
gitSync: string;
|
|
119
|
+
gitInitialized: boolean;
|
|
120
|
+
gitHighlightInputs: number;
|
|
121
|
+
gitRequest: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function getGitSync(cwd: string): Promise<string> {
|
|
125
|
+
try {
|
|
126
|
+
// Pi exposes the branch name, but not upstream divergence. Keep this
|
|
127
|
+
// asynchronous and outside render() so a slow Git repository cannot block TUI rendering.
|
|
128
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-list", "--left-right", "--count", "HEAD...@{upstream}"], {
|
|
129
|
+
timeout: 1000,
|
|
130
|
+
maxBuffer: 1024,
|
|
131
|
+
});
|
|
132
|
+
// With HEAD...@{upstream}, left is local-only commits (push) and right
|
|
133
|
+
// is upstream-only commits (pull). An absent upstream is intentionally hidden.
|
|
134
|
+
const [ahead, behind] = stdout.trim().split(/\s+/).map(Number);
|
|
135
|
+
if (!Number.isInteger(ahead) || !Number.isInteger(behind)) return "";
|
|
136
|
+
return `${ahead ? ` ↑${ahead}` : ""}${behind ? ` ↓${behind}` : ""}`;
|
|
137
|
+
} catch {
|
|
138
|
+
// No repository, upstream, or reachable Git command: omit sync status.
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function updateGitSync(state: FooterState): Promise<void> {
|
|
144
|
+
const cwd = state.cwd;
|
|
145
|
+
const request = ++state.gitRequest;
|
|
146
|
+
const gitSync = await getGitSync(cwd);
|
|
147
|
+
// Refresh events can overlap; an older result must not restart the highlight.
|
|
148
|
+
if (state.cwd !== cwd || request !== state.gitRequest) return;
|
|
149
|
+
if (state.gitInitialized && state.gitSync !== gitSync) state.gitHighlightInputs = 2;
|
|
150
|
+
state.gitSync = gitSync;
|
|
151
|
+
state.gitInitialized = true;
|
|
114
152
|
}
|
|
115
153
|
|
|
116
154
|
function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
117
|
-
|
|
155
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
156
|
+
if (state.cwd !== cwd) {
|
|
157
|
+
state.gitSync = "";
|
|
158
|
+
state.gitInitialized = false;
|
|
159
|
+
state.gitHighlightInputs = 0;
|
|
160
|
+
}
|
|
161
|
+
state.cwd = cwd;
|
|
118
162
|
state.model = ctx.model?.id ?? "no-model";
|
|
119
163
|
const usage = ctx.getContextUsage();
|
|
120
164
|
if (usage) {
|
|
@@ -203,7 +247,10 @@ function buildLine(
|
|
|
203
247
|
export default function (pi: ExtensionAPI) {
|
|
204
248
|
let config = readConfig();
|
|
205
249
|
let enabled = config.enabled !== false;
|
|
206
|
-
const state: FooterState = {
|
|
250
|
+
const state: FooterState = {
|
|
251
|
+
cwd: process.cwd(), model: "no-model", context: "?", gitSync: "",
|
|
252
|
+
gitInitialized: false, gitHighlightInputs: 0, gitRequest: 0,
|
|
253
|
+
};
|
|
207
254
|
let requestRender: (() => void) | null = null;
|
|
208
255
|
let disposeFooter: (() => void) | null = null;
|
|
209
256
|
const usageLimits = new UsageLimits(() => requestRender?.());
|
|
@@ -226,7 +273,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
273
|
}
|
|
227
274
|
|
|
228
275
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
229
|
-
|
|
276
|
+
// Branch changes are Pi's invalidation signal; refresh the async Git
|
|
277
|
+
// divergence separately because footer rendering must remain synchronous.
|
|
278
|
+
const unsub = footerData.onBranchChange(() => {
|
|
279
|
+
void updateGitSync(state).then(() => tui.requestRender());
|
|
280
|
+
tui.requestRender();
|
|
281
|
+
});
|
|
230
282
|
const request = () => tui.requestRender();
|
|
231
283
|
requestRender = request;
|
|
232
284
|
let disposed = false;
|
|
@@ -249,12 +301,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
301
|
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
250
302
|
: [];
|
|
251
303
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
304
|
+
const syncText = state.gitHighlightInputs > 0 ? theme.bold(state.gitSync) : state.gitSync;
|
|
305
|
+
const branchText = branch ? `${branch}${syncText}` : "";
|
|
252
306
|
const statusSeparator = config.powerlineSeparator ? theme.fg("dim", " ") : " ";
|
|
253
307
|
const line = buildLine(
|
|
254
308
|
width,
|
|
255
309
|
config.showPath ? theme.fg("dim", abbreviateHome(state.cwd, homedir())) : "",
|
|
256
310
|
statuses.length ? statusSeparator + statuses.map((status) => theme.fg("dim", status)).join(statusSeparator) : "",
|
|
257
|
-
|
|
311
|
+
// Keep unsynced branches readable; bold counts separately mark recent changes.
|
|
312
|
+
branchText ? theme.fg(state.gitSync ? "text" : "dim", ` ${branchText}`) : "",
|
|
258
313
|
config.showModel ? theme.bold(state.model) : "",
|
|
259
314
|
config.showContext ? theme.fg("dim", theme.bold(state.context)) : "",
|
|
260
315
|
(available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
|
|
@@ -274,6 +329,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
329
|
/** Cheap refresh: update plain state and request one render. */
|
|
275
330
|
function refresh(ctx: ExtensionContext): void {
|
|
276
331
|
updateState(ctx, state);
|
|
332
|
+
void updateGitSync(state).then(() => requestRender?.());
|
|
277
333
|
requestRender?.();
|
|
278
334
|
}
|
|
279
335
|
|
|
@@ -285,6 +341,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
285
341
|
install(ctx);
|
|
286
342
|
});
|
|
287
343
|
|
|
344
|
+
// Count user submissions, not turn_end: one prompt can trigger many tool turns.
|
|
345
|
+
// Extension-generated prompts do not consume the user's highlight window.
|
|
346
|
+
pi.on("input", (event, ctx) => {
|
|
347
|
+
if (event.source !== "extension" && state.gitHighlightInputs > 0) state.gitHighlightInputs--;
|
|
348
|
+
refresh(ctx);
|
|
349
|
+
});
|
|
350
|
+
|
|
288
351
|
pi.on("model_select", async (_event, ctx) => {
|
|
289
352
|
selectUsage(ctx);
|
|
290
353
|
refresh(ctx);
|
package/package.json
CHANGED