@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 +44 -0
- package/README.md +54 -20
- package/docs/assets/statusline-configuration.png +0 -0
- package/docs/assets/statusline-ui.png +0 -0
- package/package.json +8 -7
- package/src/{config.ts → core/config.ts} +23 -63
- package/src/core/snapshot.ts +75 -0
- package/src/core/usage-runtime.ts +62 -0
- package/src/index.ts +89 -150
- package/src/shared/types.ts +63 -0
- package/src/{ui/statusline-editor.ts → tui/editor.ts} +91 -114
- package/src/{render.ts → tui/render.ts} +25 -82
- package/src/tui/theme.ts +35 -0
- package/src/ui/statusline-theme.ts +0 -67
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
|
[](https://www.npmjs.com/package/@pi-vault/pi-status)
|
|
4
4
|
[](https://github.com/pi-vault/pi-status/actions/workflows/quality.yml)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Replace Pi's default footer with a
|
|
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
|
|
10
|
+
Default footer:
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
```text
|
|
13
|
+
model-with-reasoning · current-dir
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Screenshots
|
|
17
|
+
|
|
18
|
+
Default status line rendering:
|
|
19
|
+
|
|
20
|
+

|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
Interactive configuration editor (`/statusline`):
|
|
15
23
|
|
|
16
|
-
|
|
24
|
+

|
|
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
|
-
|
|
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
|
-
|
|
50
|
+
The editor lets you:
|
|
33
51
|
|
|
34
52
|
- turn footer items on or off
|
|
35
|
-
- reorder
|
|
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
|
-
|
|
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
|
|
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`
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
119
|
+
## Development
|
|
85
120
|
|
|
86
121
|
```bash
|
|
87
122
|
pnpm install
|
|
88
123
|
pnpm check
|
|
89
|
-
pnpm pack
|
|
90
|
-
pi -e .
|
|
124
|
+
pnpm run pack:dry-run
|
|
91
125
|
```
|
|
92
126
|
|
|
93
127
|
## License
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-vault/pi-status",
|
|
3
|
-
"version": "0.1
|
|
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.
|
|
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.
|
|
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.
|
|
53
|
-
"@earendil-works/pi-tui": "^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 "
|
|
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 (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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
|
|
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
|
+
}
|