@pi-vault/pi-status 0.1.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
@@ -2,45 +2,66 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40pi-vault%2Fpi-status)](https://www.npmjs.com/package/@pi-vault/pi-status)
4
4
  [![Quality](https://github.com/pi-vault/pi-status/actions/workflows/quality.yml/badge.svg?branch=master)](https://github.com/pi-vault/pi-status/actions/workflows/quality.yml)
5
- [![Node >= 22.12](https://img.shields.io/badge/node-%3E%3D22.12-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
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
+ ```
15
+
16
+ ## Screenshots
17
+
18
+ Default status line rendering:
19
+
20
+ ![Status line UI](docs/assets/statusline-ui.png)
13
21
 
14
- > **Status:** Early-adopter release (`v0.1.0`). It is ready to use, but the available segments and configuration details may still change before `v1.0`.
22
+ Interactive configuration editor (`/statusline`):
15
23
 
16
- ## Install
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
- Then reload Pi:
34
+ Optional: install `pi-usage` if you want the `five-hour-limit` and `weekly-limit` footer segments:
35
+
36
+ ```bash
37
+ pi install npm:@pi-vault/pi-usage
38
+ ```
39
+
40
+ Reload Pi after installing or upgrading:
23
41
 
24
42
  ```bash
25
43
  /reload
26
44
  ```
27
45
 
28
- ## Use
46
+ ## Use `/statusline`
29
47
 
30
- Once installed, the footer updates automatically.
48
+ Once installed, the footer updates automatically. Run `/statusline` inside Pi to open the interactive editor.
31
49
 
32
- Use `/statusline` inside Pi to:
50
+ The editor lets you:
33
51
 
34
52
  - turn footer items on or off
35
- - reorder the items you want to see
53
+ - reorder enabled items with `Left` and `Right`
54
+ - search the segment list
36
55
  - preview the result before saving
37
56
  - control which extension status messages are shown
38
57
 
39
58
  Changes are saved and reused the next time Pi starts.
40
59
 
41
- ## Available Status Items
60
+ During editing, the live footer is temporarily hidden so the inline UI can use the full width cleanly.
61
+
62
+ ## Available Footer Items
42
63
 
43
- You can build your footer from these items:
64
+ You can compose the footer from these segment IDs:
44
65
 
45
66
  - `model`
46
67
  - `model-with-reasoning`
@@ -59,9 +80,11 @@ You can build your footer from these items:
59
80
  - `weekly-limit`
60
81
  - `extension-statuses`
61
82
 
62
- `five-hour-limit` and `weekly-limit` come from [`@pi-vault/pi-usage`](https://www.npmjs.com/package/@pi-vault/pi-usage) and only appear when that usage data is available.
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.
63
86
 
64
- ## Common Setups
87
+ ## Common Examples
65
88
 
66
89
  Keep it minimal:
67
90
 
@@ -75,19 +98,30 @@ Show more session detail:
75
98
  model · run-state · git-branch · context-used · context-remaining · session-id
76
99
  ```
77
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
+
78
113
  ## Compatibility
79
114
 
80
- - Node.js `>=22.12`
115
+ - Node.js `>=22.19`
81
116
  - Pi host environment with `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`
82
- - 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`
83
118
 
84
- ## Development Setup
119
+ ## Development
85
120
 
86
121
  ```bash
87
122
  pnpm install
88
123
  pnpm check
89
- pnpm pack --dry-run
90
- pi -e .
124
+ pnpm run pack:dry-run
91
125
  ```
92
126
 
93
127
  ## License
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-vault/pi-status",
3
- "version": "0.1.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>",
@@ -29,19 +29,20 @@
29
29
  },
30
30
  "pi": {
31
31
  "extensions": [
32
- "./src/index.ts",
33
- "node_modules/@pi-vault/pi-usage/src/index.ts"
32
+ "./src/index.ts"
34
33
  ]
35
34
  },
36
35
  "engines": {
37
- "node": ">=22.12"
36
+ "node": ">=22.19"
38
37
  },
39
38
  "files": [
40
39
  "src",
40
+ "docs/assets",
41
+ "CHANGELOG.md",
41
42
  "README.md"
42
43
  ],
43
44
  "dependencies": {
44
- "@pi-vault/pi-usage": "^0.1.1"
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"
@@ -11,14 +11,11 @@ import { homedir } from "node:os";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  import {
13
13
  DEFAULT_SEGMENTS,
14
+ isKnownSegment,
15
+ type PiStatusConfig,
14
16
  type StatusFilter,
15
17
  type StatusLineSegmentId,
16
- } from "./render.ts";
17
-
18
- export type PiStatusConfig = {
19
- segments: StatusLineSegmentId[];
20
- filter: StatusFilter;
21
- };
18
+ } from "../shared/types.ts";
22
19
 
23
20
  export type ConfigLoadResult = {
24
21
  config: PiStatusConfig;
@@ -30,25 +27,6 @@ export const DEFAULT_CONFIG: PiStatusConfig = {
30
27
  filter: { mode: "all", hidden: [] },
31
28
  };
32
29
 
33
- const KNOWN_SEGMENTS = new Set<StatusLineSegmentId>([
34
- "model",
35
- "model-with-reasoning",
36
- "project-name",
37
- "current-dir",
38
- "git-branch",
39
- "run-state",
40
- "context-remaining",
41
- "context-used",
42
- "context-window-size",
43
- "used-tokens",
44
- "total-input-tokens",
45
- "total-output-tokens",
46
- "session-id",
47
- "five-hour-limit",
48
- "weekly-limit",
49
- "extension-statuses",
50
- ]);
51
-
52
30
  function cloneDefaultConfig(): PiStatusConfig {
53
31
  return {
54
32
  segments: [...DEFAULT_CONFIG.segments],
@@ -76,11 +54,10 @@ export function normalizeSegments(input: unknown): StatusLineSegmentId[] {
76
54
 
77
55
  for (const value of input) {
78
56
  if (typeof value !== "string") continue;
79
- if (!KNOWN_SEGMENTS.has(value as StatusLineSegmentId)) continue;
80
- const id = value as StatusLineSegmentId;
81
- if (seen.has(id)) continue;
82
- seen.add(id);
83
- out.push(id);
57
+ if (!isKnownSegment(value)) continue;
58
+ if (seen.has(value)) continue;
59
+ seen.add(value);
60
+ out.push(value);
84
61
  }
85
62
 
86
63
  return out;
@@ -103,8 +80,9 @@ function normalizeFilterValues(input: unknown): string[] {
103
80
  }
104
81
 
105
82
  export function normalizeStatusFilter(input: unknown): StatusFilter {
106
- if (!input || typeof input !== "object" || Array.isArray(input))
83
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
107
84
  return { mode: "all", hidden: [] };
85
+ }
108
86
  const mode = (input as { mode?: unknown }).mode;
109
87
 
110
88
  if (mode === "all") {
@@ -127,8 +105,9 @@ export function normalizeStatusFilter(input: unknown): StatusFilter {
127
105
  function readJsonObject(path: string): Record<string, unknown> | null {
128
106
  try {
129
107
  const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
130
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
108
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
131
109
  return null;
110
+ }
132
111
  return parsed as Record<string, unknown>;
133
112
  } catch {
134
113
  return null;
@@ -141,24 +120,17 @@ type SettingsFileState =
141
120
  | { exists: true; malformed: true };
142
121
 
143
122
  function readSettingsFileState(path: string): SettingsFileState {
144
- if (!existsSync(path)) {
145
- return { exists: false, value: {} };
146
- }
147
-
123
+ if (!existsSync(path)) return { exists: false, value: {} };
148
124
  const parsed = readJsonObject(path);
149
- if (parsed) {
150
- return { exists: true, value: parsed };
151
- }
152
-
125
+ if (parsed) return { exists: true, value: parsed };
153
126
  return { exists: true, malformed: true };
154
127
  }
155
128
 
156
129
  function normalizePiStatus(input: unknown): PiStatusConfig {
157
- if (!input || typeof input !== "object" || Array.isArray(input))
130
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
158
131
  return cloneDefaultConfig();
159
- const segments = normalizeSegments(
160
- (input as { segments?: unknown }).segments,
161
- );
132
+ }
133
+ const segments = normalizeSegments((input as { segments?: unknown }).segments);
162
134
  const filter = normalizeStatusFilter((input as { filter?: unknown }).filter);
163
135
  return {
164
136
  segments: segments.length > 0 ? segments : [...DEFAULT_SEGMENTS],
@@ -167,18 +139,12 @@ function normalizePiStatus(input: unknown): PiStatusConfig {
167
139
  }
168
140
 
169
141
  function mergePiStatus(globalValue: unknown, projectValue: unknown): unknown {
170
- if (
171
- !globalValue ||
172
- typeof globalValue !== "object" ||
173
- Array.isArray(globalValue)
174
- )
142
+ if (!globalValue || typeof globalValue !== "object" || Array.isArray(globalValue)) {
175
143
  return projectValue ?? globalValue;
176
- if (
177
- !projectValue ||
178
- typeof projectValue !== "object" ||
179
- Array.isArray(projectValue)
180
- )
144
+ }
145
+ if (!projectValue || typeof projectValue !== "object" || Array.isArray(projectValue)) {
181
146
  return globalValue;
147
+ }
182
148
  const g = globalValue as Record<string, unknown>;
183
149
  const p = projectValue as Record<string, unknown>;
184
150
  const merged: Record<string, unknown> = { ...g, ...p };
@@ -186,12 +152,8 @@ function mergePiStatus(globalValue: unknown, projectValue: unknown): unknown {
186
152
  const gFilter = g.filter;
187
153
  const pFilter = p.filter;
188
154
  if (
189
- gFilter &&
190
- typeof gFilter === "object" &&
191
- !Array.isArray(gFilter) &&
192
- pFilter &&
193
- typeof pFilter === "object" &&
194
- !Array.isArray(pFilter)
155
+ gFilter && typeof gFilter === "object" && !Array.isArray(gFilter) &&
156
+ pFilter && typeof pFilter === "object" && !Array.isArray(pFilter)
195
157
  ) {
196
158
  merged.filter = {
197
159
  ...(gFilter as Record<string, unknown>),
@@ -240,9 +202,7 @@ export function saveConfigToSettings(
240
202
 
241
203
  const targetState = readSettingsFileState(path);
242
204
  if ("malformed" in targetState) {
243
- throw new Error(
244
- `Refusing to write malformed or non-object settings file: ${path}`,
245
- );
205
+ throw new Error(`Refusing to write malformed or non-object settings file: ${path}`);
246
206
  }
247
207
 
248
208
  const base = targetState.value;
@@ -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
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ USAGE_CORE_READY_EVENT,
3
+ USAGE_CORE_REQUEST_EVENT,
4
+ USAGE_CORE_UPDATE_CURRENT_EVENT,
5
+ } from "@pi-vault/pi-usage/events";
6
+ import type { UsageCoreState } from "@pi-vault/pi-usage/types";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+
9
+ function isUsageCoreState(value: unknown): value is UsageCoreState {
10
+ return Boolean(value && typeof value === "object");
11
+ }
12
+
13
+ export function createUsageRuntime(pi: ExtensionAPI) {
14
+ let available = false;
15
+ let state: UsageCoreState | undefined;
16
+ let onChange: (() => void) | undefined;
17
+
18
+ const acceptPayload = (payload: unknown): void => {
19
+ if (!payload || typeof payload !== "object") return;
20
+ const maybe = payload as { state?: unknown };
21
+ const next = maybe.state ?? payload;
22
+ if (!isUsageCoreState(next)) return;
23
+ state = next;
24
+ available = true;
25
+ onChange?.();
26
+ };
27
+
28
+ const requestCurrent = (): void => {
29
+ pi.events.emit(USAGE_CORE_REQUEST_EVENT, {
30
+ type: "current",
31
+ reply(payload: unknown) {
32
+ acceptPayload(payload);
33
+ },
34
+ });
35
+ };
36
+
37
+ const unsubscribeReady = pi.events.on(USAGE_CORE_READY_EVENT, acceptPayload);
38
+ const unsubscribeUpdate = pi.events.on(
39
+ USAGE_CORE_UPDATE_CURRENT_EVENT,
40
+ acceptPayload,
41
+ );
42
+
43
+ requestCurrent();
44
+
45
+ return {
46
+ getAvailable(): boolean {
47
+ return available;
48
+ },
49
+ getState(): UsageCoreState | undefined {
50
+ return state;
51
+ },
52
+ setOnChange(listener: (() => void) | undefined): void {
53
+ onChange = listener;
54
+ },
55
+ requestCurrent,
56
+ dispose(): void {
57
+ onChange = undefined;
58
+ unsubscribeReady();
59
+ unsubscribeUpdate();
60
+ },
61
+ };
62
+ }