pi-minimal-footer 0.4.2 → 0.5.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 +10 -0
- package/README.md +3 -2
- package/VISION.md +2 -1
- package/extensions/index.ts +40 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Show Git upstream divergence next to the branch, for example ` main ↑2 ↓1`; `↑` is local commits to push and `↓` is upstream commits to pull
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Query Git divergence asynchronously and omit it safely when the repository has no upstream or Git is unavailable
|
|
19
|
+
|
|
10
20
|
## [0.4.2] - 2026-10-07
|
|
11
21
|
|
|
12
22
|
### Fixed
|
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 and upstream divergence: ` main ↑2 ↓1` (`↑` to push, `↓` to pull); counts appear when an upstream is configured
|
|
18
18
|
|
|
19
19
|
**Right**
|
|
20
20
|
|
|
@@ -59,7 +59,8 @@ 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
|
+
- Git sync counts are omitted for branches without an upstream, detached HEADs, and non-Git directories.
|
|
63
64
|
- `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
65
|
|
|
65
66
|
## Reading the footer
|
package/VISION.md
CHANGED
|
@@ -8,11 +8,12 @@ The [README](README.md) is the user manual. This document records current design
|
|
|
8
8
|
|
|
9
9
|
## Layout contract
|
|
10
10
|
|
|
11
|
-
- Directory and git branch form the left group, separated by ``.
|
|
11
|
+
- Directory and git branch form the left group, separated by ``. Pi supplies the branch name; the extension asynchronously runs `git rev-list --left-right --count HEAD...@{upstream}` to append local/upstream divergence as `↑N`/`↓N` (push/pull). The counts are omitted when no upstream is configured, and Git work stays outside synchronous rendering so a slow repository cannot block the TUI.
|
|
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.
|
|
15
15
|
- Measure terminal columns with `visibleWidth`, not string length. Preserve ANSI styling and wide-character accounting when truncating.
|
|
16
|
+
- Git divergence refreshes on Pi's branch-change signal and normal footer state refreshes; it is best-effort and never allowed to prevent the footer from loading.
|
|
16
17
|
|
|
17
18
|
### Space allocation
|
|
18
19
|
|
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,6 +115,32 @@ interface FooterState {
|
|
|
111
115
|
cwd: string;
|
|
112
116
|
model: string;
|
|
113
117
|
context: string;
|
|
118
|
+
gitSync: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function getGitSync(cwd: string): Promise<string> {
|
|
122
|
+
try {
|
|
123
|
+
// Pi exposes the branch name, but not upstream divergence. Keep this
|
|
124
|
+
// asynchronous and outside render() so a slow Git repository cannot block TUI rendering.
|
|
125
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-list", "--left-right", "--count", "HEAD...@{upstream}"], {
|
|
126
|
+
timeout: 1000,
|
|
127
|
+
maxBuffer: 1024,
|
|
128
|
+
});
|
|
129
|
+
// With HEAD...@{upstream}, left is local-only commits (push) and right
|
|
130
|
+
// is upstream-only commits (pull). An absent upstream is intentionally hidden.
|
|
131
|
+
const [ahead, behind] = stdout.trim().split(/\s+/).map(Number);
|
|
132
|
+
if (!Number.isInteger(ahead) || !Number.isInteger(behind)) return "";
|
|
133
|
+
return `${ahead ? ` ↑${ahead}` : ""}${behind ? ` ↓${behind}` : ""}`;
|
|
134
|
+
} catch {
|
|
135
|
+
// No repository, upstream, or reachable Git command: omit sync status.
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function updateGitSync(state: FooterState): Promise<void> {
|
|
141
|
+
const cwd = state.cwd;
|
|
142
|
+
const gitSync = await getGitSync(cwd);
|
|
143
|
+
if (state.cwd === cwd) state.gitSync = gitSync;
|
|
114
144
|
}
|
|
115
145
|
|
|
116
146
|
function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
@@ -203,7 +233,7 @@ function buildLine(
|
|
|
203
233
|
export default function (pi: ExtensionAPI) {
|
|
204
234
|
let config = readConfig();
|
|
205
235
|
let enabled = config.enabled !== false;
|
|
206
|
-
const state: FooterState = { cwd: process.cwd(), model: "no-model", context: "?" };
|
|
236
|
+
const state: FooterState = { cwd: process.cwd(), model: "no-model", context: "?", gitSync: "" };
|
|
207
237
|
let requestRender: (() => void) | null = null;
|
|
208
238
|
let disposeFooter: (() => void) | null = null;
|
|
209
239
|
const usageLimits = new UsageLimits(() => requestRender?.());
|
|
@@ -226,7 +256,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
226
256
|
}
|
|
227
257
|
|
|
228
258
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
229
|
-
|
|
259
|
+
// Branch changes are Pi's invalidation signal; refresh the async Git
|
|
260
|
+
// divergence separately because footer rendering must remain synchronous.
|
|
261
|
+
const unsub = footerData.onBranchChange(() => {
|
|
262
|
+
void updateGitSync(state).then(() => tui.requestRender());
|
|
263
|
+
tui.requestRender();
|
|
264
|
+
});
|
|
230
265
|
const request = () => tui.requestRender();
|
|
231
266
|
requestRender = request;
|
|
232
267
|
let disposed = false;
|
|
@@ -249,12 +284,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
249
284
|
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
250
285
|
: [];
|
|
251
286
|
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
287
|
+
const branchText = branch ? `${branch}${state.gitSync}` : "";
|
|
252
288
|
const statusSeparator = config.powerlineSeparator ? theme.fg("dim", " ") : " ";
|
|
253
289
|
const line = buildLine(
|
|
254
290
|
width,
|
|
255
291
|
config.showPath ? theme.fg("dim", abbreviateHome(state.cwd, homedir())) : "",
|
|
256
292
|
statuses.length ? statusSeparator + statuses.map((status) => theme.fg("dim", status)).join(statusSeparator) : "",
|
|
257
|
-
|
|
293
|
+
branchText ? theme.fg("dim", ` ${branchText}`) : "",
|
|
258
294
|
config.showModel ? theme.bold(state.model) : "",
|
|
259
295
|
config.showContext ? theme.fg("dim", theme.bold(state.context)) : "",
|
|
260
296
|
(available, maxCells = BASE_BAR_CELLS) => usageLimits.line(available, theme, maxCells),
|
|
@@ -274,6 +310,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
274
310
|
/** Cheap refresh: update plain state and request one render. */
|
|
275
311
|
function refresh(ctx: ExtensionContext): void {
|
|
276
312
|
updateState(ctx, state);
|
|
313
|
+
void updateGitSync(state).then(() => requestRender?.());
|
|
277
314
|
requestRender?.();
|
|
278
315
|
}
|
|
279
316
|
|
package/package.json
CHANGED