@erichll/pi-statusline 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/CHANGELOG.md +17 -0
- package/README.md +160 -26
- package/examples/statusline.json +63 -11
- package/package.json +1 -1
- package/src/compaction.ts +32 -0
- package/src/config.ts +9 -14
- package/src/index.ts +24 -8
- package/src/panel.ts +28 -13
- package/src/render.ts +13 -6
- package/src/runtime-render.ts +62 -0
- package/src/runtime.ts +130 -0
- package/src/session.ts +34 -5
- package/src/types.ts +1 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 - 2026-10-04
|
|
4
|
+
|
|
5
|
+
- Use the same muted color for No content and Avg; default modelDisplay to name while preserving explicit last preferences.
|
|
6
|
+
- Remove Minimal and the preset selector; support only Powerline, without migration for retired preset values.
|
|
7
|
+
- Make Powerline the default with Nerd icons, Powerline separators, model display name, basename paths, context bar and total tokens enabled; ship the layout as the example configuration.
|
|
8
|
+
- Omit the startup/idle footer label and its separator.
|
|
9
|
+
- Lead the second row with one Elapsed duration; reuse explicit `tps` Elapsed display text without an extra Running/Done duration.
|
|
10
|
+
- Track monotonic runs through retries, tools, UI waits and compaction until `agent_settled`; retain the final run's footer summary.
|
|
11
|
+
- Add ID-based concurrent/nested tool activity, UI-wait precedence and configurable non-empty-content observation warnings (ten seconds by default).
|
|
12
|
+
- Add assistant-only per-run usage, whole-run average output rate and cache-read ratio without changing cumulative session totals.
|
|
13
|
+
- Add compaction activity/result and whole-session successful compaction counts.
|
|
14
|
+
- Remove the Until compact metric, its runtime setting and compaction-capacity calculation.
|
|
15
|
+
- Add runtime settings with draft preview, numeric validation and backward-compatible version-1 partial configuration.
|
|
16
|
+
- Keep statistics limited to existing provider statuses and Pi public events/usage; do not expose unsupported request-level metrics or require provider changes. Ignore and drop obsolete experimental request/integration configuration on save.
|
|
17
|
+
- Remove the read-only run details command/panel; retain footer statistics, configuration and live preview.
|
|
18
|
+
- Expand lifecycle/configuration/render tests and isolated offline PTY smoke across regular/fullscreen layouts.
|
|
19
|
+
|
|
3
20
|
## 0.1.0 - 2026-10-03
|
|
4
21
|
|
|
5
22
|
- Make bar mode the default and use whole cells to avoid visual gaps from partial glyphs; non-zero usage fills at least one cell.
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @erichll/pi-statusline
|
|
2
2
|
|
|
3
|
-
A configurable footer for Pi 1.0 with Git status, session usage, themes, and live configuration preview. Uses Pi's native footer and leaves the editor intact.
|
|
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.
|
|
4
4
|
|
|
5
5
|
```text
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Example model(high) [█░░░░░░░░░]11.9% pi-packages main ● *2 ↑2 49.4k
|
|
7
|
+
Elapsed 5m 42s · Avg 45 tok/s · Cache 86% · Turns 3 · Compactions 1
|
|
8
8
|
```
|
|
9
9
|
|
|
10
10
|
## Install
|
|
@@ -13,7 +13,9 @@ extension status text, when available
|
|
|
13
13
|
pi install npm:@erichll/pi-statusline
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Requires Node.js >=22.19, Pi >=1.0.0,
|
|
16
|
+
Requires Node.js >=22.19, Pi >=1.0.0, a Nerd Font for the default Powerline layout,
|
|
17
|
+
and optionally Git. Switch icons/separators in the panel for a font-independent
|
|
18
|
+
layout. The package has no
|
|
17
19
|
other runtime dependencies and makes no network requests or credential lookups.
|
|
18
20
|
Only one extension should own Pi's footer, so disable any other footer
|
|
19
21
|
extension in `pi config` before enabling this one.
|
|
@@ -49,6 +51,8 @@ published package, `npm:@erichll/pi-statusline`.
|
|
|
49
51
|
The panel supports arrow navigation, Space to toggle a segment, Alt+Up/Down to
|
|
50
52
|
reorder it, and Enter to edit its properties. Choose **Save and apply globally**
|
|
51
53
|
to persist the draft. Esc backs out of a property editor or cancels the panel.
|
|
54
|
+
**Runtime options** edits the same draft; Space toggles booleans and Enter edits
|
|
55
|
+
the no-content threshold.
|
|
52
56
|
**Restore defaults in preview** changes only the draft until saved.
|
|
53
57
|
Preview uses the real footer renderer and current session data, or clearly
|
|
54
58
|
labelled example data for an empty session. A save error leaves the active
|
|
@@ -69,32 +73,93 @@ existing file to the new path before running `/reload`; the old path is no longe
|
|
|
69
73
|
{
|
|
70
74
|
"version": 1,
|
|
71
75
|
"enabled": true,
|
|
72
|
-
"preset": "
|
|
73
|
-
"icons": "
|
|
74
|
-
"separator": "
|
|
76
|
+
"preset": "powerline",
|
|
77
|
+
"icons": "nerd",
|
|
78
|
+
"separator": "powerline",
|
|
75
79
|
"pathMode": "basename",
|
|
76
80
|
"modelDisplay": "name",
|
|
77
|
-
"contextDisplay": "bar"
|
|
81
|
+
"contextDisplay": "bar",
|
|
82
|
+
"segments": [
|
|
83
|
+
{
|
|
84
|
+
"id": "model",
|
|
85
|
+
"enabled": true
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "thinking",
|
|
89
|
+
"enabled": true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"id": "context",
|
|
93
|
+
"enabled": true
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "directory",
|
|
97
|
+
"enabled": true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
"id": "git",
|
|
101
|
+
"enabled": true
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "cost",
|
|
105
|
+
"enabled": false
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"id": "input",
|
|
109
|
+
"enabled": false
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "output",
|
|
113
|
+
"enabled": false
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "tokens",
|
|
117
|
+
"enabled": true
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "cacheRead",
|
|
121
|
+
"enabled": false
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "cacheWrite",
|
|
125
|
+
"enabled": false
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "statuses",
|
|
129
|
+
"enabled": true
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"runtime": {
|
|
133
|
+
"enabled": true,
|
|
134
|
+
"status": true,
|
|
135
|
+
"tools": true,
|
|
136
|
+
"noContent": true,
|
|
137
|
+
"noContentSeconds": 10,
|
|
138
|
+
"avg": true,
|
|
139
|
+
"cache": true,
|
|
140
|
+
"counts": true,
|
|
141
|
+
"retainSummary": true
|
|
142
|
+
}
|
|
78
143
|
}
|
|
79
144
|
```
|
|
80
145
|
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
supplies semantic colors; Powerline backgrounds
|
|
84
|
-
|
|
146
|
+
- Layout: `powerline` is the only preset; there is no preset selector.
|
|
147
|
+
Customize segments and styles directly, or restore defaults in preview.
|
|
148
|
+
Pi's current light/dark theme supplies semantic colors; Powerline backgrounds
|
|
149
|
+
adapt to its appearance.
|
|
150
|
+
- Icons: `nerd` (default; requires a Nerd Font), `unicode`, `ascii`. ASCII mode
|
|
85
151
|
replaces built-in decorative glyphs, not user-provided names or custom icons.
|
|
86
152
|
Both graphical modes use `` for the model by default; this glyph requires a
|
|
87
153
|
Nerd Font. A custom model icon still overrides the default.
|
|
88
|
-
- Separators: `pipe`, `dot`, `space
|
|
154
|
+
- Separators: `powerline` (default), `pipe`, `dot`, `space`. ASCII mode uses plain separators.
|
|
89
155
|
- Paths: `basename`, `abbreviated`, `full` (home directory abbreviated as `~`).
|
|
90
|
-
- Model display: `name` (default
|
|
91
|
-
|
|
92
|
-
display name is configured
|
|
93
|
-
|
|
94
|
-
Switching presets preserves this preference; restoring defaults resets it.
|
|
156
|
+
- Model display: `name` (default) uses the display name with full ID fallback.
|
|
157
|
+
Choose `last` for the final non-empty `/`-separated component of the **model ID**,
|
|
158
|
+
even when a display name is configured. For example, `provider/a/b/c/d` becomes `d`.
|
|
159
|
+
Restoring defaults resets this preference to `name`.
|
|
95
160
|
- Context display: `bar` (default, `[██░░░░░░░░]20%`) or `text`
|
|
96
161
|
(`12.7k/272k(4.7%)`). Choose **Context display: bar** in the panel or set
|
|
97
|
-
`"contextDisplay": "bar"`.
|
|
162
|
+
`"contextDisplay": "bar"`.
|
|
98
163
|
The ten-cell bar uses whole blocks to avoid gaps inside partial glyphs. Fill
|
|
99
164
|
rounds to the nearest cell, with at least one filled cell for non-zero usage;
|
|
100
165
|
the percentage retains the more precise value. ASCII mode uses `#` and `-`.
|
|
@@ -133,15 +198,77 @@ Available segments:
|
|
|
133
198
|
| `directory` | Current working directory | On |
|
|
134
199
|
| `git` | Branch, clean/dirty/conflict, staged `+`, unstaged `*`, untracked `?`, ahead/behind | On |
|
|
135
200
|
| `cost` | Pi-recorded cumulative USD cost; enable in the panel when wanted | Off |
|
|
136
|
-
| `
|
|
201
|
+
| `tokens` | Cumulative total tokens | On |
|
|
202
|
+
| `input`, `output` | Cumulative input or output tokens | Off |
|
|
137
203
|
| `cacheRead`, `cacheWrite` | Cumulative cache token counts | Off |
|
|
138
|
-
| `statuses` | Other extensions' `setStatus()` text, with terminal controls stripped | On |
|
|
204
|
+
| `statuses` | Other extensions' `setStatus()` text on the runtime row, with terminal controls stripped | On |
|
|
139
205
|
|
|
140
|
-
The
|
|
206
|
+
The default Powerline preset enables model/thinking, context, directory, Git, total
|
|
207
|
+
tokens and extension statuses. Cost, input/output and cache token counters stay off.
|
|
141
208
|
See [examples/statusline.json](examples/statusline.json) for an explicit layout.
|
|
142
|
-
Unknown versions, duplicate segments, invalid colors and malformed JSON are
|
|
209
|
+
Unknown presets or versions, duplicate segments, invalid colors and malformed JSON are
|
|
143
210
|
rejected with a notification; reload retains the last working configuration.
|
|
144
211
|
Removing the file restores defaults on reload. Saves use an atomic file replacement.
|
|
212
|
+
Obsolete `runtime.untilCompact`, `providerMetrics` and `integrations` keys are
|
|
213
|
+
ignored and dropped on the next save; they do not require a configuration migration.
|
|
214
|
+
|
|
215
|
+
## Runtime row and compaction
|
|
216
|
+
|
|
217
|
+
Runtime metrics use Pi's public events and reported usage, without modifying the
|
|
218
|
+
provider. `Elapsed`
|
|
219
|
+
leads the second row; no additional `Running 342s` or `Done 342s` duration is
|
|
220
|
+
shown. A run starts at the first `agent_start` and ends at `agent_settled`, not
|
|
221
|
+
`agent_end`. Retries, tools, UI waits and compaction do not reset its monotonic
|
|
222
|
+
clock. `retainSummary` keeps the last run's footer summary until the next run
|
|
223
|
+
(disable it to hide the run summary at settlement). The idle
|
|
224
|
+
footer has no Idle label or leading separator.
|
|
225
|
+
|
|
226
|
+
- `Turns` counts Pi `turn_start` events, not upstream requests. Internal retries
|
|
227
|
+
may make more than one request in a turn; no request count is inferred from
|
|
228
|
+
turn events. Compaction does not increase the model-turn count.
|
|
229
|
+
- Tool activity is keyed by tool-call ID, including concurrent/nested calls and
|
|
230
|
+
updates. It shows `Tool bash 26s` or `Tools 2 active`; tools cannot decide the
|
|
231
|
+
model run's outcome. Pi UI prompts take precedence with `Awaiting input`.
|
|
232
|
+
- `No content 10s` means **no non-empty text, reasoning or tool-call delta has
|
|
233
|
+
been observed** for ten seconds. It uses the same muted color as Avg.
|
|
234
|
+
It is not a server timeout or request phase.
|
|
235
|
+
Tools, compaction and UI waits suppress it. The integer threshold accepts
|
|
236
|
+
1–3600 seconds. Empty deltas and usage-only updates do not reset it.
|
|
237
|
+
- Per-run usage includes only actual assistant messages. Streaming counters are
|
|
238
|
+
replaced, not repeatedly added. `Avg = output tokens / total run seconds`,
|
|
239
|
+
including tools, UI waits, retries and compaction; it is not inference speed.
|
|
240
|
+
`Cache = cacheRead / (input + cacheRead + cacheWrite)`. Missing usage/invalid
|
|
241
|
+
denominators are unavailable, not made-up zeroes. A genuinely reported zero
|
|
242
|
+
remains zero. These values never replace cumulative session totals.
|
|
243
|
+
- `Compacting… 4s` suppresses rate/cache noise while
|
|
244
|
+
compaction is active. Success restores the base row; failure/cancellation can
|
|
245
|
+
show `Compaction failed` / `Compaction cancelled` while no model run is active,
|
|
246
|
+
until subsequent activity.
|
|
247
|
+
- `Compactions` counts unique successful entries across the **whole session file**,
|
|
248
|
+
including other branches, not merely the selected tree path.
|
|
249
|
+
`runtime.enabled: false` hides the statusline's runtime metrics. Individual
|
|
250
|
+
options hide their corresponding items; `status` controls the local Elapsed
|
|
251
|
+
and compaction-result labels. The `statuses` segment independently controls other
|
|
252
|
+
extensions' text. If its `tps` entry is precisely a plain `Elapsed …` duration,
|
|
253
|
+
that display text is moved to the front and replaces the local Elapsed item.
|
|
254
|
+
All other statuses remain opaque text: they are never parsed into counters or
|
|
255
|
+
provider metrics. The provider's Elapsed may use its own pause
|
|
256
|
+
semantics; Avg always uses the independent monotonic run clock.
|
|
257
|
+
|
|
258
|
+
Runtime summaries are in-memory since extension load; reload/new session resets
|
|
259
|
+
them, unlike persisted usage/compaction totals.
|
|
260
|
+
|
|
261
|
+
## Existing provider statuses
|
|
262
|
+
|
|
263
|
+
The CLIProxyAPI provider already publishes an Elapsed footer status through
|
|
264
|
+
`setStatus("tps", ...)` and a TPS/token-summary notification at settlement. The
|
|
265
|
+
statusline can reuse the explicit Elapsed text; it does not parse the notification.
|
|
266
|
+
Its own Avg and Cache come from Pi-reported assistant usage and the local run clock.
|
|
267
|
+
|
|
268
|
+
No provider changes, adapter handshake or private network instrumentation are
|
|
269
|
+
required. Request counts, first-text/reasoning request latency, per-request duration,
|
|
270
|
+
transport reporting and retry associations are not exposed by this package. The
|
|
271
|
+
provider may implement transports/retries internally without exposing these metrics.
|
|
145
272
|
|
|
146
273
|
## Statistics and refresh behavior
|
|
147
274
|
|
|
@@ -171,7 +298,9 @@ Rendering performs no file reads, Git queries or full-session scans. Streaming
|
|
|
171
298
|
refresh requests are coalesced to 100 ms. Narrow terminals shorten model/path
|
|
172
299
|
labels, then hide cost, cache/token counters, the thinking suffix, Git and directory in
|
|
173
300
|
that order, retaining model/context where possible. The secondary status row
|
|
174
|
-
|
|
301
|
+
drops extension text and cache/rate first, preserving
|
|
302
|
+
leading Elapsed and compaction status where possible, then truncates by terminal
|
|
303
|
+
cell width. Reload, session replacement, disabling and shutdown
|
|
175
304
|
release timers and subscriptions. RPC, JSON and print mode do not install UI
|
|
176
305
|
components or start polling.
|
|
177
306
|
|
|
@@ -184,8 +313,13 @@ npm test --workspace @erichll/pi-statusline
|
|
|
184
313
|
npm run smoke --workspace @erichll/pi-statusline
|
|
185
314
|
```
|
|
186
315
|
|
|
187
|
-
Tests cover rendering and panel interactions, configuration persistence,
|
|
188
|
-
|
|
316
|
+
Tests cover rendering and panel interactions, configuration persistence, runtime
|
|
317
|
+
and extension-event wiring, compaction, session usage and real temporary Git
|
|
318
|
+
repositories. The POSIX PTY smoke runs **regular and fullscreen**, with isolated
|
|
319
|
+
agent/project directories and an entirely offline provider. It covers streaming, nested tools/UI waits,
|
|
320
|
+
no-content/cancellation, failed/successful compaction, model/theme changes,
|
|
321
|
+
40-column layout, draft cancel/save, off/on, reload and new session. It writes
|
|
322
|
+
`/tmp/pi-statusline-pty-<layout>.log` for diagnosis. To roll back a persistent
|
|
189
323
|
installation, disable this extension in `pi config`, re-enable the previous
|
|
190
324
|
footer and reload Pi. `/statusline off` restores the native footer, not another
|
|
191
325
|
extension's editor or widgets.
|
package/examples/statusline.json
CHANGED
|
@@ -1,19 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
3
|
"enabled": true,
|
|
4
|
-
"preset": "
|
|
5
|
-
"icons": "
|
|
6
|
-
"separator": "
|
|
4
|
+
"preset": "powerline",
|
|
5
|
+
"icons": "nerd",
|
|
6
|
+
"separator": "powerline",
|
|
7
7
|
"pathMode": "basename",
|
|
8
8
|
"modelDisplay": "name",
|
|
9
9
|
"contextDisplay": "bar",
|
|
10
10
|
"segments": [
|
|
11
|
-
{
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
{
|
|
12
|
+
"id": "model",
|
|
13
|
+
"enabled": true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "thinking",
|
|
17
|
+
"enabled": true
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"id": "context",
|
|
21
|
+
"enabled": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "directory",
|
|
25
|
+
"enabled": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"id": "git",
|
|
29
|
+
"enabled": true
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "cost",
|
|
33
|
+
"enabled": false
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"id": "input",
|
|
37
|
+
"enabled": false
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "output",
|
|
41
|
+
"enabled": false
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "tokens",
|
|
45
|
+
"enabled": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "cacheRead",
|
|
49
|
+
"enabled": false
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"id": "cacheWrite",
|
|
53
|
+
"enabled": false
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "statuses",
|
|
57
|
+
"enabled": true
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"runtime": {
|
|
61
|
+
"enabled": true,
|
|
62
|
+
"status": true,
|
|
63
|
+
"tools": true,
|
|
64
|
+
"noContent": true,
|
|
65
|
+
"noContentSeconds": 10,
|
|
66
|
+
"avg": true,
|
|
67
|
+
"cache": true,
|
|
68
|
+
"counts": true,
|
|
69
|
+
"retainSummary": true
|
|
70
|
+
}
|
|
19
71
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Clock } from "./runtime.ts";
|
|
3
|
+
import { safeReason } from "./text.ts";
|
|
4
|
+
|
|
5
|
+
export interface CompactionSnapshot {
|
|
6
|
+
active: boolean;
|
|
7
|
+
elapsedMs: number;
|
|
8
|
+
count: number;
|
|
9
|
+
last?: { status: "success" | "failed" | "cancelled"; reason?: string };
|
|
10
|
+
showFailure: boolean;
|
|
11
|
+
}
|
|
12
|
+
export class CompactionState {
|
|
13
|
+
private clock: Clock;
|
|
14
|
+
private started: number | null = null;
|
|
15
|
+
private ids = new Set<string>();
|
|
16
|
+
private last: CompactionSnapshot["last"];
|
|
17
|
+
private showFailure = false;
|
|
18
|
+
constructor(clock: Clock = () => performance.now()) { this.clock = clock; }
|
|
19
|
+
start(): void { if (this.started === null) this.started = this.clock(); this.showFailure = false; }
|
|
20
|
+
finish(status: "success" | "failed" | "cancelled", id?: string, reason?: string): void {
|
|
21
|
+
this.started = null; if (status === "success" && id) this.ids.add(id);
|
|
22
|
+
this.last = { status, ...(reason ? { reason: safeReason(reason) } : {}) }; this.showFailure = status !== "success";
|
|
23
|
+
}
|
|
24
|
+
activity(): void { this.showFailure = false; }
|
|
25
|
+
update(entries: readonly SessionEntry[]): void {
|
|
26
|
+
// getEntries contains the entire file, including compactions on other branches.
|
|
27
|
+
this.ids = new Set(entries.filter((entry) => entry.type === "compaction").map((entry) => entry.id));
|
|
28
|
+
}
|
|
29
|
+
snapshot(): CompactionSnapshot {
|
|
30
|
+
return { active: this.started !== null, elapsedMs: this.started === null ? 0 : Math.max(0, this.clock() - this.started), count: this.ids.size, last: this.last, showFailure: this.showFailure };
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
-
import type { ColorValue,
|
|
4
|
+
import type { ColorValue, StatuslineConfig } from "./types.ts";
|
|
5
5
|
import { SEGMENT_IDS } from "./types.ts";
|
|
6
6
|
|
|
7
7
|
// A deliberately small, documented set of Pi semantic foreground colors.
|
|
@@ -9,15 +9,13 @@ export const COLORS = ["text", "accent", "muted", "dim", "success", "warning", "
|
|
|
9
9
|
export function validColor(value: unknown): value is ColorValue {
|
|
10
10
|
return typeof value === "string" && (/^#[\da-f]{6}$/i.test(value) || (COLORS as readonly string[]).includes(value));
|
|
11
11
|
}
|
|
12
|
-
export function presetConfig(
|
|
13
|
-
const enabled = new Set<string>(
|
|
12
|
+
export function presetConfig(): StatuslineConfig {
|
|
13
|
+
const enabled = new Set<string>(["model", "thinking", "directory", "git", "context", "tokens", "statuses"]);
|
|
14
14
|
return {
|
|
15
|
-
version: 1, enabled: true, preset, icons: "
|
|
16
|
-
separator:
|
|
15
|
+
version: 1, enabled: true, preset: "powerline", icons: "nerd",
|
|
16
|
+
separator: "powerline", pathMode: "basename", modelDisplay: "name", contextDisplay: "bar",
|
|
17
17
|
segments: SEGMENT_IDS.map((id) => ({ id, enabled: enabled.has(id) })),
|
|
18
|
-
runtime: { enabled: true, status: true, tools: true, noContent: true, noContentSeconds: 10, avg: true, cache: true, counts: true,
|
|
19
|
-
providerMetrics: { phase: true, firstText: true, firstReasoning: true, duration: false, transport: false },
|
|
20
|
-
integrations: { cliproxyapi: "auto" },
|
|
18
|
+
runtime: { enabled: true, status: true, tools: true, noContent: true, noContentSeconds: 10, avg: true, cache: true, counts: true, retainSummary: true },
|
|
21
19
|
};
|
|
22
20
|
}
|
|
23
21
|
function record(value: unknown): value is Record<string, unknown> {
|
|
@@ -34,14 +32,15 @@ function boolean(value: unknown, name: string): boolean {
|
|
|
34
32
|
export function parseConfig(raw: unknown): StatuslineConfig {
|
|
35
33
|
if (!record(raw)) throw new Error("Configuration must be an object");
|
|
36
34
|
if (raw.version !== undefined && raw.version !== 1) throw new Error("Unsupported configuration version");
|
|
37
|
-
|
|
35
|
+
if (raw.preset !== undefined) choice(raw.preset, ["powerline"] as const, "preset");
|
|
36
|
+
const config = presetConfig();
|
|
38
37
|
if (raw.enabled !== undefined) config.enabled = boolean(raw.enabled, "enabled");
|
|
39
38
|
if (raw.icons !== undefined) config.icons = choice(raw.icons, ["unicode", "nerd", "ascii"], "icons");
|
|
40
39
|
if (raw.separator !== undefined) config.separator = choice(raw.separator, ["pipe", "dot", "space", "powerline"], "separator");
|
|
41
40
|
if (raw.pathMode !== undefined) config.pathMode = choice(raw.pathMode, ["basename", "abbreviated", "full"], "pathMode");
|
|
42
41
|
if (raw.modelDisplay !== undefined) config.modelDisplay = choice(raw.modelDisplay, ["name", "last"], "modelDisplay");
|
|
43
42
|
if (raw.contextDisplay !== undefined) config.contextDisplay = choice(raw.contextDisplay, ["text", "bar"], "contextDisplay");
|
|
44
|
-
for (const group of ["runtime"
|
|
43
|
+
for (const group of ["runtime"] as const) {
|
|
45
44
|
if (raw[group] === undefined) continue;
|
|
46
45
|
if (!record(raw[group])) throw new Error(`Invalid ${group}`);
|
|
47
46
|
for (const key of Object.keys(config[group])) {
|
|
@@ -53,10 +52,6 @@ export function parseConfig(raw: unknown): StatuslineConfig {
|
|
|
53
52
|
} else (config[group] as unknown as Record<string, unknown>)[key] = boolean(value, `${group}.${key}`);
|
|
54
53
|
}
|
|
55
54
|
}
|
|
56
|
-
if (raw.integrations !== undefined) {
|
|
57
|
-
if (!record(raw.integrations)) throw new Error("Invalid integrations");
|
|
58
|
-
if (raw.integrations.cliproxyapi !== undefined) config.integrations.cliproxyapi = choice(raw.integrations.cliproxyapi, ["auto", "off", "on"], "integrations.cliproxyapi");
|
|
59
|
-
}
|
|
60
55
|
if (raw.segments !== undefined) {
|
|
61
56
|
if (!Array.isArray(raw.segments)) throw new Error("segments must be an array");
|
|
62
57
|
const seen = new Set<string>();
|
package/src/index.ts
CHANGED
|
@@ -24,31 +24,45 @@ export default function statusline(pi: ExtensionAPI): void {
|
|
|
24
24
|
session.install();
|
|
25
25
|
});
|
|
26
26
|
pi.on("session_shutdown", () => { generation++; session?.dispose(); session = undefined; });
|
|
27
|
+
pi.on("agent_start", (_event, ctx) => { session?.setContext(ctx); session?.startRun(); session?.schedule(true); });
|
|
28
|
+
pi.on("turn_start", (_event, ctx) => { session?.setContext(ctx); session?.startTurn(); session?.schedule(true); });
|
|
29
|
+
pi.on("agent_before_settle", (event) => { session?.runtime.beforeSettle(event.outcome); });
|
|
30
|
+
pi.on("agent_settled", (_event, ctx) => { session?.setContext(ctx); session?.settle(); session?.schedule(true); });
|
|
31
|
+
pi.on("ui_prompt_start", () => { session?.runtime.uiStart(); session?.schedule(true); });
|
|
32
|
+
pi.on("ui_prompt_end", () => { session?.runtime.uiEnd(); session?.schedule(true); });
|
|
33
|
+
pi.on("tool_execution_start", (event, ctx) => { session?.setContext(ctx); session?.runtime.toolStart(event.toolCallId, event.toolName); session?.schedule(true); });
|
|
34
|
+
pi.on("session_before_compact", (_event, ctx) => {
|
|
35
|
+
session?.setContext(ctx); session?.compaction.start(); session?.runtime.compaction(true); session?.schedule(true);
|
|
36
|
+
});
|
|
27
37
|
pi.on("message_start", (event, ctx) => {
|
|
28
38
|
session?.setContext(ctx);
|
|
29
|
-
if (event.message.role === "assistant") session?.setLive(undefined);
|
|
39
|
+
if (event.message.role === "assistant") { session?.setLive(undefined); session?.runtime.messageStart(event.message); }
|
|
30
40
|
session?.schedule(true);
|
|
31
41
|
});
|
|
32
42
|
pi.on("message_update", (event, ctx) => {
|
|
33
43
|
session?.setContext(ctx);
|
|
34
|
-
if (event.message.role === "assistant") session?.setLive(event.message);
|
|
44
|
+
if (event.message.role === "assistant") { session?.setLive(event.message); session?.runtime.stream(event.assistantMessageEvent); session?.runtime.message(event.message); }
|
|
35
45
|
session?.schedule();
|
|
36
46
|
});
|
|
37
47
|
pi.on("message_end", (event, ctx) => {
|
|
38
48
|
session?.setContext(ctx);
|
|
39
|
-
if (event.message.role === "assistant") session?.setLive(event.message);
|
|
49
|
+
if (event.message.role === "assistant") { session?.setLive(event.message); session?.runtime.message(event.message, true); }
|
|
40
50
|
session?.schedule(true);
|
|
41
51
|
});
|
|
42
52
|
const refresh = (_event: unknown, ctx: ExtensionContext) => { session?.setContext(ctx); session?.schedule(true); };
|
|
43
53
|
const resetLive = (event: unknown, ctx: ExtensionContext) => { session?.setLive(undefined); refresh(event, ctx); };
|
|
44
54
|
pi.on("agent_end", resetLive);
|
|
45
|
-
pi.on("turn_end", refresh);
|
|
55
|
+
pi.on("turn_end", (event, ctx) => { if (event.message.role === "assistant") session?.runtime.message(event.message, true); session?.runtime.turnEnd(); refresh(event, ctx); });
|
|
46
56
|
pi.on("model_select", refresh);
|
|
47
57
|
pi.on("thinking_level_select", refresh);
|
|
48
|
-
pi.on("session_compact",
|
|
49
|
-
|
|
58
|
+
pi.on("session_compact", (event, ctx) => {
|
|
59
|
+
session?.compaction.finish("success", event.compactionEntry.id); session?.runtime.compaction(false); resetLive(event, ctx);
|
|
60
|
+
});
|
|
61
|
+
pi.on("session_compact_failed", (event, ctx) => {
|
|
62
|
+
session?.compaction.finish(event.aborted ? "cancelled" : "failed", undefined, event.errorMessage); session?.runtime.compaction(false); refresh(event, ctx);
|
|
63
|
+
});
|
|
50
64
|
pi.on("session_tree", resetLive);
|
|
51
|
-
pi.on("tool_execution_end", (
|
|
65
|
+
pi.on("tool_execution_end", (event, ctx) => { session?.setContext(ctx); session?.runtime.toolEnd(event.toolCallId); session?.requestGit(); session?.schedule(true); });
|
|
52
66
|
pi.on("user_bash", (_event, ctx) => { session?.setContext(ctx); session?.requestGit(); });
|
|
53
67
|
|
|
54
68
|
pi.registerCommand("statusline", {
|
|
@@ -67,7 +81,9 @@ export default function statusline(pi: ExtensionAPI): void {
|
|
|
67
81
|
if (token !== generation) return;
|
|
68
82
|
config = loaded.config;
|
|
69
83
|
if (loaded.warning) ctx.ui.notify(loaded.warning, "warning");
|
|
70
|
-
current.
|
|
84
|
+
current.dispose();
|
|
85
|
+
session = new FooterSession(ctx, config);
|
|
86
|
+
session.refresh(true); session.install();
|
|
71
87
|
return;
|
|
72
88
|
}
|
|
73
89
|
if (command) { ctx.ui.notify("Usage: /statusline [on|off|reload]", "info"); return; }
|
package/src/panel.ts
CHANGED
|
@@ -26,6 +26,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
26
26
|
private disposed = false;
|
|
27
27
|
private hasFocus = false;
|
|
28
28
|
private listHeight = 0;
|
|
29
|
+
private section: "runtime" | undefined;
|
|
29
30
|
|
|
30
31
|
constructor(options: PanelOptions) { this.options = options; this.draft = structuredClone(options.config); this.rebuild(); }
|
|
31
32
|
get focused(): boolean { return this.hasFocus; }
|
|
@@ -39,6 +40,10 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
39
40
|
return thinking;
|
|
40
41
|
}
|
|
41
42
|
private items(): SelectItem[] {
|
|
43
|
+
if (this.section) {
|
|
44
|
+
const labels: Record<string, string> = { enabled: "Runtime metrics", status: "Elapsed / compaction status", tools: "Tool activity", noContent: "No content warning", noContentSeconds: "No content threshold (seconds)", avg: "Average output speed", cache: "Cache read ratio", counts: "Turns and compactions", retainSummary: "Retain last run summary" };
|
|
45
|
+
return [...Object.entries(this.draft[this.section]).map(([key, value]) => ({ value: `option:${key}`, label: `${labels[key] ?? key}: ${value}` })), { value: "back", label: "Back to settings" }];
|
|
46
|
+
}
|
|
42
47
|
if (this.editing) {
|
|
43
48
|
const segment = this.editing;
|
|
44
49
|
return [
|
|
@@ -52,7 +57,6 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
52
57
|
];
|
|
53
58
|
}
|
|
54
59
|
return [
|
|
55
|
-
{ value: "preset", label: `Preset: ${this.draft.preset}`, description: "Cycles presets and resets segment styles/order" },
|
|
56
60
|
{ value: "defaultEnabled", label: `Enable on startup: ${this.draft.enabled ? "yes" : "no"}` },
|
|
57
61
|
{ value: "icons", label: `Icons: ${this.draft.icons}` },
|
|
58
62
|
{ value: "separator", label: `Separator: ${this.draft.separator}` },
|
|
@@ -60,6 +64,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
60
64
|
{ value: "modelDisplay", label: `Model display: ${this.draft.modelDisplay}`, description: "name: display name · last: final model ID component" },
|
|
61
65
|
{ value: "contextDisplay", label: `Context display: ${this.draft.contextDisplay}`, description: "text: tokens/capacity(percent) · bar: progress and percent" },
|
|
62
66
|
...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
|
+
{ value: "runtime", label: "Runtime options" },
|
|
63
68
|
{ value: "save", label: "Save and apply globally" },
|
|
64
69
|
{ value: "reset", label: "Restore defaults in preview" },
|
|
65
70
|
{ value: "cancel", label: "Cancel" },
|
|
@@ -78,11 +83,29 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
78
83
|
}
|
|
79
84
|
private cancel(): void {
|
|
80
85
|
if (this.input) { this.input = undefined; this.error = ""; }
|
|
86
|
+
else if (this.section) { const section = this.section; this.section = undefined; this.rebuild(section); }
|
|
81
87
|
else if (this.editing) { const id = this.editing.id; this.editing = undefined; this.rebuild(`segment:${id}`); }
|
|
82
88
|
else this.options.done();
|
|
83
89
|
}
|
|
84
90
|
private activate(value: string): void {
|
|
85
91
|
this.error = "";
|
|
92
|
+
if (this.section) {
|
|
93
|
+
if (value === "back") { this.cancel(); return; }
|
|
94
|
+
const key = value.slice("option:".length);
|
|
95
|
+
if (key === "noContentSeconds") {
|
|
96
|
+
this.input = new Input(); this.input.focused = this.hasFocus; this.input.setValue(String(this.draft.runtime.noContentSeconds));
|
|
97
|
+
this.inputLabel = "No content threshold (1–3600 seconds)";
|
|
98
|
+
this.input.onEscape = () => this.cancel();
|
|
99
|
+
this.input.onSubmit = (text) => {
|
|
100
|
+
try { this.draft = parseConfig({ ...this.draft, runtime: { ...this.draft.runtime, noContentSeconds: Number(text) } }); this.input = undefined; this.error = ""; this.rebuild(value); }
|
|
101
|
+
catch (error) { this.error = error instanceof Error ? error.message : String(error); }
|
|
102
|
+
};
|
|
103
|
+
} else {
|
|
104
|
+
const group = this.draft[this.section] as unknown as Record<string, unknown>;
|
|
105
|
+
if (typeof group[key] === "boolean") group[key] = !group[key];
|
|
106
|
+
}
|
|
107
|
+
this.rebuild(value); return;
|
|
108
|
+
}
|
|
86
109
|
if (this.editing) {
|
|
87
110
|
if (value === "back") this.cancel();
|
|
88
111
|
else if (value === "enabled") { this.editing.enabled = !this.editing.enabled; this.rebuild(value); }
|
|
@@ -92,22 +115,13 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
92
115
|
}
|
|
93
116
|
const cycle = <T,>(current: T, options: readonly T[]): T => options[(options.indexOf(current) + 1) % options.length]!;
|
|
94
117
|
switch (value) {
|
|
95
|
-
case "preset": {
|
|
96
|
-
const enabled = this.draft.enabled;
|
|
97
|
-
const modelDisplay = this.draft.modelDisplay;
|
|
98
|
-
const contextDisplay = this.draft.contextDisplay;
|
|
99
|
-
this.draft = presetConfig(cycle(this.draft.preset, ["cometix", "minimal", "powerline"]));
|
|
100
|
-
this.draft.enabled = enabled;
|
|
101
|
-
this.draft.modelDisplay = modelDisplay;
|
|
102
|
-
this.draft.contextDisplay = contextDisplay;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
118
|
case "defaultEnabled": this.draft.enabled = !this.draft.enabled; break;
|
|
106
119
|
case "icons": this.draft.icons = cycle(this.draft.icons, ["unicode", "nerd", "ascii"]); break;
|
|
107
120
|
case "separator": this.draft.separator = cycle(this.draft.separator, ["pipe", "dot", "space", "powerline"]); break;
|
|
108
121
|
case "pathMode": this.draft.pathMode = cycle(this.draft.pathMode, ["basename", "abbreviated", "full"]); break;
|
|
109
122
|
case "modelDisplay": this.draft.modelDisplay = cycle(this.draft.modelDisplay, ["name", "last"]); break;
|
|
110
123
|
case "contextDisplay": this.draft.contextDisplay = cycle(this.draft.contextDisplay, ["text", "bar"]); break;
|
|
124
|
+
case "runtime": this.section = value; this.rebuild(); return;
|
|
111
125
|
case "reset": this.draft = presetConfig(); break;
|
|
112
126
|
case "cancel": this.options.done(); return;
|
|
113
127
|
case "save": void this.save(); return;
|
|
@@ -160,7 +174,8 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
160
174
|
[this.draft.segments[segmentIndex], this.draft.segments[next]] = [this.draft.segments[next]!, this.draft.segments[segmentIndex]!];
|
|
161
175
|
this.rebuild(selected);
|
|
162
176
|
}
|
|
163
|
-
} else this.
|
|
177
|
+
} else if (this.section && matchesKey(data, "space") && selected?.startsWith("option:")) this.activate(selected);
|
|
178
|
+
else this.list.handleInput(data);
|
|
164
179
|
}
|
|
165
180
|
if (!this.disposed) this.options.requestRender();
|
|
166
181
|
}
|
|
@@ -172,7 +187,7 @@ export class StatuslinePanel implements Component, Focusable {
|
|
|
172
187
|
const { data, example } = this.options.snapshot();
|
|
173
188
|
const preview = renderFooter(width, data, this.draft, this.options.theme());
|
|
174
189
|
const lines = [
|
|
175
|
-
this.paint(`Statusline settings${this.editing ? ` / ${this.editing.id}` : ""}`, "accent"),
|
|
190
|
+
this.paint(`Statusline settings${this.editing ? ` / ${this.editing.id}` : this.section ? ` / ${this.section}` : ""}`, "accent"),
|
|
176
191
|
this.paint(example ? "Preview · example data" : "Preview · current session", "muted"),
|
|
177
192
|
...(preview.length ? preview : [this.paint("(all segments hidden)", "muted")]),
|
|
178
193
|
this.paint("Changes are a draft until Save. Fees are Pi-reported USD.", "muted"),
|
package/src/render.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { cleanText } from "./text.ts";
|
|
|
6
6
|
import type { ColorValue, SegmentConfig, SegmentId, StatuslineConfig, StatuslineSnapshot } from "./types.ts";
|
|
7
7
|
import { emptyGit } from "./git.ts";
|
|
8
8
|
import { emptyUsage } from "./usage.ts";
|
|
9
|
+
import { renderRuntime } from "./runtime-render.ts";
|
|
9
10
|
|
|
10
11
|
export type RenderTheme = Pick<Theme, "style" | "colors" | "appearance">;
|
|
11
12
|
// Keep rainbow hues independent of semantic theme colors, which can repeat.
|
|
@@ -77,7 +78,7 @@ function segmentText(id: SegmentId, data: StatuslineSnapshot, config: Statusline
|
|
|
77
78
|
case "statuses": return [...data.statuses.values()].map(cleanText).filter(Boolean).join(" · ") || undefined;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
|
-
function defaultColor(id: SegmentId, data: StatuslineSnapshot
|
|
81
|
+
function defaultColor(id: SegmentId, data: StatuslineSnapshot): ColorValue {
|
|
81
82
|
if (id === "context") {
|
|
82
83
|
if ((data.context.percent ?? 0) > 90) return "error";
|
|
83
84
|
if ((data.context.percent ?? 0) > 70) return "warning";
|
|
@@ -91,7 +92,6 @@ function defaultColor(id: SegmentId, data: StatuslineSnapshot, config: Statuslin
|
|
|
91
92
|
const colors: Record<string, ColorValue> = { off: "thinkingOff", minimal: "thinkingMinimal", low: "thinkingLow", medium: "thinkingMedium", high: "thinkingHigh", xhigh: "thinkingXhigh", max: "thinkingMax" };
|
|
92
93
|
return colors[data.thinking] ?? "muted";
|
|
93
94
|
}
|
|
94
|
-
if (config.preset === "minimal") return "muted";
|
|
95
95
|
if (id === "model") return "accent";
|
|
96
96
|
if (id === "directory") return "success";
|
|
97
97
|
if (id === "context") return "#c678dd";
|
|
@@ -154,19 +154,23 @@ export function renderFooter(width: number, data: StatuslineSnapshot, config: St
|
|
|
154
154
|
if (width === 0) return [];
|
|
155
155
|
const parts: Part[] = [];
|
|
156
156
|
let secondary: Part | undefined;
|
|
157
|
+
// CLIProxyAPI's published `tps` status is display text only.
|
|
158
|
+
const tps = cleanText(data.statuses.get("tps") ?? "");
|
|
159
|
+
const elapsedStatus = config.segments.some((s) => s.id === "statuses" && s.enabled) && /^Elapsed \d+[dhms](?: \d+[hms])*$/.test(tps) ? tps : undefined;
|
|
160
|
+
const statuses = elapsedStatus ? new Map([...data.statuses].filter(([key]) => key !== "tps")) : data.statuses;
|
|
157
161
|
const thinking = config.segments.find((segment) => segment.id === "thinking");
|
|
158
162
|
for (const segment of config.segments) {
|
|
159
163
|
if (!segment.enabled) continue;
|
|
160
|
-
const raw = segmentText(segment.id, data, config);
|
|
164
|
+
const raw = segmentText(segment.id, segment.id === "statuses" ? { ...data, statuses } : data, config);
|
|
161
165
|
if (raw === undefined) continue;
|
|
162
166
|
let text = cleanText(raw);
|
|
163
167
|
if (config.icons === "ascii") text = text.replaceAll(" · ", " / ").replaceAll("…", "...");
|
|
164
168
|
const background = segment.background ?? (config.preset === "powerline" ? (theme.appearance === "light" ? (parts.length % 2 ? "#e4e9f2" : "#d6dde9") : (parts.length % 2 ? "#283447" : "#202938")) : undefined);
|
|
165
|
-
const part: Part = { config: segment, text, icon: cleanText(segment.icon ?? ICONS[config.icons][segment.id] ?? ""), foreground: segment.color ?? defaultColor(segment.id, data
|
|
169
|
+
const part: Part = { config: segment, text, icon: cleanText(segment.icon ?? ICONS[config.icons][segment.id] ?? ""), foreground: segment.color ?? defaultColor(segment.id, data), background };
|
|
166
170
|
if (segment.id === "model" && data.reasoning && thinking?.enabled) {
|
|
167
171
|
part.thinking = {
|
|
168
172
|
text: cleanText(data.thinking || "off"),
|
|
169
|
-
foreground: thinking.color ?? (["high", "xhigh", "max"].includes(data.thinking) ? "rainbow" : defaultColor("thinking", data
|
|
173
|
+
foreground: thinking.color ?? (["high", "xhigh", "max"].includes(data.thinking) ? "rainbow" : defaultColor("thinking", data)),
|
|
170
174
|
};
|
|
171
175
|
}
|
|
172
176
|
if (segment.id === "statuses") secondary = part;
|
|
@@ -194,7 +198,8 @@ export function renderFooter(width: number, data: StatuslineSnapshot, config: St
|
|
|
194
198
|
}
|
|
195
199
|
const lines: string[] = [];
|
|
196
200
|
if (parts.length) lines.push(truncateToWidth(line, width, ellipsis));
|
|
197
|
-
|
|
201
|
+
const secondLine = renderRuntime(width, data, config, theme, secondary ? renderPart(secondary, theme) : undefined, elapsedStatus);
|
|
202
|
+
if (secondLine) lines.push(secondLine);
|
|
198
203
|
return lines;
|
|
199
204
|
}
|
|
200
205
|
|
|
@@ -205,5 +210,7 @@ export function sampleSnapshot(): StatuslineSnapshot {
|
|
|
205
210
|
usage: { ...emptyUsage(), input: 32000, output: 5400, cacheRead: 12000, cost: 0.12 },
|
|
206
211
|
git: { ...emptyGit("repository"), branch: "main", unstaged: 2, ahead: 2 },
|
|
207
212
|
statuses: new Map([["example", "Example extension status"]]),
|
|
213
|
+
runtime: { runId: "example", status: "Running", elapsedMs: 28000, turns: 3, toolCalls: 2, tools: [], awaitingInput: false, noContentMs: 0, usage: { ...emptyUsage(), output: 1260 }, avg: 45, cache: 0.86 },
|
|
214
|
+
compaction: { active: false, elapsedMs: 0, count: 1, showFailure: false },
|
|
208
215
|
};
|
|
209
216
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { RenderTheme } from "./render.ts";
|
|
3
|
+
import { cleanText } from "./text.ts";
|
|
4
|
+
import type { StatuslineConfig, StatuslineSnapshot } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export function seconds(ms: number): string { return `${Math.floor(ms / 1000)}s`; }
|
|
7
|
+
export function elapsed(ms: number): string {
|
|
8
|
+
let remaining = Math.max(0, Math.floor(ms / 1000));
|
|
9
|
+
const parts: string[] = [];
|
|
10
|
+
for (const [unit, size] of [["d", 86400], ["h", 3600], ["m", 60], ["s", 1]] as const) {
|
|
11
|
+
const value = Math.floor(remaining / size); remaining %= size;
|
|
12
|
+
if (value || parts.length || unit === "s") parts.push(`${value}${unit}`);
|
|
13
|
+
}
|
|
14
|
+
return parts.join(" ");
|
|
15
|
+
}
|
|
16
|
+
interface Part { id: string; text: string; color?: "text" | "muted" | "warning" | "error" | "success"; styled?: string }
|
|
17
|
+
|
|
18
|
+
/** Layout consumes snapshots only; width decisions use terminal cells, including ANSI. */
|
|
19
|
+
export function renderRuntime(width: number, data: StatuslineSnapshot, config: StatuslineConfig, theme: RenderTheme, extensionStatus?: string, elapsedStatus?: string): string | undefined {
|
|
20
|
+
const parts: Part[] = [];
|
|
21
|
+
const runtime = data.runtime, compact = data.compaction;
|
|
22
|
+
const enabled = config.runtime.enabled;
|
|
23
|
+
const add = (id: string, text: string, color?: Part["color"]) => parts.push({ id, text, color });
|
|
24
|
+
if (elapsedStatus) add("elapsed", elapsedStatus);
|
|
25
|
+
else if (enabled && config.runtime.status && runtime?.runId) add("elapsed", `Elapsed ${elapsed(runtime.elapsedMs)}`);
|
|
26
|
+
if (enabled && compact?.active) {
|
|
27
|
+
add("status", `${config.icons === "ascii" ? "Compacting..." : "Compacting…"} ${seconds(compact.elapsedMs)}`, "warning");
|
|
28
|
+
if (config.runtime.counts) add("compactions", `Compactions ${compact.count}`);
|
|
29
|
+
} else {
|
|
30
|
+
if (enabled && runtime) {
|
|
31
|
+
if (config.runtime.status) {
|
|
32
|
+
const failure = compact?.showFailure && runtime.status !== "Running" ? `Compaction ${compact.last?.status}` : undefined;
|
|
33
|
+
if (failure) add("status", failure, "error");
|
|
34
|
+
}
|
|
35
|
+
let busy = false;
|
|
36
|
+
if (runtime.awaitingInput) { add("activity", "Awaiting input", "warning"); busy = true; }
|
|
37
|
+
else if (config.runtime.tools && runtime.tools.length) {
|
|
38
|
+
add("activity", runtime.tools.length > 1 ? `Tools ${runtime.tools.length} active` : `Tool ${cleanText(runtime.tools[0]!.name).slice(0, 40)} ${seconds(runtime.tools[0]!.elapsedMs)}`);
|
|
39
|
+
busy = true;
|
|
40
|
+
} else if (config.runtime.noContent && runtime.noContentMs !== null && runtime.noContentMs >= config.runtime.noContentSeconds * 1000) {
|
|
41
|
+
add("activity", `No content ${seconds(runtime.noContentMs)}`); busy = true;
|
|
42
|
+
}
|
|
43
|
+
if (!busy) {
|
|
44
|
+
if (config.runtime.avg && runtime.avg !== null) add("avg", `Avg ${Math.round(runtime.avg)} tok/s`);
|
|
45
|
+
if (config.runtime.cache && runtime.cache !== null) add("cache", `Cache ${Math.round(runtime.cache * 100)}%`);
|
|
46
|
+
}
|
|
47
|
+
if (config.runtime.counts && runtime.runId) add("turns", `Turns ${runtime.turns}`);
|
|
48
|
+
}
|
|
49
|
+
if (enabled && config.runtime.counts && compact) add("compactions", `Compactions ${compact.count}`);
|
|
50
|
+
if (extensionStatus) parts.push({ id: "statuses", text: extensionStatus, styled: extensionStatus });
|
|
51
|
+
}
|
|
52
|
+
const separator = config.icons === "ascii" ? " | " : " · ";
|
|
53
|
+
const draw = () => parts.map((part) => part.styled ?? theme.style(part.text, { fg: theme.colors[part.color ?? "muted"] })).join(theme.style(separator, { fg: theme.colors.dim }));
|
|
54
|
+
let line = draw();
|
|
55
|
+
for (const id of ["statuses", "cache", "avg", "compactions", "turns", "activity"]) {
|
|
56
|
+
if (visibleWidth(line) <= width) break;
|
|
57
|
+
const index = parts.findIndex((part) => part.id === id);
|
|
58
|
+
if (index !== -1) parts.splice(index, 1);
|
|
59
|
+
line = draw();
|
|
60
|
+
}
|
|
61
|
+
return parts.length ? truncateToWidth(line, width, config.icons === "ascii" ? "..." : "…") : undefined;
|
|
62
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { AssistantMessage, AssistantMessageEvent } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { AgentActivityOutcome } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { cleanText } from "./text.ts";
|
|
5
|
+
import { emptyUsage, readUsage } from "./usage.ts";
|
|
6
|
+
import type { UsageTotals } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export type Clock = () => number;
|
|
9
|
+
export interface RuntimeSnapshot {
|
|
10
|
+
runId: string | null;
|
|
11
|
+
status: "Idle" | "Running" | "Done" | "Cancelled" | "Failed";
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
turns: number;
|
|
14
|
+
toolCalls: number;
|
|
15
|
+
tools: { name: string; elapsedMs: number }[];
|
|
16
|
+
awaitingInput: boolean;
|
|
17
|
+
noContentMs: number | null;
|
|
18
|
+
usage: UsageTotals | null;
|
|
19
|
+
avg: number | null;
|
|
20
|
+
cache: number | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A run survives agent_end, retries and boundary continuations until agent_settled. */
|
|
24
|
+
export class RuntimeState {
|
|
25
|
+
private clock: Clock;
|
|
26
|
+
private startMs = 0;
|
|
27
|
+
private endMs = 0;
|
|
28
|
+
private runId: string | null = null;
|
|
29
|
+
private state: RuntimeSnapshot["status"] = "Idle";
|
|
30
|
+
private outcome: AgentActivityOutcome = "completed";
|
|
31
|
+
private aborted = false;
|
|
32
|
+
private turns = 0;
|
|
33
|
+
private tools = new Map<string, { name: string; start: number }>();
|
|
34
|
+
private seenTools = new Set<string>();
|
|
35
|
+
private prompts = 0;
|
|
36
|
+
private modelActive = false;
|
|
37
|
+
private compacting = false;
|
|
38
|
+
private lastContent = 0;
|
|
39
|
+
private messages = new Map<string, UsageTotals>();
|
|
40
|
+
private activeMessage: string | undefined;
|
|
41
|
+
private messageAliases = new Map<string, string>();
|
|
42
|
+
private sequence = 0;
|
|
43
|
+
constructor(clock: Clock = () => performance.now()) { this.clock = clock; }
|
|
44
|
+
get running(): boolean { return this.state === "Running"; }
|
|
45
|
+
get id(): string | null { return this.runId; }
|
|
46
|
+
reset(): void {
|
|
47
|
+
this.runId = null; this.state = "Idle"; this.startMs = 0; this.endMs = 0;
|
|
48
|
+
this.outcome = "completed"; this.aborted = false; this.turns = 0; this.tools.clear(); this.seenTools.clear();
|
|
49
|
+
this.prompts = 0; this.modelActive = false; this.compacting = false;
|
|
50
|
+
this.messages.clear(); this.messageAliases.clear(); this.activeMessage = undefined; this.sequence = 0;
|
|
51
|
+
}
|
|
52
|
+
start(): boolean {
|
|
53
|
+
if (this.running) return false;
|
|
54
|
+
const prompts = this.prompts;
|
|
55
|
+
this.reset(); this.prompts = prompts;
|
|
56
|
+
this.runId = randomUUID(); this.startMs = this.clock(); this.lastContent = this.startMs; this.state = "Running";
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
beforeSettle(outcome: AgentActivityOutcome): void { if (this.running && !this.aborted) this.outcome = outcome; }
|
|
60
|
+
observeAbort(): void { if (this.running) { this.aborted = true; this.outcome = "aborted"; } }
|
|
61
|
+
settle(): void {
|
|
62
|
+
if (!this.running) return;
|
|
63
|
+
this.endMs = this.clock();
|
|
64
|
+
this.state = { completed: "Done", aborted: "Cancelled", error: "Failed" }[this.outcome] as RuntimeSnapshot["status"];
|
|
65
|
+
this.modelActive = false; this.tools.clear(); this.activeMessage = undefined;
|
|
66
|
+
}
|
|
67
|
+
turnStart(): void {
|
|
68
|
+
if (!this.running) return;
|
|
69
|
+
// A continuation/retry supersedes the previous attempt's observed outcome.
|
|
70
|
+
this.outcome = "completed"; this.aborted = false;
|
|
71
|
+
this.turns++; this.modelActive = true; this.lastContent = this.clock(); this.activeMessage = undefined;
|
|
72
|
+
}
|
|
73
|
+
turnEnd(): void { this.modelActive = false; }
|
|
74
|
+
content(): void { if (this.running && !this.compacting) this.lastContent = this.clock(); }
|
|
75
|
+
stream(event: AssistantMessageEvent): void {
|
|
76
|
+
if ((event.type === "text_delta" || event.type === "thinking_delta" || event.type === "toolcall_delta") && event.delta.length > 0) this.content();
|
|
77
|
+
// A tool name/ID in the initial block is content even before JSON deltas arrive.
|
|
78
|
+
if (event.type === "toolcall_start") {
|
|
79
|
+
const block = event.partial.content[event.contentIndex];
|
|
80
|
+
if (block?.type === "toolCall" && (block.name || block.id)) this.content();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
private alias(message: AssistantMessage): string { return `${message.provider}/${message.model}/${message.timestamp}`; }
|
|
84
|
+
messageStart(message: AssistantMessage): void {
|
|
85
|
+
if (!this.running || this.compacting) return;
|
|
86
|
+
this.activeMessage = String(++this.sequence);
|
|
87
|
+
this.messageAliases.set(this.alias(message), this.activeMessage);
|
|
88
|
+
}
|
|
89
|
+
message(message: AssistantMessage, final = false): void {
|
|
90
|
+
if (!this.running || this.compacting) return;
|
|
91
|
+
const alias = this.alias(message);
|
|
92
|
+
const responseAlias = message.responseId ? `${message.provider}/${message.model}/response:${message.responseId}` : undefined;
|
|
93
|
+
const key = (responseAlias ? this.messageAliases.get(responseAlias) : undefined) ?? this.messageAliases.get(alias) ?? this.activeMessage ?? String(++this.sequence);
|
|
94
|
+
this.messageAliases.set(alias, key);
|
|
95
|
+
if (responseAlias) this.messageAliases.set(responseAlias, key);
|
|
96
|
+
// Missing usage stays unavailable. Zeroes explicitly supplied by Pi are valid.
|
|
97
|
+
if (message.usage && [message.usage.input, message.usage.output, message.usage.cacheRead, message.usage.cacheWrite].every((n) => Number.isFinite(n) && n >= 0)) {
|
|
98
|
+
this.messages.set(key, readUsage(message.usage));
|
|
99
|
+
}
|
|
100
|
+
if (final) {
|
|
101
|
+
this.modelActive = false;
|
|
102
|
+
if (!this.aborted) this.outcome = message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "error" : "completed";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
toolStart(id: string, name: string): void {
|
|
106
|
+
if (!this.running || this.seenTools.has(id)) return;
|
|
107
|
+
this.seenTools.add(id); this.tools.set(id, { name: cleanText(name).slice(0, 40) || "unknown", start: this.clock() });
|
|
108
|
+
}
|
|
109
|
+
toolEnd(id: string): void { this.tools.delete(id); }
|
|
110
|
+
uiStart(): void { this.prompts++; }
|
|
111
|
+
uiEnd(): void { this.prompts = Math.max(0, this.prompts - 1); this.lastContent = this.clock(); }
|
|
112
|
+
compaction(active: boolean): void { this.compacting = active; this.lastContent = this.clock(); }
|
|
113
|
+
snapshot(retainSummary = true): RuntimeSnapshot {
|
|
114
|
+
const now = this.clock();
|
|
115
|
+
const hidden = !retainSummary && !this.running;
|
|
116
|
+
const elapsedMs = hidden || !this.runId ? 0 : Math.max(0, (this.running ? now : this.endMs) - this.startMs);
|
|
117
|
+
const usage = !hidden && this.messages.size ? emptyUsage() : null;
|
|
118
|
+
if (usage) for (const contribution of this.messages.values()) for (const key of Object.keys(usage) as (keyof UsageTotals)[]) usage[key] += contribution[key];
|
|
119
|
+
const denominator = usage ? usage.input + usage.cacheRead + usage.cacheWrite : 0;
|
|
120
|
+
return {
|
|
121
|
+
runId: hidden ? null : this.runId, status: hidden ? "Idle" : this.state, elapsedMs,
|
|
122
|
+
turns: hidden ? 0 : this.turns, toolCalls: hidden ? 0 : this.seenTools.size,
|
|
123
|
+
tools: [...this.tools.values()].map((tool) => ({ name: tool.name, elapsedMs: Math.max(0, now - tool.start) })),
|
|
124
|
+
awaitingInput: this.prompts > 0,
|
|
125
|
+
noContentMs: this.running && this.modelActive && !this.tools.size && !this.prompts && !this.compacting ? Math.max(0, now - this.lastContent) : null,
|
|
126
|
+
usage, avg: usage && elapsedMs > 0 ? usage.output / (elapsedMs / 1000) : null,
|
|
127
|
+
cache: usage && denominator > 0 ? usage.cacheRead / denominator : null,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
package/src/session.ts
CHANGED
|
@@ -6,8 +6,12 @@ import { emptyGit, GitPoller, type GitReader } from "./git.ts";
|
|
|
6
6
|
import { renderFooter } from "./render.ts";
|
|
7
7
|
import type { StatuslineConfig, StatuslineSnapshot } from "./types.ts";
|
|
8
8
|
import { UsageCache } from "./usage.ts";
|
|
9
|
+
import { RuntimeState } from "./runtime.ts";
|
|
10
|
+
import { CompactionState } from "./compaction.ts";
|
|
9
11
|
|
|
10
12
|
export class FooterSession {
|
|
13
|
+
readonly runtime = new RuntimeState();
|
|
14
|
+
readonly compaction = new CompactionState();
|
|
11
15
|
private ctx: ExtensionContext;
|
|
12
16
|
private config: StatuslineConfig;
|
|
13
17
|
private usage = new UsageCache();
|
|
@@ -24,22 +28,43 @@ export class FooterSession {
|
|
|
24
28
|
private displayKey = "";
|
|
25
29
|
private context: StatuslineSnapshot["context"] = { tokens: null, contextWindow: 0, percent: null };
|
|
26
30
|
private readonly gitReader: GitReader | undefined;
|
|
31
|
+
private signal: AbortSignal | undefined;
|
|
32
|
+
private readonly onAbort = () => { this.runtime.observeAbort(); this.schedule(true); };
|
|
27
33
|
|
|
28
34
|
constructor(ctx: ExtensionContext, config: StatuslineConfig, gitReader?: GitReader) {
|
|
29
35
|
this.ctx = ctx; this.config = config; this.gitReader = gitReader;
|
|
30
36
|
}
|
|
31
37
|
get active(): boolean { return this.ownsFooter && !this.closed; }
|
|
32
|
-
setContext(ctx: ExtensionContext): void { this.ctx = ctx; }
|
|
38
|
+
setContext(ctx: ExtensionContext): void { this.ctx = ctx; this.watchSignal(); }
|
|
39
|
+
private watchSignal(): void {
|
|
40
|
+
if (this.closed || !this.ownsFooter || !this.runtime.running) return;
|
|
41
|
+
const signal = this.ctx.signal;
|
|
42
|
+
if (!signal || signal === this.signal) return;
|
|
43
|
+
this.clearSignal(); this.signal = signal;
|
|
44
|
+
if (signal.aborted) this.onAbort();
|
|
45
|
+
else signal.addEventListener("abort", this.onAbort, { once: true });
|
|
46
|
+
}
|
|
47
|
+
private clearSignal(): void { this.signal?.removeEventListener("abort", this.onAbort); this.signal = undefined; }
|
|
48
|
+
startRun(): void {
|
|
49
|
+
this.compaction.activity();
|
|
50
|
+
this.runtime.start();
|
|
51
|
+
this.watchSignal();
|
|
52
|
+
}
|
|
53
|
+
startTurn(): void {
|
|
54
|
+
this.compaction.activity(); this.runtime.turnStart(); this.watchSignal();
|
|
55
|
+
}
|
|
56
|
+
settle(): void { this.runtime.settle(); this.clearSignal(); }
|
|
33
57
|
install(): void {
|
|
34
58
|
if (this.closed || this.ctx.mode !== "tui" || !this.config.enabled) return;
|
|
35
59
|
this.ctx.ui.setFooter((tui, _theme, provider) => {
|
|
36
60
|
this.ownsFooter = true; this.tui = tui; this.provider = provider;
|
|
61
|
+
this.watchSignal();
|
|
37
62
|
this.poller = new GitPoller(this.ctx.cwd, (git) => { this.git = git; this.repaint(); }, this.gitReader);
|
|
38
63
|
this.unsubscribe = provider.onBranchChange(() => this.requestGit());
|
|
39
64
|
if (this.config.segments.some((segment) => segment.id === "git" && segment.enabled)) this.poller.start();
|
|
40
65
|
this.refresh(true);
|
|
41
66
|
// Covers usage entries appended outside normal message events (e.g. cache warming).
|
|
42
|
-
this.timer = setInterval(() => this.refresh(),
|
|
67
|
+
this.timer = setInterval(() => this.refresh(), 1000);
|
|
43
68
|
this.timer.unref();
|
|
44
69
|
return {
|
|
45
70
|
render: (width: number) => renderFooter(width, this.snapshot(), this.config, this.ctx.ui.theme),
|
|
@@ -49,6 +74,7 @@ export class FooterSession {
|
|
|
49
74
|
});
|
|
50
75
|
}
|
|
51
76
|
private stopResources(): void {
|
|
77
|
+
this.clearSignal();
|
|
52
78
|
clearInterval(this.timer); clearTimeout(this.scheduled);
|
|
53
79
|
this.timer = undefined; this.scheduled = undefined;
|
|
54
80
|
this.poller?.dispose(); this.poller = undefined;
|
|
@@ -60,7 +86,7 @@ export class FooterSession {
|
|
|
60
86
|
this.stopResources();
|
|
61
87
|
if (restore && !this.closed) this.ctx.ui.setFooter(undefined);
|
|
62
88
|
}
|
|
63
|
-
dispose(): void { this.stopResources(); this.closed = true; }
|
|
89
|
+
dispose(): void { this.stopResources(); this.clearSignal(); this.closed = true; }
|
|
64
90
|
apply(config: StatuslineConfig): void {
|
|
65
91
|
if (this.closed) return;
|
|
66
92
|
this.disable(); this.config = config;
|
|
@@ -83,11 +109,13 @@ export class FooterSession {
|
|
|
83
109
|
const manager = this.ctx.sessionManager;
|
|
84
110
|
const key = `${manager.getSessionId()}:${manager.getLeafId()}:${this.ctx.model?.provider}/${this.ctx.model?.id}/${this.ctx.model?.contextWindow}`;
|
|
85
111
|
if (force || key !== this.statsKey) {
|
|
86
|
-
|
|
112
|
+
const entries = manager.getEntries();
|
|
113
|
+
this.usage.update(entries);
|
|
114
|
+
this.compaction.update(entries);
|
|
87
115
|
const context = this.ctx.getContextUsage();
|
|
88
116
|
this.context = context ?? { tokens: null, contextWindow: this.ctx.model?.contextWindow ?? 0, percent: null };
|
|
89
|
-
this.statsKey = key;
|
|
90
117
|
}
|
|
118
|
+
this.statsKey = key;
|
|
91
119
|
this.repaint();
|
|
92
120
|
}
|
|
93
121
|
private repaint(): void {
|
|
@@ -103,6 +131,7 @@ export class FooterSession {
|
|
|
103
131
|
thinking: this.ctx.thinkingLevel ?? "off", cwd: this.ctx.cwd, home: homedir(),
|
|
104
132
|
context: this.context, usage: this.usage.snapshot(), git: this.git,
|
|
105
133
|
statuses: this.provider?.getExtensionStatuses() ?? new Map(),
|
|
134
|
+
runtime: this.runtime.snapshot(this.config.runtime.retainSummary), compaction: this.compaction.snapshot(),
|
|
106
135
|
};
|
|
107
136
|
}
|
|
108
137
|
}
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
export const SEGMENT_IDS = ["model", "thinking", "context", "directory", "git", "cost", "input", "output", "tokens", "cacheRead", "cacheWrite", "statuses"] as const;
|
|
4
4
|
export type SegmentId = typeof SEGMENT_IDS[number];
|
|
5
5
|
export type ColorValue = ThemeColor | `#${string}`;
|
|
6
|
-
export type Preset = "
|
|
6
|
+
export type Preset = "powerline";
|
|
7
7
|
export type IconMode = "unicode" | "nerd" | "ascii";
|
|
8
8
|
export interface RuntimeConfig {
|
|
9
9
|
enabled: boolean;
|
|
@@ -14,16 +14,8 @@ export interface RuntimeConfig {
|
|
|
14
14
|
avg: boolean;
|
|
15
15
|
cache: boolean;
|
|
16
16
|
counts: boolean;
|
|
17
|
-
untilCompact: boolean;
|
|
18
17
|
retainSummary: boolean;
|
|
19
18
|
}
|
|
20
|
-
export interface ProviderMetricsConfig {
|
|
21
|
-
phase: boolean;
|
|
22
|
-
firstText: boolean;
|
|
23
|
-
firstReasoning: boolean;
|
|
24
|
-
duration: boolean;
|
|
25
|
-
transport: boolean;
|
|
26
|
-
}
|
|
27
19
|
export interface SegmentConfig {
|
|
28
20
|
id: SegmentId;
|
|
29
21
|
enabled: boolean;
|
|
@@ -43,8 +35,6 @@ export interface StatuslineConfig {
|
|
|
43
35
|
contextDisplay: "text" | "bar";
|
|
44
36
|
segments: SegmentConfig[];
|
|
45
37
|
runtime: RuntimeConfig;
|
|
46
|
-
providerMetrics: ProviderMetricsConfig;
|
|
47
|
-
integrations: { cliproxyapi: "auto" | "off" | "on" };
|
|
48
38
|
}
|
|
49
39
|
export interface UsageTotals {
|
|
50
40
|
input: number;
|
|
@@ -77,5 +67,4 @@ export interface StatuslineSnapshot {
|
|
|
77
67
|
statuses: ReadonlyMap<string, string>;
|
|
78
68
|
runtime?: import("./runtime.ts").RuntimeSnapshot;
|
|
79
69
|
compaction?: import("./compaction.ts").CompactionSnapshot;
|
|
80
|
-
telemetry?: import("./receiver.ts").TelemetrySnapshot;
|
|
81
70
|
}
|