@erichll/pi-statusline 0.2.0 → 0.4.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 +11 -0
- package/README.md +10 -3
- package/examples/statusline.json +1 -0
- package/package.json +7 -7
- package/src/config.ts +6 -1
- package/src/panel.ts +4 -2
- package/src/render.ts +10 -0
- package/src/runtime-render.ts +2 -2
- package/src/session.ts +2 -2
- package/src/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-10-08
|
|
4
|
+
|
|
5
|
+
- Upgrade Pi AI, coding-agent, and TUI development dependencies to `^1.1.0`
|
|
6
|
+
and replace wildcard peers with the same tested baseline. Node types
|
|
7
|
+
(`^26.6.4`) and TypeScript (`^7.0.2`) already match the latest stable versions.
|
|
8
|
+
|
|
9
|
+
## 0.3.0 - 2026-10-04
|
|
10
|
+
|
|
11
|
+
- Add `spacing` (0–3 blank lines, default 1) to control the gap between the input box and the statusline; configurable in the panel and persisted in the configuration.
|
|
12
|
+
- Hide the `Compactions` count while it is zero, including the first active compaction.
|
|
13
|
+
|
|
3
14
|
## 0.2.0 - 2026-10-04
|
|
4
15
|
|
|
5
16
|
- Use the same muted color for No content and Avg; default modelDisplay to name while preserving explicit last preferences.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @erichll/pi-statusline
|
|
2
2
|
|
|
3
|
-
A configurable footer for Pi 1.
|
|
3
|
+
A configurable footer for Pi 1.1 with Git status, session usage, runtime/tool activity, themes, and live configuration preview. Uses Pi's native footer and leaves the editor intact.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
6
|
Example model(high) [█░░░░░░░░░]11.9% pi-packages main ● *2 ↑2 49.4k
|
|
@@ -13,7 +13,7 @@ Elapsed 5m 42s · Avg 45 tok/s · Cache 86% · Turns 3 · Compactions 1
|
|
|
13
13
|
pi install npm:@erichll/pi-statusline
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Requires Node.js >=22.19, Pi >=1.
|
|
16
|
+
Requires Node.js >=22.19, Pi >=1.1.0, a Nerd Font for the default Powerline layout,
|
|
17
17
|
and optionally Git. Switch icons/separators in the panel for a font-independent
|
|
18
18
|
layout. The package has no
|
|
19
19
|
other runtime dependencies and makes no network requests or credential lookups.
|
|
@@ -79,6 +79,7 @@ existing file to the new path before running `/reload`; the old path is no longe
|
|
|
79
79
|
"pathMode": "basename",
|
|
80
80
|
"modelDisplay": "name",
|
|
81
81
|
"contextDisplay": "bar",
|
|
82
|
+
"spacing": 1,
|
|
82
83
|
"segments": [
|
|
83
84
|
{
|
|
84
85
|
"id": "model",
|
|
@@ -167,6 +168,11 @@ existing file to the new path before running `/reload`; the old path is no longe
|
|
|
167
168
|
the bar completely and retain the reported percentage. Both modes use the
|
|
168
169
|
existing context warning colors. Text mode includes token counts and capacity;
|
|
169
170
|
bar mode shows progress and percentage.
|
|
171
|
+
- Spacing: `1` (default), `0`, `2` or `3` blank lines above the statusline.
|
|
172
|
+
Pi renders the footer directly under the editor, so this is the only supported
|
|
173
|
+
way to add breathing room between the input box and the statusline; the editor's
|
|
174
|
+
own bottom border cannot be removed. Choose **Spacing above statusline** in the
|
|
175
|
+
panel or set `"spacing": 0` for a flush footer.
|
|
170
176
|
- `segments`: optional ordered array of `{ id, enabled, icon?, color?, iconColor?, background? }`.
|
|
171
177
|
An explicit list is authoritative: omitted segments are disabled but remain
|
|
172
178
|
available in the panel. The `statuses` segment always occupies the second row.
|
|
@@ -245,7 +251,8 @@ footer has no Idle label or leading separator.
|
|
|
245
251
|
show `Compaction failed` / `Compaction cancelled` while no model run is active,
|
|
246
252
|
until subsequent activity.
|
|
247
253
|
- `Compactions` counts unique successful entries across the **whole session file**,
|
|
248
|
-
including other branches, not merely the selected tree path.
|
|
254
|
+
including other branches, not merely the selected tree path. It is hidden
|
|
255
|
+
while the count is zero, including during the first active compaction.
|
|
249
256
|
`runtime.enabled: false` hides the statusline's runtime metrics. Individual
|
|
250
257
|
options hide their corresponding items; `status` controls the local Elapsed
|
|
251
258
|
and compaction-result labels. The `statuses` segment independently controls other
|
package/examples/statusline.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-statusline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Configurable Pi statusline with live preview, Git and session usage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
"smoke": "python3 test/pty-smoke.py"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@earendil-works/pi-ai": "
|
|
21
|
-
"@earendil-works/pi-coding-agent": "
|
|
22
|
-
"@earendil-works/pi-tui": "
|
|
20
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
21
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
22
|
+
"@earendil-works/pi-tui": "^1.1.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@earendil-works/pi-ai": "^1.
|
|
26
|
-
"@earendil-works/pi-coding-agent": "^1.
|
|
27
|
-
"@earendil-works/pi-tui": "^1.
|
|
25
|
+
"@earendil-works/pi-ai": "^1.1.0",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "^1.1.0",
|
|
27
|
+
"@earendil-works/pi-tui": "^1.1.0",
|
|
28
28
|
"@types/node": "^26.6.4",
|
|
29
29
|
"typescript": "^7.0.2"
|
|
30
30
|
}
|
package/src/config.ts
CHANGED
|
@@ -13,7 +13,7 @@ export function presetConfig(): StatuslineConfig {
|
|
|
13
13
|
const enabled = new Set<string>(["model", "thinking", "directory", "git", "context", "tokens", "statuses"]);
|
|
14
14
|
return {
|
|
15
15
|
version: 1, enabled: true, preset: "powerline", icons: "nerd",
|
|
16
|
-
separator: "powerline", pathMode: "basename", modelDisplay: "name", contextDisplay: "bar",
|
|
16
|
+
separator: "powerline", pathMode: "basename", modelDisplay: "name", contextDisplay: "bar", spacing: 1,
|
|
17
17
|
segments: SEGMENT_IDS.map((id) => ({ id, enabled: enabled.has(id) })),
|
|
18
18
|
runtime: { enabled: true, status: true, tools: true, noContent: true, noContentSeconds: 10, avg: true, cache: true, counts: true, retainSummary: true },
|
|
19
19
|
};
|
|
@@ -29,6 +29,10 @@ function boolean(value: unknown, name: string): boolean {
|
|
|
29
29
|
if (typeof value !== "boolean") throw new Error(`Invalid ${name}`);
|
|
30
30
|
return value;
|
|
31
31
|
}
|
|
32
|
+
function integer(value: unknown, min: number, max: number, name: string): number {
|
|
33
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) throw new Error(`Invalid ${name} (${min}–${max})`);
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
32
36
|
export function parseConfig(raw: unknown): StatuslineConfig {
|
|
33
37
|
if (!record(raw)) throw new Error("Configuration must be an object");
|
|
34
38
|
if (raw.version !== undefined && raw.version !== 1) throw new Error("Unsupported configuration version");
|
|
@@ -40,6 +44,7 @@ export function parseConfig(raw: unknown): StatuslineConfig {
|
|
|
40
44
|
if (raw.pathMode !== undefined) config.pathMode = choice(raw.pathMode, ["basename", "abbreviated", "full"], "pathMode");
|
|
41
45
|
if (raw.modelDisplay !== undefined) config.modelDisplay = choice(raw.modelDisplay, ["name", "last"], "modelDisplay");
|
|
42
46
|
if (raw.contextDisplay !== undefined) config.contextDisplay = choice(raw.contextDisplay, ["text", "bar"], "contextDisplay");
|
|
47
|
+
if (raw.spacing !== undefined) config.spacing = integer(raw.spacing, 0, 3, "spacing");
|
|
43
48
|
for (const group of ["runtime"] as const) {
|
|
44
49
|
if (raw[group] === undefined) continue;
|
|
45
50
|
if (!record(raw[group])) throw new Error(`Invalid ${group}`);
|
package/src/panel.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Input, matchesKey, SelectList, truncateToWidth, type Component, type Focusable, type SelectItem } from "@earendil-works/pi-tui";
|
|
2
2
|
import { COLORS, parseConfig, presetConfig } from "./config.ts";
|
|
3
|
-
import { cleanText, renderFooter, type RenderTheme } from "./render.ts";
|
|
3
|
+
import { cleanText, renderFooter, withSpacing, type RenderTheme } from "./render.ts";
|
|
4
4
|
import type { SegmentConfig, StatuslineConfig, StatuslineSnapshot } from "./types.ts";
|
|
5
5
|
|
|
6
6
|
interface PanelOptions {
|
|
@@ -63,6 +63,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
63
63
|
{ value: "pathMode", label: `Path: ${this.draft.pathMode}` },
|
|
64
64
|
{ value: "modelDisplay", label: `Model display: ${this.draft.modelDisplay}`, description: "name: display name · last: final model ID component" },
|
|
65
65
|
{ value: "contextDisplay", label: `Context display: ${this.draft.contextDisplay}`, description: "text: tokens/capacity(percent) · bar: progress and percent" },
|
|
66
|
+
{ value: "spacing", label: `Spacing above statusline: ${this.draft.spacing}`, description: "Blank lines between the input box and the statusline (0–3)" },
|
|
66
67
|
...this.draft.segments.filter((segment) => segment.id !== "thinking").map((segment) => ({ value: `segment:${segment.id}`, label: `[${segment.enabled ? "x" : " "}] ${segment.id}`, description: segment.id === "statuses" ? "Secondary row · Space toggles, Enter edits" : "Space toggles · Alt+Up/Down reorders · Enter edits" })),
|
|
67
68
|
{ value: "runtime", label: "Runtime options" },
|
|
68
69
|
{ value: "save", label: "Save and apply globally" },
|
|
@@ -121,6 +122,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
121
122
|
case "pathMode": this.draft.pathMode = cycle(this.draft.pathMode, ["basename", "abbreviated", "full"]); break;
|
|
122
123
|
case "modelDisplay": this.draft.modelDisplay = cycle(this.draft.modelDisplay, ["name", "last"]); break;
|
|
123
124
|
case "contextDisplay": this.draft.contextDisplay = cycle(this.draft.contextDisplay, ["text", "bar"]); break;
|
|
125
|
+
case "spacing": this.draft.spacing = cycle(this.draft.spacing, [0, 1, 2, 3]); break;
|
|
124
126
|
case "runtime": this.section = value; this.rebuild(); return;
|
|
125
127
|
case "reset": this.draft = presetConfig(); break;
|
|
126
128
|
case "cancel": this.options.done(); return;
|
|
@@ -185,7 +187,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
185
187
|
if (width <= 0) return [];
|
|
186
188
|
if (this.listHeight !== Math.max(1, this.options.height() - 12)) this.rebuild(this.list.getSelectedItem()?.value);
|
|
187
189
|
const { data, example } = this.options.snapshot();
|
|
188
|
-
const preview = renderFooter(width, data, this.draft, this.options.theme());
|
|
190
|
+
const preview = withSpacing(renderFooter(width, data, this.draft, this.options.theme()), this.draft.spacing);
|
|
189
191
|
const lines = [
|
|
190
192
|
this.paint(`Statusline settings${this.editing ? ` / ${this.editing.id}` : this.section ? ` / ${this.section}` : ""}`, "accent"),
|
|
191
193
|
this.paint(example ? "Preview · example data" : "Preview · current session", "muted"),
|
package/src/render.ts
CHANGED
|
@@ -203,6 +203,16 @@ export function renderFooter(width: number, data: StatuslineSnapshot, config: St
|
|
|
203
203
|
return lines;
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Pi renders the footer directly under the editor, so leading blank lines are the only
|
|
208
|
+
* extension-side way to adjust the gap between the input box and the statusline.
|
|
209
|
+
*/
|
|
210
|
+
export function withSpacing(lines: string[], spacing: number): string[] {
|
|
211
|
+
const count = Number.isFinite(spacing) ? Math.max(0, Math.floor(spacing)) : 0;
|
|
212
|
+
if (lines.length === 0 || count === 0) return lines;
|
|
213
|
+
return [...Array<string>(count).fill(""), ...lines];
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
export function sampleSnapshot(): StatuslineSnapshot {
|
|
207
217
|
return {
|
|
208
218
|
model: "Example model", modelId: "example/provider/model", reasoning: true, thinking: "high", cwd: "/projects/pi-packages", home: "/home/example",
|
package/src/runtime-render.ts
CHANGED
|
@@ -25,7 +25,7 @@ export function renderRuntime(width: number, data: StatuslineSnapshot, config: S
|
|
|
25
25
|
else if (enabled && config.runtime.status && runtime?.runId) add("elapsed", `Elapsed ${elapsed(runtime.elapsedMs)}`);
|
|
26
26
|
if (enabled && compact?.active) {
|
|
27
27
|
add("status", `${config.icons === "ascii" ? "Compacting..." : "Compacting…"} ${seconds(compact.elapsedMs)}`, "warning");
|
|
28
|
-
if (config.runtime.counts) add("compactions", `Compactions ${compact.count}`);
|
|
28
|
+
if (config.runtime.counts && compact.count > 0) add("compactions", `Compactions ${compact.count}`);
|
|
29
29
|
} else {
|
|
30
30
|
if (enabled && runtime) {
|
|
31
31
|
if (config.runtime.status) {
|
|
@@ -46,7 +46,7 @@ export function renderRuntime(width: number, data: StatuslineSnapshot, config: S
|
|
|
46
46
|
}
|
|
47
47
|
if (config.runtime.counts && runtime.runId) add("turns", `Turns ${runtime.turns}`);
|
|
48
48
|
}
|
|
49
|
-
if (enabled && config.runtime.counts && compact) add("compactions", `Compactions ${compact.count}`);
|
|
49
|
+
if (enabled && config.runtime.counts && compact && compact.count > 0) add("compactions", `Compactions ${compact.count}`);
|
|
50
50
|
if (extensionStatus) parts.push({ id: "statuses", text: extensionStatus, styled: extensionStatus });
|
|
51
51
|
}
|
|
52
52
|
const separator = config.icons === "ascii" ? " | " : " · ";
|
package/src/session.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
|
3
3
|
import type { ExtensionContext, ReadonlyFooterDataProvider } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
5
5
|
import { emptyGit, GitPoller, type GitReader } from "./git.ts";
|
|
6
|
-
import { renderFooter } from "./render.ts";
|
|
6
|
+
import { renderFooter, withSpacing } from "./render.ts";
|
|
7
7
|
import type { StatuslineConfig, StatuslineSnapshot } from "./types.ts";
|
|
8
8
|
import { UsageCache } from "./usage.ts";
|
|
9
9
|
import { RuntimeState } from "./runtime.ts";
|
|
@@ -67,7 +67,7 @@ export class FooterSession {
|
|
|
67
67
|
this.timer = setInterval(() => this.refresh(), 1000);
|
|
68
68
|
this.timer.unref();
|
|
69
69
|
return {
|
|
70
|
-
render: (width: number) => renderFooter(width, this.snapshot(), this.config, this.ctx.ui.theme),
|
|
70
|
+
render: (width: number) => withSpacing(renderFooter(width, this.snapshot(), this.config, this.ctx.ui.theme), this.config.spacing),
|
|
71
71
|
invalidate: () => {}, // Theme and extension statuses are read fresh on every render.
|
|
72
72
|
dispose: () => this.stopResources(),
|
|
73
73
|
};
|
package/src/types.ts
CHANGED
|
@@ -33,6 +33,8 @@ export interface StatuslineConfig {
|
|
|
33
33
|
pathMode: "basename" | "abbreviated" | "full";
|
|
34
34
|
modelDisplay: "name" | "last";
|
|
35
35
|
contextDisplay: "text" | "bar";
|
|
36
|
+
/** Blank lines rendered above the statusline, between the editor and the footer (0–3). */
|
|
37
|
+
spacing: number;
|
|
36
38
|
segments: SegmentConfig[];
|
|
37
39
|
runtime: RuntimeConfig;
|
|
38
40
|
}
|