@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 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.0 with Git status, session usage, runtime/tool activity, themes, and live configuration preview. Uses Pi's native footer and leaves the editor intact.
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.0.0, a Nerd Font for the default Powerline layout,
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
@@ -7,6 +7,7 @@
7
7
  "pathMode": "basename",
8
8
  "modelDisplay": "name",
9
9
  "contextDisplay": "bar",
10
+ "spacing": 1,
10
11
  "segments": [
11
12
  {
12
13
  "id": "model",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erichll/pi-statusline",
3
- "version": "0.2.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.0.0",
26
- "@earendil-works/pi-coding-agent": "^1.0.0",
27
- "@earendil-works/pi-tui": "^1.0.0",
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",
@@ -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
  }