@pi-vault/pi-status 0.1.0 → 0.2.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/README.md +21 -5
- package/docs/assets/statusline-configuration.png +0 -0
- package/docs/assets/statusline-ui.png +0 -0
- package/package.json +5 -5
- package/src/{config.ts → core/config.ts} +23 -63
- package/src/core/usage-runtime.ts +62 -0
- package/src/index.ts +50 -107
- 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} +24 -81
- package/src/tui/theme.ts +35 -0
- package/src/ui/statusline-theme.ts +0 -67
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
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.
|
|
@@ -11,20 +11,36 @@ Default status line:
|
|
|
11
11
|
|
|
12
12
|
`model-with-reasoning · current-dir`
|
|
13
13
|
|
|
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`.
|
|
15
|
-
|
|
16
14
|
## Install
|
|
17
15
|
|
|
16
|
+
Install `pi-status`:
|
|
17
|
+
|
|
18
18
|
```bash
|
|
19
19
|
pi install npm:@pi-vault/pi-status
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
Optional: install `pi-usage` if you want `/usage` plus the usage-backed footer segments:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pi install npm:@pi-vault/pi-usage
|
|
26
|
+
```
|
|
27
|
+
|
|
22
28
|
Then reload Pi:
|
|
23
29
|
|
|
24
30
|
```bash
|
|
25
31
|
/reload
|
|
26
32
|
```
|
|
27
33
|
|
|
34
|
+
## Screenshots
|
|
35
|
+
|
|
36
|
+
Default status line rendering:
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
Interactive configuration editor (`/statusline`):
|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
28
44
|
## Use
|
|
29
45
|
|
|
30
46
|
Once installed, the footer updates automatically.
|
|
@@ -59,7 +75,7 @@ You can build your footer from these items:
|
|
|
59
75
|
- `weekly-limit`
|
|
60
76
|
- `extension-statuses`
|
|
61
77
|
|
|
62
|
-
`five-hour-limit` and `weekly-limit`
|
|
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.
|
|
63
79
|
|
|
64
80
|
## Common Setups
|
|
65
81
|
|
|
@@ -77,7 +93,7 @@ model · run-state · git-branch · context-used · context-remaining · session
|
|
|
77
93
|
|
|
78
94
|
## Compatibility
|
|
79
95
|
|
|
80
|
-
- Node.js `>=22.
|
|
96
|
+
- Node.js `>=22.19`
|
|
81
97
|
- Pi host environment with `@earendil-works/pi-coding-agent` and `@earendil-works/pi-tui`
|
|
82
98
|
- Tested in this repo against `@earendil-works/pi-coding-agent@0.78.x` and `@earendil-works/pi-tui@0.78.x`
|
|
83
99
|
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-vault/pi-status",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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,19 @@
|
|
|
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
41
|
"README.md"
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@pi-vault/pi-usage": "^0.
|
|
44
|
+
"@pi-vault/pi-usage": "^0.2.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -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,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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,20 +2,15 @@ import type {
|
|
|
2
2
|
ExtensionAPI,
|
|
3
3
|
ExtensionContext,
|
|
4
4
|
} from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import {
|
|
6
|
-
USAGE_CORE_READY_EVENT,
|
|
7
|
-
USAGE_CORE_REQUEST_EVENT,
|
|
8
|
-
USAGE_CORE_UPDATE_CURRENT_EVENT,
|
|
9
|
-
} from "@pi-vault/pi-usage/events";
|
|
10
|
-
import type { UsageCoreState } from "@pi-vault/pi-usage/types";
|
|
11
5
|
import {
|
|
12
6
|
loadConfig,
|
|
13
7
|
saveConfigToSettings,
|
|
14
|
-
|
|
15
|
-
} from "./
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
8
|
+
} from "./core/config.ts";
|
|
9
|
+
import { createUsageRuntime } from "./core/usage-runtime.ts";
|
|
10
|
+
import type { PiStatusConfig } from "./shared/types.ts";
|
|
11
|
+
import { createStatusLineEditor } from "./tui/editor.ts";
|
|
12
|
+
import { buildFooterLine } from "./tui/render.ts";
|
|
13
|
+
import { fromPiTheme, noTheme, type StatusLineTheme } from "./tui/theme.ts";
|
|
19
14
|
|
|
20
15
|
type FooterComponent = {
|
|
21
16
|
render: (width: number) => string[];
|
|
@@ -35,21 +30,14 @@ type FooterFactory = (
|
|
|
35
30
|
footerData: FooterDataLike,
|
|
36
31
|
) => FooterComponent;
|
|
37
32
|
|
|
38
|
-
// Used only to suppress the live footer while a custom UI (e.g. /statusline)
|
|
39
|
-
// is open. Renders no lines and is a no-op otherwise.
|
|
40
33
|
const EMPTY_FOOTER_FACTORY: FooterFactory = () => ({
|
|
41
|
-
render(
|
|
34
|
+
render(): string[] {
|
|
42
35
|
return [];
|
|
43
36
|
},
|
|
44
37
|
invalidate(): void {},
|
|
45
38
|
dispose(): void {},
|
|
46
39
|
});
|
|
47
40
|
|
|
48
|
-
// `ctx.ui.custom(...)` is expected to hand us a live Pi theme, but the
|
|
49
|
-
// runtime contract is loose and the theme may be missing in some test or
|
|
50
|
-
// boot contexts. We only adapt it when both `fg` and `bold` are callable so
|
|
51
|
-
// the editor always gets a well-formed `StatuslineMenuTheme` and never has
|
|
52
|
-
// to defend against partial themes at render time.
|
|
53
41
|
function isLiveTheme(value: unknown): boolean {
|
|
54
42
|
if (!value || typeof value !== "object") return false;
|
|
55
43
|
const candidate = value as { fg?: unknown; bold?: unknown };
|
|
@@ -66,8 +54,7 @@ function aggregateBranchTotals(ctx: ExtensionContext): {
|
|
|
66
54
|
|
|
67
55
|
for (const entry of branch ?? []) {
|
|
68
56
|
if (!entry || typeof entry !== "object") continue;
|
|
69
|
-
|
|
70
|
-
if (type !== "message") continue;
|
|
57
|
+
if ((entry as { type?: unknown }).type !== "message") continue;
|
|
71
58
|
const message = (
|
|
72
59
|
entry as {
|
|
73
60
|
message?: {
|
|
@@ -81,8 +68,7 @@ function aggregateBranchTotals(ctx: ExtensionContext): {
|
|
|
81
68
|
if (!usage) continue;
|
|
82
69
|
if (typeof usage.input === "number") totals.input += usage.input;
|
|
83
70
|
if (typeof usage.output === "number") totals.output += usage.output;
|
|
84
|
-
if (typeof usage.totalTokens === "number")
|
|
85
|
-
totals.totalTokens += usage.totalTokens;
|
|
71
|
+
if (typeof usage.totalTokens === "number") totals.totalTokens += usage.totalTokens;
|
|
86
72
|
}
|
|
87
73
|
|
|
88
74
|
return totals;
|
|
@@ -92,38 +78,28 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
92
78
|
let runtimeConfig: PiStatusConfig = loadConfig().config;
|
|
93
79
|
let currentCtx: ExtensionContext | undefined;
|
|
94
80
|
let requestRender: (() => void) | undefined;
|
|
95
|
-
let usageState: UsageCoreState | undefined;
|
|
96
81
|
let lastGitBranch: string | null = null;
|
|
97
82
|
let lastExtensionStatuses = new Map<string, string>();
|
|
98
83
|
|
|
84
|
+
const usageRuntime = createUsageRuntime(pi);
|
|
85
|
+
|
|
99
86
|
function refreshRuntimeConfig(cwd?: string): void {
|
|
100
87
|
runtimeConfig = loadConfig(cwd ? { cwd } : undefined).config;
|
|
101
88
|
}
|
|
102
89
|
|
|
103
|
-
function acceptUsageState(payload: unknown): void {
|
|
104
|
-
if (!payload || typeof payload !== "object") return;
|
|
105
|
-
const maybe = payload as { state?: unknown };
|
|
106
|
-
const next =
|
|
107
|
-
maybe.state && typeof maybe.state === "object" ? maybe.state : payload;
|
|
108
|
-
usageState = next as UsageCoreState;
|
|
109
|
-
requestRender?.();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
90
|
function installFooter(ctx: ExtensionContext): void {
|
|
113
91
|
if (!ctx.hasUI) return;
|
|
114
92
|
|
|
115
93
|
const factory: FooterFactory = (tui, theme, footerData) => {
|
|
116
94
|
requestRender = () => tui.requestRender?.();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
);
|
|
95
|
+
usageRuntime.setOnChange(requestRender);
|
|
96
|
+
const unsubscribe = footerData.onBranchChange?.(() => tui.requestRender?.());
|
|
120
97
|
|
|
121
98
|
return {
|
|
122
99
|
dispose() {
|
|
123
100
|
unsubscribe?.();
|
|
124
|
-
if (requestRender === tui.requestRender)
|
|
125
|
-
|
|
126
|
-
}
|
|
101
|
+
if (requestRender === tui.requestRender) requestRender = undefined;
|
|
102
|
+
usageRuntime.setOnChange(requestRender);
|
|
127
103
|
},
|
|
128
104
|
invalidate() {
|
|
129
105
|
requestRender?.();
|
|
@@ -131,9 +107,7 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
131
107
|
render(width: number) {
|
|
132
108
|
const activeCtx = currentCtx ?? ctx;
|
|
133
109
|
lastGitBranch = footerData.getGitBranch();
|
|
134
|
-
lastExtensionStatuses = new Map(
|
|
135
|
-
footerData.getExtensionStatuses().entries(),
|
|
136
|
-
);
|
|
110
|
+
lastExtensionStatuses = new Map(footerData.getExtensionStatuses().entries());
|
|
137
111
|
const line = buildFooterLine(
|
|
138
112
|
{
|
|
139
113
|
model: activeCtx.model,
|
|
@@ -148,7 +122,7 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
148
122
|
contextUsage: activeCtx.getContextUsage(),
|
|
149
123
|
branchTotals: aggregateBranchTotals(activeCtx),
|
|
150
124
|
sessionId: activeCtx.sessionManager.getSessionId(),
|
|
151
|
-
usageState,
|
|
125
|
+
usageState: usageRuntime.getState(),
|
|
152
126
|
extensionStatuses: lastExtensionStatuses,
|
|
153
127
|
filter: runtimeConfig.filter,
|
|
154
128
|
segments: runtimeConfig.segments,
|
|
@@ -156,7 +130,6 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
156
130
|
theme,
|
|
157
131
|
width,
|
|
158
132
|
);
|
|
159
|
-
|
|
160
133
|
return [line];
|
|
161
134
|
},
|
|
162
135
|
};
|
|
@@ -166,8 +139,7 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
166
139
|
}
|
|
167
140
|
|
|
168
141
|
function installEmptyFooter(ctx: ExtensionContext): void {
|
|
169
|
-
if (
|
|
170
|
-
ctx.ui.setFooter(EMPTY_FOOTER_FACTORY as never);
|
|
142
|
+
if (ctx.hasUI) ctx.ui.setFooter(EMPTY_FOOTER_FACTORY as never);
|
|
171
143
|
}
|
|
172
144
|
|
|
173
145
|
function refresh(ctx: ExtensionContext): void {
|
|
@@ -176,27 +148,6 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
176
148
|
requestRender?.();
|
|
177
149
|
}
|
|
178
150
|
|
|
179
|
-
const unsubscribeUsageReady = pi.events.on(
|
|
180
|
-
USAGE_CORE_READY_EVENT,
|
|
181
|
-
(payload: unknown) => {
|
|
182
|
-
acceptUsageState(payload);
|
|
183
|
-
},
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
const unsubscribeUsageUpdate = pi.events.on(
|
|
187
|
-
USAGE_CORE_UPDATE_CURRENT_EVENT,
|
|
188
|
-
(payload: unknown) => {
|
|
189
|
-
acceptUsageState(payload);
|
|
190
|
-
},
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
pi.events.emit(USAGE_CORE_REQUEST_EVENT, {
|
|
194
|
-
type: "current",
|
|
195
|
-
reply(payload: unknown) {
|
|
196
|
-
acceptUsageState(payload);
|
|
197
|
-
},
|
|
198
|
-
});
|
|
199
|
-
|
|
200
151
|
pi.registerCommand("statusline", {
|
|
201
152
|
description: "Configure statusline segments and extension-status filters",
|
|
202
153
|
handler: async (_args, ctx) => {
|
|
@@ -205,44 +156,39 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
205
156
|
return;
|
|
206
157
|
}
|
|
207
158
|
|
|
208
|
-
const discovered = [...lastExtensionStatuses.keys()].sort((a, b) =>
|
|
209
|
-
a.localeCompare(b),
|
|
210
|
-
);
|
|
159
|
+
const discovered = [...lastExtensionStatuses.keys()].sort((a, b) => a.localeCompare(b));
|
|
211
160
|
|
|
212
161
|
let result: PiStatusConfig | null = null;
|
|
213
162
|
try {
|
|
214
163
|
installEmptyFooter(ctx);
|
|
215
|
-
result = await ctx.ui.custom<PiStatusConfig | null>(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
});
|
|
244
|
-
},
|
|
245
|
-
);
|
|
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: {
|
|
171
|
+
model: activeCtx.model,
|
|
172
|
+
cwd: activeCtx.cwd,
|
|
173
|
+
thinkingLevel: String(pi.getThinkingLevel()),
|
|
174
|
+
gitBranch: lastGitBranch,
|
|
175
|
+
runState: !activeCtx.isIdle()
|
|
176
|
+
? "busy"
|
|
177
|
+
: activeCtx.hasPendingMessages()
|
|
178
|
+
? "queued"
|
|
179
|
+
: "idle",
|
|
180
|
+
contextUsage: activeCtx.getContextUsage(),
|
|
181
|
+
branchTotals: aggregateBranchTotals(activeCtx),
|
|
182
|
+
sessionId: activeCtx.sessionManager.getSessionId(),
|
|
183
|
+
usageState: usageRuntime.getState(),
|
|
184
|
+
extensionStatuses: lastExtensionStatuses,
|
|
185
|
+
},
|
|
186
|
+
theme: menuTheme,
|
|
187
|
+
done,
|
|
188
|
+
requestRender: () => tui.requestRender?.(),
|
|
189
|
+
usageAvailable: usageRuntime.getAvailable(),
|
|
190
|
+
});
|
|
191
|
+
});
|
|
246
192
|
} finally {
|
|
247
193
|
installFooter(ctx);
|
|
248
194
|
}
|
|
@@ -254,16 +200,14 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
254
200
|
runtimeConfig = result;
|
|
255
201
|
requestRender?.();
|
|
256
202
|
} catch (error) {
|
|
257
|
-
const message =
|
|
258
|
-
error instanceof Error
|
|
259
|
-
? error.message
|
|
260
|
-
: "Failed to save statusline settings";
|
|
203
|
+
const message = error instanceof Error ? error.message : "Failed to save statusline settings";
|
|
261
204
|
ctx.ui.notify(message, "warning");
|
|
262
205
|
}
|
|
263
206
|
},
|
|
264
207
|
});
|
|
265
208
|
|
|
266
209
|
pi.on("session_start", (_event, ctx) => {
|
|
210
|
+
usageRuntime.requestCurrent();
|
|
267
211
|
refreshRuntimeConfig(ctx.cwd);
|
|
268
212
|
currentCtx = ctx;
|
|
269
213
|
installFooter(ctx);
|
|
@@ -286,8 +230,7 @@ export default function createExtension(pi: ExtensionAPI): void {
|
|
|
286
230
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
287
231
|
currentCtx = undefined;
|
|
288
232
|
requestRender = undefined;
|
|
289
|
-
|
|
290
|
-
unsubscribeUsageUpdate();
|
|
233
|
+
usageRuntime.setOnChange(undefined);
|
|
291
234
|
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
292
235
|
});
|
|
293
236
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export type StatusLineSegmentId =
|
|
2
|
+
| "model"
|
|
3
|
+
| "model-with-reasoning"
|
|
4
|
+
| "project-name"
|
|
5
|
+
| "current-dir"
|
|
6
|
+
| "git-branch"
|
|
7
|
+
| "run-state"
|
|
8
|
+
| "context-remaining"
|
|
9
|
+
| "context-used"
|
|
10
|
+
| "context-window-size"
|
|
11
|
+
| "used-tokens"
|
|
12
|
+
| "total-input-tokens"
|
|
13
|
+
| "total-output-tokens"
|
|
14
|
+
| "session-id"
|
|
15
|
+
| "five-hour-limit"
|
|
16
|
+
| "weekly-limit"
|
|
17
|
+
| "extension-statuses";
|
|
18
|
+
|
|
19
|
+
export type StatusFilter =
|
|
20
|
+
| { mode: "all"; hidden: string[] }
|
|
21
|
+
| { mode: "only"; shown: string[] };
|
|
22
|
+
|
|
23
|
+
export type PiStatusConfig = {
|
|
24
|
+
segments: StatusLineSegmentId[];
|
|
25
|
+
filter: StatusFilter;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const KNOWN_SEGMENTS: readonly StatusLineSegmentId[] = [
|
|
29
|
+
"model",
|
|
30
|
+
"model-with-reasoning",
|
|
31
|
+
"project-name",
|
|
32
|
+
"current-dir",
|
|
33
|
+
"git-branch",
|
|
34
|
+
"run-state",
|
|
35
|
+
"context-remaining",
|
|
36
|
+
"context-used",
|
|
37
|
+
"context-window-size",
|
|
38
|
+
"used-tokens",
|
|
39
|
+
"total-input-tokens",
|
|
40
|
+
"total-output-tokens",
|
|
41
|
+
"session-id",
|
|
42
|
+
"five-hour-limit",
|
|
43
|
+
"weekly-limit",
|
|
44
|
+
"extension-statuses",
|
|
45
|
+
] as const;
|
|
46
|
+
|
|
47
|
+
export const DEFAULT_SEGMENTS: readonly StatusLineSegmentId[] = [
|
|
48
|
+
"model-with-reasoning",
|
|
49
|
+
"current-dir",
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
export const USAGE_SEGMENTS = new Set<StatusLineSegmentId>([
|
|
53
|
+
"five-hour-limit",
|
|
54
|
+
"weekly-limit",
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export function isKnownSegment(value: string): value is StatusLineSegmentId {
|
|
58
|
+
return (KNOWN_SEGMENTS as readonly string[]).includes(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function isUsageSegment(id: StatusLineSegmentId): boolean {
|
|
62
|
+
return USAGE_SEGMENTS.has(id);
|
|
63
|
+
}
|