pi-minimal-footer 0.1.2 → 0.1.3
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 +23 -1
- package/README.md +14 -6
- package/extensions/index.ts +122 -96
- package/media/github-preview.png +0 -0
- package/media/preview.png +0 -0
- package/package.json +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.1.3] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Strict TypeScript type checking for the Pi extension
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Refresh README and package-gallery screenshots with Pi 1.0.3 and the current minimal footer
|
|
19
|
+
- Refresh context after persisted turns, final agent settlement, compaction, and session-tree navigation without reinstalling the footer on each event
|
|
20
|
+
- Read `minFooter` from Pi's configured agent directory at session start or explicit toggle; retain unrelated settings
|
|
21
|
+
- On narrow terminals, shrink/drop the path, then extension statuses, then git branch before truncating model/context
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Preserve right alignment on narrow terminals after dropping extension statuses or git branch, including truncated model/context
|
|
26
|
+
- Avoid stale branch subscriptions and render references after footer teardown
|
|
27
|
+
- Abbreviate home only for the exact home path or its children
|
|
28
|
+
- Show the custom footer only in terminal UI mode
|
|
29
|
+
- Clarify that `showSkills` displays extension statuses, not installed skills
|
|
30
|
+
|
|
10
31
|
## [0.1.2] - 2026-09-01
|
|
11
32
|
|
|
12
33
|
### Changed
|
|
@@ -65,7 +86,8 @@ First npm-ready release.
|
|
|
65
86
|
|
|
66
87
|
- Project scaffold — `extensions/index.ts` with basic footer structure, `package.json` with pi extension manifest, `README.md`, `LICENSE` (MIT)
|
|
67
88
|
|
|
68
|
-
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.
|
|
89
|
+
[Unreleased]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.3...HEAD
|
|
90
|
+
[0.1.3]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.2...v0.1.3
|
|
69
91
|
[0.1.2]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.1...v0.1.2
|
|
70
92
|
[0.1.1]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.1.0...v0.1.1
|
|
71
93
|
[0.1.0]: https://github.com/Ryu-CZ/pi-minimal-footer/compare/v0.0.0...v0.1.0
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
A clean, compact one-line footer for [Pi](https://github.com/earendil-works/pi).
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
~/path/to/dir (
|
|
21
|
+
~/path/to/dir (status1 | status2) main sonnet 12/128k
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|

|
|
@@ -41,13 +41,21 @@ cp -r extensions/* ~/.pi/agent/extensions/
|
|
|
41
41
|
ln -s "$PWD/extensions" ~/.pi/agent/extensions/minimal-footer
|
|
42
42
|
```
|
|
43
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
|
+
|
|
44
48
|
## Features
|
|
45
49
|
|
|
46
|
-
- **Working directory** —
|
|
47
|
-
- **
|
|
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)
|
|
48
52
|
- **Git branch** — current branch name
|
|
49
53
|
- **Model** — active model ID
|
|
50
|
-
- **Context usage** — tokens used / context window (e.g., `12/128k`)
|
|
54
|
+
- **Context usage** — tokens used / context window (e.g., `12/128k`). After compaction, Pi reports usage as unknown (`?`) until a subsequent model response.
|
|
55
|
+
|
|
56
|
+
On narrow terminals, the footer shrinks or drops the path, then drops extension statuses and the git branch before truncating model/context. The remaining segments stay right-aligned.
|
|
57
|
+
|
|
58
|
+
The footer appears only in Pi's interactive terminal UI, not in print, JSON, or RPC modes.
|
|
51
59
|
|
|
52
60
|
## Commands
|
|
53
61
|
|
|
@@ -57,11 +65,11 @@ ln -s "$PWD/extensions" ~/.pi/agent/extensions/minimal-footer
|
|
|
57
65
|
| `/minfooter on` | Enable |
|
|
58
66
|
| `/minfooter off` | Disable |
|
|
59
67
|
|
|
60
|
-
> The `/minfooter` command only toggles the `enabled` flag. To show or hide individual segments, edit
|
|
68
|
+
> The `/minfooter` command only toggles the `enabled` flag. To show or hide individual segments, edit the agent settings file directly.
|
|
61
69
|
|
|
62
70
|
## Configuration
|
|
63
71
|
|
|
64
|
-
Settings live in
|
|
72
|
+
Settings live in Pi's configured agent directory, in `settings.json` under the `minFooter` key (normally `~/.pi/agent/settings.json`). The footer reads settings on session start and when `/minfooter` is run; external edits do not take effect until one of those actions (there is no file watcher). Unrelated settings are preserved when toggling.
|
|
65
73
|
|
|
66
74
|
```json
|
|
67
75
|
{
|
package/extensions/index.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Minimal footer — replaces pi's default footer with a clean status line:
|
|
3
3
|
*
|
|
4
|
-
* ~/path/to/dir (skill1 | skill2) main sonnet
|
|
4
|
+
* ~/path/to/dir (skill1 | skill2) main sonnet 12/128k
|
|
5
5
|
*
|
|
6
|
-
* Settings are persisted in ~/.pi/agent
|
|
6
|
+
* Settings are persisted in the agent directory (usually ~/.pi/agent)
|
|
7
|
+
* settings.json under "minFooter".
|
|
7
8
|
*
|
|
8
9
|
* Commands:
|
|
9
10
|
* /minfooter — toggle on/off
|
|
10
11
|
* /minfooter on|off — force state
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
import type { ExtensionAPI, ExtensionContext
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
14
16
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
15
17
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
|
-
import { join, dirname } from "node:path";
|
|
18
|
+
import { join, dirname, sep } from "node:path";
|
|
17
19
|
import { homedir } from "node:os";
|
|
18
20
|
|
|
19
21
|
// ── Settings ──────────────────────────────────────────────────────────
|
|
@@ -41,7 +43,7 @@ const DEFAULT_SETTINGS: FooterSettings = {
|
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
function settingsPath(): string {
|
|
44
|
-
return join(
|
|
46
|
+
return join(getAgentDir(), "settings.json");
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function readSettings(): Settings {
|
|
@@ -64,11 +66,6 @@ function writeSettings(patch: Partial<Settings>): void {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
function readEnabled(): boolean {
|
|
68
|
-
const s = readSettings();
|
|
69
|
-
return s.minFooter?.enabled !== false; // default to true
|
|
70
|
-
}
|
|
71
|
-
|
|
72
69
|
function readConfig(): FooterSettings {
|
|
73
70
|
const s = readSettings();
|
|
74
71
|
return { ...DEFAULT_SETTINGS, ...s.minFooter };
|
|
@@ -89,19 +86,22 @@ function formatSize(n: number | null | undefined): string {
|
|
|
89
86
|
return String(n);
|
|
90
87
|
}
|
|
91
88
|
|
|
89
|
+
/** Abbreviate only an exact home match or a boundary-prefixed child path. */
|
|
90
|
+
function abbreviateHome(p: string, home: string): string {
|
|
91
|
+
if (p === home) return "~";
|
|
92
|
+
if (p.startsWith(home + sep)) return "~" + p.slice(home.length);
|
|
93
|
+
return p;
|
|
94
|
+
}
|
|
95
|
+
|
|
92
96
|
// ── State ─────────────────────────────────────────────────────────────
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
context: "?",
|
|
100
|
-
branch: "",
|
|
101
|
-
footerData: null as ReadonlyFooterDataProvider | null,
|
|
102
|
-
};
|
|
98
|
+
interface FooterState {
|
|
99
|
+
cwd: string;
|
|
100
|
+
model: string;
|
|
101
|
+
context: string;
|
|
102
|
+
}
|
|
103
103
|
|
|
104
|
-
function updateState(ctx: ExtensionContext): void {
|
|
104
|
+
function updateState(ctx: ExtensionContext, state: FooterState): void {
|
|
105
105
|
state.cwd = ctx.cwd ?? process.cwd();
|
|
106
106
|
state.model = ctx.model?.id ?? "no-model";
|
|
107
107
|
const usage = ctx.getContextUsage();
|
|
@@ -121,101 +121,126 @@ function updateState(ctx: ExtensionContext): void {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
// ──
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (config.showGitBranch) {
|
|
147
|
-
const branch = footerData.getGitBranch();
|
|
148
|
-
if (branch) segments.push(` ${branch}`);
|
|
124
|
+
// ── Layout ────────────────────────────────────────────────────────────
|
|
125
|
+
//
|
|
126
|
+
// Priority when space runs out:
|
|
127
|
+
// 1. drop/truncate the path first (truncate, then drop entirely)
|
|
128
|
+
// 2. drop extension statuses
|
|
129
|
+
// 3. drop the git branch
|
|
130
|
+
// 4. retain model/context as long as possible
|
|
131
|
+
// 5. last resort: truncate the remaining model/context to fit
|
|
132
|
+
//
|
|
133
|
+
// Every returned line is guaranteed to fit `width` (visible-width safe).
|
|
134
|
+
|
|
135
|
+
function buildLine(width: number, path: string, statuses: string, branch: string, model: string, context: string): string {
|
|
136
|
+
if (width <= 0) return "";
|
|
137
|
+
const core = [model, context].filter(Boolean).join(" ");
|
|
138
|
+
const right = [statuses, branch, core].filter(Boolean).join(" ");
|
|
139
|
+
const rightWidth = visibleWidth(right);
|
|
140
|
+
|
|
141
|
+
if (rightWidth < width) {
|
|
142
|
+
const left = path ? truncateToWidth(path, width - rightWidth - 1, "...", true) : " ".repeat(width - rightWidth - 1);
|
|
143
|
+
return `${left} ${right}`;
|
|
149
144
|
}
|
|
150
145
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return segments.join(" ");
|
|
146
|
+
// Drop the path, then extension statuses, then git; protect model/context until last.
|
|
147
|
+
let remaining = right;
|
|
148
|
+
if (rightWidth > width) remaining = [branch, core].filter(Boolean).join(" ");
|
|
149
|
+
if (visibleWidth(remaining) > width) remaining = core;
|
|
150
|
+
remaining = truncateToWidth(remaining, width, "...");
|
|
151
|
+
return " ".repeat(width - visibleWidth(remaining)) + remaining;
|
|
160
152
|
}
|
|
161
153
|
|
|
162
154
|
// ── Extension ─────────────────────────────────────────────────────────
|
|
163
155
|
|
|
164
156
|
export default function (pi: ExtensionAPI) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const left = buildLeft();
|
|
180
|
-
const right = buildRight(footerData, (s) => theme.bold(s));
|
|
181
|
-
const rightW = visibleWidth(right);
|
|
182
|
-
|
|
183
|
-
// If the right side (model + context) alone doesn't fit, drop the path and truncate it.
|
|
184
|
-
if (rightW >= width) {
|
|
185
|
-
return [theme.fg("dim", truncateToWidth(right, width, ""))];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Otherwise keep the right side intact and fit the path into the remaining
|
|
189
|
-
// space (padded to exactly fill it, keeping the right side right-aligned).
|
|
190
|
-
const leftPart = truncateToWidth(left, width - rightW - 1, "...", true);
|
|
191
|
-
return [theme.fg("dim", `${leftPart} ${right}`)];
|
|
192
|
-
},
|
|
193
|
-
invalidate(): void {},
|
|
194
|
-
dispose: unsub,
|
|
195
|
-
};
|
|
196
|
-
});
|
|
197
|
-
} else {
|
|
198
|
-
ctx.ui.setFooter(undefined);
|
|
157
|
+
let config = readConfig();
|
|
158
|
+
let enabled = config.enabled !== false;
|
|
159
|
+
const state: FooterState = { cwd: process.cwd(), model: "no-model", context: "?" };
|
|
160
|
+
let requestRender: (() => void) | null = null;
|
|
161
|
+
let disposeFooter: (() => void) | null = null;
|
|
162
|
+
|
|
163
|
+
function install(ctx: ExtensionContext): void {
|
|
164
|
+
config = readConfig();
|
|
165
|
+
enabled = config.enabled !== false;
|
|
166
|
+
if (ctx.mode !== "tui") return;
|
|
167
|
+
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
if (disposeFooter) ctx.ui.setFooter(undefined);
|
|
170
|
+
return;
|
|
199
171
|
}
|
|
172
|
+
|
|
173
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
174
|
+
const unsub = footerData.onBranchChange(() => tui.requestRender());
|
|
175
|
+
const request = () => tui.requestRender();
|
|
176
|
+
requestRender = request;
|
|
177
|
+
let disposed = false;
|
|
178
|
+
const dispose = () => {
|
|
179
|
+
if (disposed) return;
|
|
180
|
+
disposed = true;
|
|
181
|
+
unsub();
|
|
182
|
+
if (requestRender === request) requestRender = null;
|
|
183
|
+
if (disposeFooter === dispose) disposeFooter = null;
|
|
184
|
+
};
|
|
185
|
+
disposeFooter = dispose;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
render(width: number): string[] {
|
|
189
|
+
const skills = config.showSkills
|
|
190
|
+
? [...footerData.getExtensionStatuses().values()].filter((s) => s.trim())
|
|
191
|
+
: [];
|
|
192
|
+
const branch = config.showGitBranch ? footerData.getGitBranch() : null;
|
|
193
|
+
const line = buildLine(
|
|
194
|
+
width,
|
|
195
|
+
config.showPath ? abbreviateHome(state.cwd, homedir()) : "",
|
|
196
|
+
skills.length ? `(${skills.join(" | ")})` : "",
|
|
197
|
+
branch ? ` ${branch}` : "",
|
|
198
|
+
config.showModel ? theme.bold(state.model) : "",
|
|
199
|
+
config.showContext ? theme.bold(state.context) : "",
|
|
200
|
+
);
|
|
201
|
+
return [theme.fg("dim", line)];
|
|
202
|
+
},
|
|
203
|
+
invalidate(): void {},
|
|
204
|
+
dispose,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Cheap refresh: update plain state and request one render. */
|
|
210
|
+
function refresh(ctx: ExtensionContext): void {
|
|
211
|
+
updateState(ctx, state);
|
|
212
|
+
requestRender?.();
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
// ── Lifecycle events
|
|
215
|
+
// ── Lifecycle events (passive; no footer re-install) ────────────────
|
|
203
216
|
|
|
204
217
|
pi.on("session_start", async (_event, ctx) => {
|
|
205
|
-
|
|
206
|
-
|
|
218
|
+
refresh(ctx);
|
|
219
|
+
install(ctx);
|
|
207
220
|
});
|
|
208
221
|
|
|
209
222
|
pi.on("model_select", async (_event, ctx) => {
|
|
210
|
-
|
|
223
|
+
refresh(ctx);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
pi.on("turn_end", async (_event, ctx) => {
|
|
227
|
+
refresh(ctx);
|
|
211
228
|
});
|
|
212
229
|
|
|
213
|
-
pi.on("
|
|
214
|
-
|
|
230
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
231
|
+
refresh(ctx);
|
|
215
232
|
});
|
|
216
233
|
|
|
217
234
|
pi.on("session_compact", async (_event, ctx) => {
|
|
218
|
-
|
|
235
|
+
refresh(ctx);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
239
|
+
refresh(ctx);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
pi.on("session_shutdown", async () => {
|
|
243
|
+
disposeFooter?.();
|
|
219
244
|
});
|
|
220
245
|
|
|
221
246
|
// ── Command ─────────────────────────────────────────────────────────
|
|
@@ -227,7 +252,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
227
252
|
else if (args === "off") enabled = false;
|
|
228
253
|
else enabled = !enabled;
|
|
229
254
|
writeEnabled(enabled);
|
|
230
|
-
|
|
255
|
+
refresh(ctx);
|
|
256
|
+
install(ctx);
|
|
231
257
|
ctx.ui.notify(`Minimal footer ${enabled ? "enabled" : "disabled"}`, "info");
|
|
232
258
|
},
|
|
233
259
|
});
|
package/media/github-preview.png
CHANGED
|
Binary file
|
package/media/preview.png
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-minimal-footer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A minimal footer extension for pi — clean status line with path, skills, model, and context usage.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -30,10 +30,20 @@
|
|
|
30
30
|
"type": "git",
|
|
31
31
|
"url": "git+https://github.com/Ryu-CZ/pi-minimal-footer.git"
|
|
32
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node --test --test-concurrency=1 tests/*.test.mjs",
|
|
35
|
+
"check": "tsc --noEmit"
|
|
36
|
+
},
|
|
33
37
|
"peerDependencies": {
|
|
34
38
|
"@earendil-works/pi-coding-agent": "*",
|
|
35
39
|
"@earendil-works/pi-tui": "*"
|
|
36
40
|
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "1.0.3",
|
|
43
|
+
"@earendil-works/pi-tui": "1.0.3",
|
|
44
|
+
"@types/node": "22.20.5",
|
|
45
|
+
"typescript": "7.0.2"
|
|
46
|
+
},
|
|
37
47
|
"pi": {
|
|
38
48
|
"extensions": [
|
|
39
49
|
"./extensions"
|