@pi-vault/pi-status 0.2.0 → 0.2.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 ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@pi-vault/pi-status` are documented in this file.
4
+
5
+ ## 0.2.1 - 2026-06-14
6
+
7
+ ### Changed
8
+
9
+ - Updated the Pi host baseline to the `0.79.x` package line and refreshed the packaged dependency set.
10
+ - Reworked the README around install, reload, `/statusline`, footer segments, and `pi-usage` integration so the published docs match current behavior.
11
+ - Added this changelog to the published package contents.
12
+
13
+ ### Internal
14
+
15
+ - Refactored internal snapshot and runtime-state code without changing the public behavior of the extension.
16
+ - Exported `formatSegment` with full test coverage to harden segment rendering behavior.
17
+
18
+ ## 0.2.0 - 2026-06-07
19
+
20
+ ### Added
21
+
22
+ - Screenshots for the live footer and interactive `/statusline` editor.
23
+ - A usage runtime that integrates with `@pi-vault/pi-usage` for live limit-backed footer segments.
24
+
25
+ ### Changed
26
+
27
+ - Upgraded usage-backed segments to the `@pi-vault/pi-usage@0.2.x` line.
28
+ - Consolidated the TUI implementation and theme plumbing used by the footer preview and `/statusline`.
29
+ - Refreshed the README to cover the shipped UI and configuration flow.
30
+
31
+ ## 0.1.0 - 2026-06-02
32
+
33
+ ### Added
34
+
35
+ - Initial release of the Pi status line extension.
36
+ - A footer that can replace Pi's default footer with configurable status segments.
37
+ - The `/statusline` interactive editor for enabling, disabling, reordering, and previewing segments.
38
+ - Settings persistence through Pi's `settings.json` with project and global loading behavior.
39
+ - Segment support for model, reasoning level, project name, working directory, Git branch, run state, context metrics, token counts, session ID, usage limits, and extension statuses.
40
+ - Filtering controls for visible extension statuses.
41
+
42
+ ### Changed
43
+
44
+ - Iterated on the `/statusline` UI to use sectioned rows, search, inline rendering, live preview, theme adaptation, and footer suppression while editing.
package/README.md CHANGED
@@ -5,58 +5,63 @@
5
5
  [![Node >= 22.19](https://img.shields.io/badge/node-%3E%3D22.19-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
7
7
 
8
- Replace Pi's default footer with a cleaner Codex-style status line that stays out of the way but keeps the useful bits visible.
8
+ Replace Pi's default footer with a compact status line that shows the session details you actually care about. The extension installs a live footer and adds `/statusline`, an interactive editor for choosing, ordering, and previewing footer segments.
9
9
 
10
- Default status line:
10
+ Default footer:
11
11
 
12
- `model-with-reasoning · current-dir`
12
+ ```text
13
+ model-with-reasoning · current-dir
14
+ ```
13
15
 
14
- ## Install
16
+ ## Screenshots
15
17
 
16
- Install `pi-status`:
18
+ Default status line rendering:
19
+
20
+ ![Status line UI](docs/assets/statusline-ui.png)
21
+
22
+ Interactive configuration editor (`/statusline`):
23
+
24
+ ![Status line configuration](docs/assets/statusline-configuration.png)
25
+
26
+ ## Install And Reload
27
+
28
+ Install the extension:
17
29
 
18
30
  ```bash
19
31
  pi install npm:@pi-vault/pi-status
20
32
  ```
21
33
 
22
- Optional: install `pi-usage` if you want `/usage` plus the usage-backed footer segments:
34
+ Optional: install `pi-usage` if you want the `five-hour-limit` and `weekly-limit` footer segments:
23
35
 
24
36
  ```bash
25
37
  pi install npm:@pi-vault/pi-usage
26
38
  ```
27
39
 
28
- Then reload Pi:
40
+ Reload Pi after installing or upgrading:
29
41
 
30
42
  ```bash
31
43
  /reload
32
44
  ```
33
45
 
34
- ## Screenshots
46
+ ## Use `/statusline`
35
47
 
36
- Default status line rendering:
48
+ Once installed, the footer updates automatically. Run `/statusline` inside Pi to open the interactive editor.
37
49
 
38
- ![Status line UI](docs/assets/statusline-ui.png)
39
-
40
- Interactive configuration editor (`/statusline`):
41
-
42
- ![Status line configuration](docs/assets/statusline-configuration.png)
43
-
44
- ## Use
45
-
46
- Once installed, the footer updates automatically.
47
-
48
- Use `/statusline` inside Pi to:
50
+ The editor lets you:
49
51
 
50
52
  - turn footer items on or off
51
- - reorder the items you want to see
53
+ - reorder enabled items with `Left` and `Right`
54
+ - search the segment list
52
55
  - preview the result before saving
53
56
  - control which extension status messages are shown
54
57
 
55
58
  Changes are saved and reused the next time Pi starts.
56
59
 
57
- ## Available Status Items
60
+ During editing, the live footer is temporarily hidden so the inline UI can use the full width cleanly.
58
61
 
59
- You can build your footer from these items:
62
+ ## Available Footer Items
63
+
64
+ You can compose the footer from these segment IDs:
60
65
 
61
66
  - `model`
62
67
  - `model-with-reasoning`
@@ -75,9 +80,11 @@ You can build your footer from these items:
75
80
  - `weekly-limit`
76
81
  - `extension-statuses`
77
82
 
78
- `five-hour-limit` and `weekly-limit` require standalone [`@pi-vault/pi-usage`](https://www.npmjs.com/package/@pi-vault/pi-usage). When `pi-usage` is not installed or has not responded yet, those items are hidden from `/statusline` and omitted from the footer.
83
+ `five-hour-limit` and `weekly-limit` depend on standalone [`@pi-vault/pi-usage`](https://www.npmjs.com/package/@pi-vault/pi-usage). When `pi-usage` is not installed or has not responded yet, those segments are hidden from `/statusline` and omitted from the footer.
84
+
85
+ `extension-statuses` renders the visible extension status values reported by Pi extensions. `/statusline` also lets you hide individual status keys or switch to an allowlist.
79
86
 
80
- ## Common Setups
87
+ ## Common Examples
81
88
 
82
89
  Keep it minimal:
83
90
 
@@ -91,19 +98,30 @@ Show more session detail:
91
98
  model · run-state · git-branch · context-used · context-remaining · session-id
92
99
  ```
93
100
 
101
+ Usage-aware footer:
102
+
103
+ ```text
104
+ model-with-reasoning · current-dir · five-hour-limit · weekly-limit
105
+ ```
106
+
107
+ Show extension activity too:
108
+
109
+ ```text
110
+ model · current-dir · extension-statuses
111
+ ```
112
+
94
113
  ## Compatibility
95
114
 
96
115
  - Node.js `>=22.19`
97
116
  - Pi host environment with `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`
98
- - Tested in this repo against `@earendil-works/pi-coding-agent@0.78.x` and `@earendil-works/pi-tui@0.78.x`
117
+ - Tested in this repo against `@earendil-works/pi-coding-agent@0.79.3` and `@earendil-works/pi-tui@0.79.3`
99
118
 
100
- ## Development Setup
119
+ ## Development
101
120
 
102
121
  ```bash
103
122
  pnpm install
104
123
  pnpm check
105
- pnpm pack --dry-run
106
- pi -e .
124
+ pnpm run pack:dry-run
107
125
  ```
108
126
 
109
127
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-vault/pi-status",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Pi extension that replaces the default status with a Codex-like status",
6
6
  "author": "Lanh Hoang <lanhhoang@users.noreply.github.com>",
@@ -38,10 +38,11 @@
38
38
  "files": [
39
39
  "src",
40
40
  "docs/assets",
41
+ "CHANGELOG.md",
41
42
  "README.md"
42
43
  ],
43
44
  "dependencies": {
44
- "@pi-vault/pi-usage": "^0.2.0"
45
+ "@pi-vault/pi-usage": "^0.4.0"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "@earendil-works/pi-coding-agent": "*",
@@ -49,8 +50,8 @@
49
50
  },
50
51
  "devDependencies": {
51
52
  "@biomejs/biome": "^2.4.16",
52
- "@earendil-works/pi-coding-agent": "^0.78.0",
53
- "@earendil-works/pi-tui": "^0.78.0",
53
+ "@earendil-works/pi-coding-agent": "^0.79.3",
54
+ "@earendil-works/pi-tui": "^0.79.3",
54
55
  "@types/node": "^25.9.1",
55
56
  "typescript": "^6.0.3",
56
57
  "vitest": "^4.1.7"
@@ -0,0 +1,75 @@
1
+ import type { FooterRenderInput, ModelLike, RunState } from "../tui/render.ts";
2
+
3
+ export type SnapshotInput = {
4
+ model?: ModelLike;
5
+ cwd: string;
6
+ thinkingLevel: string;
7
+ gitBranch: string | null;
8
+ isIdle: boolean;
9
+ hasPendingMessages: boolean;
10
+ contextUsage?: {
11
+ tokens?: number | null;
12
+ contextWindow?: number;
13
+ percent?: number | null;
14
+ };
15
+ branch: unknown[];
16
+ sessionId: string;
17
+ usageState?: FooterRenderInput["usageState"];
18
+ extensionStatuses: ReadonlyMap<string, string>;
19
+ };
20
+
21
+ function aggregateBranchTotals(branch: unknown[]): {
22
+ input: number;
23
+ output: number;
24
+ totalTokens: number;
25
+ } {
26
+ const totals = { input: 0, output: 0, totalTokens: 0 };
27
+
28
+ for (const entry of branch ?? []) {
29
+ if (!entry || typeof entry !== "object") continue;
30
+ if ((entry as { type?: unknown }).type !== "message") continue;
31
+ const message = (
32
+ entry as {
33
+ message?: {
34
+ role?: unknown;
35
+ usage?: { input?: number; output?: number; totalTokens?: number };
36
+ };
37
+ }
38
+ ).message;
39
+ if (message?.role !== "assistant") continue;
40
+ const usage = message.usage;
41
+ if (!usage) continue;
42
+ if (typeof usage.input === "number") totals.input += usage.input;
43
+ if (typeof usage.output === "number") totals.output += usage.output;
44
+ if (typeof usage.totalTokens === "number")
45
+ totals.totalTokens += usage.totalTokens;
46
+ }
47
+
48
+ return totals;
49
+ }
50
+
51
+ function deriveRunState(
52
+ isIdle: boolean,
53
+ hasPendingMessages: boolean,
54
+ ): RunState {
55
+ if (!isIdle) return "busy";
56
+ if (hasPendingMessages) return "queued";
57
+ return "idle";
58
+ }
59
+
60
+ export function buildSnapshot(
61
+ input: SnapshotInput,
62
+ ): Omit<FooterRenderInput, "segments" | "filter"> {
63
+ return {
64
+ model: input.model,
65
+ cwd: input.cwd,
66
+ thinkingLevel: input.thinkingLevel,
67
+ gitBranch: input.gitBranch,
68
+ runState: deriveRunState(input.isIdle, input.hasPendingMessages),
69
+ contextUsage: input.contextUsage,
70
+ branchTotals: aggregateBranchTotals(input.branch),
71
+ sessionId: input.sessionId,
72
+ usageState: input.usageState,
73
+ extensionStatuses: input.extensionStatuses,
74
+ };
75
+ }
package/src/index.ts CHANGED
@@ -2,10 +2,8 @@ import type {
2
2
  ExtensionAPI,
3
3
  ExtensionContext,
4
4
  } from "@earendil-works/pi-coding-agent";
5
- import {
6
- loadConfig,
7
- saveConfigToSettings,
8
- } from "./core/config.ts";
5
+ import { loadConfig, saveConfigToSettings } from "./core/config.ts";
6
+ import { buildSnapshot } from "./core/snapshot.ts";
9
7
  import { createUsageRuntime } from "./core/usage-runtime.ts";
10
8
  import type { PiStatusConfig } from "./shared/types.ts";
11
9
  import { createStatusLineEditor } from "./tui/editor.ts";
@@ -30,6 +28,24 @@ type FooterFactory = (
30
28
  footerData: FooterDataLike,
31
29
  ) => FooterComponent;
32
30
 
31
+ type RuntimeState = {
32
+ config: PiStatusConfig;
33
+ ctx: ExtensionContext | undefined;
34
+ requestRender: (() => void) | undefined;
35
+ gitBranch: string | null;
36
+ extensionStatuses: Map<string, string>;
37
+ };
38
+
39
+ function createRuntimeState(): RuntimeState {
40
+ return {
41
+ config: loadConfig().config,
42
+ ctx: undefined,
43
+ requestRender: undefined,
44
+ gitBranch: null,
45
+ extensionStatuses: new Map(),
46
+ };
47
+ }
48
+
33
49
  const EMPTY_FOOTER_FACTORY: FooterFactory = () => ({
34
50
  render(): string[] {
35
51
  return [];
@@ -41,91 +57,64 @@ const EMPTY_FOOTER_FACTORY: FooterFactory = () => ({
41
57
  function isLiveTheme(value: unknown): boolean {
42
58
  if (!value || typeof value !== "object") return false;
43
59
  const candidate = value as { fg?: unknown; bold?: unknown };
44
- return typeof candidate.fg === "function" && typeof candidate.bold === "function";
45
- }
46
-
47
- function aggregateBranchTotals(ctx: ExtensionContext): {
48
- input: number;
49
- output: number;
50
- totalTokens: number;
51
- } {
52
- const totals = { input: 0, output: 0, totalTokens: 0 };
53
- const branch = ctx.sessionManager.getBranch() as unknown[];
54
-
55
- for (const entry of branch ?? []) {
56
- if (!entry || typeof entry !== "object") continue;
57
- if ((entry as { type?: unknown }).type !== "message") continue;
58
- const message = (
59
- entry as {
60
- message?: {
61
- role?: unknown;
62
- usage?: { input?: number; output?: number; totalTokens?: number };
63
- };
64
- }
65
- ).message;
66
- if (message?.role !== "assistant") continue;
67
- const usage = message.usage;
68
- if (!usage) continue;
69
- if (typeof usage.input === "number") totals.input += usage.input;
70
- if (typeof usage.output === "number") totals.output += usage.output;
71
- if (typeof usage.totalTokens === "number") totals.totalTokens += usage.totalTokens;
72
- }
73
-
74
- return totals;
60
+ return (
61
+ typeof candidate.fg === "function" && typeof candidate.bold === "function"
62
+ );
75
63
  }
76
64
 
77
65
  export default function createExtension(pi: ExtensionAPI): void {
78
- let runtimeConfig: PiStatusConfig = loadConfig().config;
79
- let currentCtx: ExtensionContext | undefined;
80
- let requestRender: (() => void) | undefined;
81
- let lastGitBranch: string | null = null;
82
- let lastExtensionStatuses = new Map<string, string>();
66
+ const state = createRuntimeState();
83
67
 
84
68
  const usageRuntime = createUsageRuntime(pi);
85
69
 
86
70
  function refreshRuntimeConfig(cwd?: string): void {
87
- runtimeConfig = loadConfig(cwd ? { cwd } : undefined).config;
71
+ state.config = loadConfig(cwd ? { cwd } : undefined).config;
88
72
  }
89
73
 
90
74
  function installFooter(ctx: ExtensionContext): void {
91
75
  if (!ctx.hasUI) return;
92
76
 
93
77
  const factory: FooterFactory = (tui, theme, footerData) => {
94
- requestRender = () => tui.requestRender?.();
95
- usageRuntime.setOnChange(requestRender);
96
- const unsubscribe = footerData.onBranchChange?.(() => tui.requestRender?.());
78
+ state.requestRender = () => tui.requestRender?.();
79
+ usageRuntime.setOnChange(state.requestRender);
80
+ const unsubscribe = footerData.onBranchChange?.(() =>
81
+ tui.requestRender?.(),
82
+ );
97
83
 
98
84
  return {
99
85
  dispose() {
100
86
  unsubscribe?.();
101
- if (requestRender === tui.requestRender) requestRender = undefined;
102
- usageRuntime.setOnChange(requestRender);
87
+ if (state.requestRender === tui.requestRender)
88
+ state.requestRender = undefined;
89
+ usageRuntime.setOnChange(state.requestRender);
103
90
  },
104
91
  invalidate() {
105
- requestRender?.();
92
+ state.requestRender?.();
106
93
  },
107
94
  render(width: number) {
108
- const activeCtx = currentCtx ?? ctx;
109
- lastGitBranch = footerData.getGitBranch();
110
- lastExtensionStatuses = new Map(footerData.getExtensionStatuses().entries());
95
+ const activeCtx = state.ctx ?? ctx;
96
+ state.gitBranch = footerData.getGitBranch();
97
+ state.extensionStatuses = new Map(
98
+ footerData.getExtensionStatuses().entries(),
99
+ );
100
+ const snapshot = buildSnapshot({
101
+ model: activeCtx.model,
102
+ cwd: activeCtx.cwd,
103
+ thinkingLevel: String(pi.getThinkingLevel()),
104
+ gitBranch: state.gitBranch,
105
+ isIdle: activeCtx.isIdle(),
106
+ hasPendingMessages: activeCtx.hasPendingMessages(),
107
+ contextUsage: activeCtx.getContextUsage(),
108
+ branch: activeCtx.sessionManager.getBranch() as unknown[],
109
+ sessionId: activeCtx.sessionManager.getSessionId(),
110
+ usageState: usageRuntime.getState(),
111
+ extensionStatuses: state.extensionStatuses,
112
+ });
111
113
  const line = buildFooterLine(
112
114
  {
113
- model: activeCtx.model,
114
- cwd: activeCtx.cwd,
115
- thinkingLevel: String(pi.getThinkingLevel()),
116
- gitBranch: lastGitBranch,
117
- runState: !activeCtx.isIdle()
118
- ? "busy"
119
- : activeCtx.hasPendingMessages()
120
- ? "queued"
121
- : "idle",
122
- contextUsage: activeCtx.getContextUsage(),
123
- branchTotals: aggregateBranchTotals(activeCtx),
124
- sessionId: activeCtx.sessionManager.getSessionId(),
125
- usageState: usageRuntime.getState(),
126
- extensionStatuses: lastExtensionStatuses,
127
- filter: runtimeConfig.filter,
128
- segments: runtimeConfig.segments,
115
+ ...snapshot,
116
+ filter: state.config.filter,
117
+ segments: state.config.segments,
129
118
  },
130
119
  theme,
131
120
  width,
@@ -143,9 +132,9 @@ export default function createExtension(pi: ExtensionAPI): void {
143
132
  }
144
133
 
145
134
  function refresh(ctx: ExtensionContext): void {
146
- currentCtx = ctx;
135
+ state.ctx = ctx;
147
136
  refreshRuntimeConfig(ctx.cwd);
148
- requestRender?.();
137
+ state.requestRender?.();
149
138
  }
150
139
 
151
140
  pi.registerCommand("statusline", {
@@ -156,39 +145,43 @@ export default function createExtension(pi: ExtensionAPI): void {
156
145
  return;
157
146
  }
158
147
 
159
- const discovered = [...lastExtensionStatuses.keys()].sort((a, b) => a.localeCompare(b));
148
+ const discovered = [...state.extensionStatuses.keys()].sort((a, b) =>
149
+ a.localeCompare(b),
150
+ );
160
151
 
161
152
  let result: PiStatusConfig | null = null;
162
153
  try {
163
154
  installEmptyFooter(ctx);
164
- result = await ctx.ui.custom<PiStatusConfig | null>((tui, theme, _keys, done) => {
165
- const activeCtx = currentCtx ?? ctx;
166
- const menuTheme: StatusLineTheme = isLiveTheme(theme) ? fromPiTheme(theme) : noTheme;
167
- return createStatusLineEditor({
168
- config: runtimeConfig,
169
- discoveredStatuses: discovered,
170
- previewInput: {
155
+ result = await ctx.ui.custom<PiStatusConfig | null>(
156
+ (tui, theme, _keys, done) => {
157
+ const activeCtx = state.ctx ?? ctx;
158
+ const menuTheme: StatusLineTheme = isLiveTheme(theme)
159
+ ? fromPiTheme(theme)
160
+ : noTheme;
161
+ const snapshot = buildSnapshot({
171
162
  model: activeCtx.model,
172
163
  cwd: activeCtx.cwd,
173
164
  thinkingLevel: String(pi.getThinkingLevel()),
174
- gitBranch: lastGitBranch,
175
- runState: !activeCtx.isIdle()
176
- ? "busy"
177
- : activeCtx.hasPendingMessages()
178
- ? "queued"
179
- : "idle",
165
+ gitBranch: state.gitBranch,
166
+ isIdle: activeCtx.isIdle(),
167
+ hasPendingMessages: activeCtx.hasPendingMessages(),
180
168
  contextUsage: activeCtx.getContextUsage(),
181
- branchTotals: aggregateBranchTotals(activeCtx),
169
+ branch: activeCtx.sessionManager.getBranch() as unknown[],
182
170
  sessionId: activeCtx.sessionManager.getSessionId(),
183
171
  usageState: usageRuntime.getState(),
184
- extensionStatuses: lastExtensionStatuses,
185
- },
186
- theme: menuTheme,
187
- done,
188
- requestRender: () => tui.requestRender?.(),
189
- usageAvailable: usageRuntime.getAvailable(),
190
- });
191
- });
172
+ extensionStatuses: state.extensionStatuses,
173
+ });
174
+ return createStatusLineEditor({
175
+ config: state.config,
176
+ discoveredStatuses: discovered,
177
+ previewInput: snapshot,
178
+ theme: menuTheme,
179
+ done,
180
+ requestRender: () => tui.requestRender?.(),
181
+ usageAvailable: usageRuntime.getAvailable(),
182
+ });
183
+ },
184
+ );
192
185
  } finally {
193
186
  installFooter(ctx);
194
187
  }
@@ -197,10 +190,13 @@ export default function createExtension(pi: ExtensionAPI): void {
197
190
 
198
191
  try {
199
192
  saveConfigToSettings(result, { cwd: ctx.cwd });
200
- runtimeConfig = result;
201
- requestRender?.();
193
+ state.config = result;
194
+ state.requestRender?.();
202
195
  } catch (error) {
203
- const message = error instanceof Error ? error.message : "Failed to save statusline settings";
196
+ const message =
197
+ error instanceof Error
198
+ ? error.message
199
+ : "Failed to save statusline settings";
204
200
  ctx.ui.notify(message, "warning");
205
201
  }
206
202
  },
@@ -209,13 +205,13 @@ export default function createExtension(pi: ExtensionAPI): void {
209
205
  pi.on("session_start", (_event, ctx) => {
210
206
  usageRuntime.requestCurrent();
211
207
  refreshRuntimeConfig(ctx.cwd);
212
- currentCtx = ctx;
208
+ state.ctx = ctx;
213
209
  installFooter(ctx);
214
210
  });
215
211
 
216
212
  pi.on("session_tree", (_event, ctx) => {
217
213
  refreshRuntimeConfig(ctx.cwd);
218
- currentCtx = ctx;
214
+ state.ctx = ctx;
219
215
  installFooter(ctx);
220
216
  });
221
217
 
@@ -228,8 +224,8 @@ export default function createExtension(pi: ExtensionAPI): void {
228
224
  });
229
225
 
230
226
  pi.on("session_shutdown", (_event, ctx) => {
231
- currentCtx = undefined;
232
- requestRender = undefined;
227
+ state.ctx = undefined;
228
+ state.requestRender = undefined;
233
229
  usageRuntime.setOnChange(undefined);
234
230
  if (ctx.hasUI) ctx.ui.setFooter(undefined);
235
231
  });
package/src/tui/render.ts CHANGED
@@ -187,7 +187,7 @@ function formatExtensionStatuses(
187
187
  return parts.join(theme.fg("dim", " | "));
188
188
  }
189
189
 
190
- function formatSegment(
190
+ export function formatSegment(
191
191
  id: StatusLineSegmentId,
192
192
  input: FooterRenderInput,
193
193
  theme: ThemeLike,