@erichll/pi-statusline 0.0.0-stage → 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 +35 -0
- package/LICENSE +21 -0
- package/README.md +329 -2
- package/examples/statusline.json +71 -0
- package/package.json +29 -4
- package/src/compaction.ts +32 -0
- package/src/config.ts +116 -0
- package/src/git.ts +105 -0
- package/src/index.ts +108 -0
- package/src/panel.ts +202 -0
- package/src/render.ts +216 -0
- package/src/runtime-render.ts +62 -0
- package/src/runtime.ts +130 -0
- package/src/session.ts +137 -0
- package/src/text.ts +11 -0
- package/src/types.ts +70 -0
- package/src/usage.ts +52 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Changelog
|
|
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
|
+
|
|
20
|
+
## 0.1.0 - 2026-10-03
|
|
21
|
+
|
|
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.
|
|
23
|
+
- Place context immediately after the model in preset layouts and add a configurable context progress bar (`contextDisplay: "bar"`).
|
|
24
|
+
- Hide the cost segment by default in every preset; it can still be enabled in settings.
|
|
25
|
+
- Use an independent rainbow palette so themes with repeated semantic colors still display distinct thinking-level hues.
|
|
26
|
+
- Use `` as the default model glyph and append the thinking level in parentheses, coloring the entire `(level)` with per-level colors and rainbow high/xhigh/max.
|
|
27
|
+
- Move thinking controls into the model settings; retain existing `thinking` enablement/color settings for compatibility.
|
|
28
|
+
- Move configuration to `extensions/pi-statusline/config.json` under Pi's agent directory.
|
|
29
|
+
- Compact context usage to `used/capacity(percent%)`, removing separator padding.
|
|
30
|
+
- Add `modelDisplay: "last"` to show only the final component of a slash-separated model ID, configurable in the preview panel.
|
|
31
|
+
- Add a native Pi footer with configurable presets.
|
|
32
|
+
- Add configurable segments, Unicode/Nerd Font/ASCII icons, colors and path display.
|
|
33
|
+
- Add a keyboard-driven configuration panel using the same renderer as the live footer.
|
|
34
|
+
- Add asynchronous Git state, native session usage accounting and extension-status display.
|
|
35
|
+
- Handle narrow terminals, session replacement, compaction and non-interactive modes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @erichll/pi-statusline
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
5
|
+
```text
|
|
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
|
+
```
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pi install npm:@erichll/pi-statusline
|
|
14
|
+
```
|
|
15
|
+
|
|
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
|
|
19
|
+
other runtime dependencies and makes no network requests or credential lookups.
|
|
20
|
+
Only one extension should own Pi's footer, so disable any other footer
|
|
21
|
+
extension in `pi config` before enabling this one.
|
|
22
|
+
|
|
23
|
+
## Local use
|
|
24
|
+
|
|
25
|
+
From this repository, try the extension for one invocation:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install
|
|
29
|
+
pi --no-extensions --extension ./packages/pi-statusline/src/index.ts
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`--no-extensions` disables other auto-discovered extensions for this invocation.
|
|
33
|
+
To use your other extensions, disable `pi-powerline-footer` in `pi config`
|
|
34
|
+
before starting Pi with `--extension ./packages/pi-statusline/src/index.ts`.
|
|
35
|
+
Only one extension should own the footer. The previous package can remain
|
|
36
|
+
installed for rollback. Loading this repository as a whole also discovers the
|
|
37
|
+
security extensions; use the statusline entrypoint to try it on its own.
|
|
38
|
+
|
|
39
|
+
This checkout is for development only. For a persistent install use the
|
|
40
|
+
published package, `npm:@erichll/pi-statusline`.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | Behavior |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `/statusline` | Open configuration and live preview |
|
|
47
|
+
| `/statusline on` | Enable for this session |
|
|
48
|
+
| `/statusline off` | Restore Pi's native footer for this session |
|
|
49
|
+
| `/statusline reload` | Reload saved configuration, including startup enablement |
|
|
50
|
+
|
|
51
|
+
The panel supports arrow navigation, Space to toggle a segment, Alt+Up/Down to
|
|
52
|
+
reorder it, and Enter to edit its properties. Choose **Save and apply globally**
|
|
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.
|
|
56
|
+
**Restore defaults in preview** changes only the draft until saved.
|
|
57
|
+
Preview uses the real footer renderer and current session data, or clearly
|
|
58
|
+
labelled example data for an empty session. A save error leaves the active
|
|
59
|
+
configuration unchanged and keeps the panel open.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
Settings live in `extensions/pi-statusline/config.json` under Pi's agent directory,
|
|
64
|
+
normally `~/.pi/agent/extensions/pi-statusline/config.json`;
|
|
65
|
+
`PI_CODING_AGENT_DIR` is respected. The extension
|
|
66
|
+
does not write Pi's general `settings.json`. Configuration is global, with no
|
|
67
|
+
project override or file watcher. After manual changes use `/statusline reload`.
|
|
68
|
+
The configuration directory is created automatically when saving from the panel.
|
|
69
|
+
When upgrading from the previous `~/.pi/agent/statusline.json` location, move the
|
|
70
|
+
existing file to the new path before running `/reload`; the old path is no longer read.
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"version": 1,
|
|
75
|
+
"enabled": true,
|
|
76
|
+
"preset": "powerline",
|
|
77
|
+
"icons": "nerd",
|
|
78
|
+
"separator": "powerline",
|
|
79
|
+
"pathMode": "basename",
|
|
80
|
+
"modelDisplay": "name",
|
|
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
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
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
|
|
151
|
+
replaces built-in decorative glyphs, not user-provided names or custom icons.
|
|
152
|
+
Both graphical modes use `` for the model by default; this glyph requires a
|
|
153
|
+
Nerd Font. A custom model icon still overrides the default.
|
|
154
|
+
- Separators: `powerline` (default), `pipe`, `dot`, `space`. ASCII mode uses plain separators.
|
|
155
|
+
- Paths: `basename`, `abbreviated`, `full` (home directory abbreviated as `~`).
|
|
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`.
|
|
160
|
+
- Context display: `bar` (default, `[██░░░░░░░░]20%`) or `text`
|
|
161
|
+
(`12.7k/272k(4.7%)`). Choose **Context display: bar** in the panel or set
|
|
162
|
+
`"contextDisplay": "bar"`.
|
|
163
|
+
The ten-cell bar uses whole blocks to avoid gaps inside partial glyphs. Fill
|
|
164
|
+
rounds to the nearest cell, with at least one filled cell for non-zero usage;
|
|
165
|
+
the percentage retains the more precise value. ASCII mode uses `#` and `-`.
|
|
166
|
+
Unknown usage shows `[??????????]?%`; values above 100% fill
|
|
167
|
+
the bar completely and retain the reported percentage. Both modes use the
|
|
168
|
+
existing context warning colors. Text mode includes token counts and capacity;
|
|
169
|
+
bar mode shows progress and percentage.
|
|
170
|
+
- `segments`: optional ordered array of `{ id, enabled, icon?, color?, iconColor?, background? }`.
|
|
171
|
+
An explicit list is authoritative: omitted segments are disabled but remain
|
|
172
|
+
available in the panel. The `statuses` segment always occupies the second row.
|
|
173
|
+
Preset layouts place context immediately after the model, before directory
|
|
174
|
+
and Git. Explicit saved segment order is respected and can be changed in the panel.
|
|
175
|
+
- Thinking is appended to the model as `(level)`, with no separate icon or
|
|
176
|
+
separator. In the panel, edit **model → Thinking level / Thinking color**.
|
|
177
|
+
The existing `thinking` configuration entry controls suffix enablement and an
|
|
178
|
+
optional color override; its position, icon and background are ignored. It
|
|
179
|
+
does not render on its own when the model is hidden or non-reasoning.
|
|
180
|
+
By default, `off`, `minimal`, `low`, and `medium` use their respective Pi theme
|
|
181
|
+
colors; `high`, `xhigh`, and `max` use a fixed violet/pink/orange/yellow/green/cyan/blue
|
|
182
|
+
palette independent of the Pi theme, distributed across the available characters.
|
|
183
|
+
The color or rainbow covers the entire `(level)`, including parentheses, and
|
|
184
|
+
the model's background covers the whole item.
|
|
185
|
+
- Icon `""` hides a glyph. In the panel, enter `:default` to restore its default.
|
|
186
|
+
Color fields accept `#RRGGBB` or `text`, `accent`, `muted`, `dim`, `success`,
|
|
187
|
+
`warning`, `error`, `border`, `borderAccent`, `borderMuted`, and Pi's
|
|
188
|
+
`thinkingOff/Minimal/Low/Medium/High/Xhigh/Max` tokens. Empty color input in the
|
|
189
|
+
panel restores the default. Explicit colors override automatic warning colors.
|
|
190
|
+
|
|
191
|
+
Available segments:
|
|
192
|
+
|
|
193
|
+
| ID | Data | Default |
|
|
194
|
+
| --- | --- | --- |
|
|
195
|
+
| `model` | Display name/full ID, or final ID component via `modelDisplay` | On |
|
|
196
|
+
| `thinking` | Controls the colored `(level)` suffix inside the model item | On |
|
|
197
|
+
| `context` | Text usage/capacity or a progress bar, followed by percentage | On |
|
|
198
|
+
| `directory` | Current working directory | On |
|
|
199
|
+
| `git` | Branch, clean/dirty/conflict, staged `+`, unstaged `*`, untracked `?`, ahead/behind | On |
|
|
200
|
+
| `cost` | Pi-recorded cumulative USD cost; enable in the panel when wanted | Off |
|
|
201
|
+
| `tokens` | Cumulative total tokens | On |
|
|
202
|
+
| `input`, `output` | Cumulative input or output tokens | Off |
|
|
203
|
+
| `cacheRead`, `cacheWrite` | Cumulative cache token counts | Off |
|
|
204
|
+
| `statuses` | Other extensions' `setStatus()` text on the runtime row, with terminal controls stripped | On |
|
|
205
|
+
|
|
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.
|
|
208
|
+
See [examples/statusline.json](examples/statusline.json) for an explicit layout.
|
|
209
|
+
Unknown presets or versions, duplicate segments, invalid colors and malformed JSON are
|
|
210
|
+
rejected with a notification; reload retains the last working configuration.
|
|
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.
|
|
272
|
+
|
|
273
|
+
## Statistics and refresh behavior
|
|
274
|
+
|
|
275
|
+
Context comes from Pi's `getContextUsage()`. It is distinct from cumulative
|
|
276
|
+
token consumption. Unknown values, including immediately after compaction, show
|
|
277
|
+
`?` until Pi can provide an estimate. Yellow begins above 70%, red above 90%.
|
|
278
|
+
Pi 1.0 bases context estimates on persisted messages; in-flight usage updates
|
|
279
|
+
the cumulative token/cost segments while context refreshes after persistence.
|
|
280
|
+
|
|
281
|
+
Session totals include all usage already recorded in the current session file:
|
|
282
|
+
assistant messages, explicit usage entries, tool-result usage, compaction and
|
|
283
|
+
branch summaries, including pre-compaction and abandoned-branch consumption.
|
|
284
|
+
Streaming responses are provisional and counted once when persisted. The total
|
|
285
|
+
token segment sums input, output, cache-read and cache-write, matching Pi's
|
|
286
|
+
native session totals. Costs are **Pi-reported values, not provider invoices**;
|
|
287
|
+
zero means no cost recorded by Pi. No subscription-quota API or child-session
|
|
288
|
+
log is queried, and private `pi-subagents` result details are not parsed.
|
|
289
|
+
|
|
290
|
+
Git queries run asynchronously every two seconds and after tool/branch changes,
|
|
291
|
+
with a one-second timeout and optional index locks disabled. No repository hides
|
|
292
|
+
the segment; failures show `?`, never a false clean state. Large output above
|
|
293
|
+
8 MiB also falls back to unknown. Worktrees, detached HEAD, unborn branches and
|
|
294
|
+
filenames containing newlines are supported. Disabling the Git segment stops
|
|
295
|
+
its polling.
|
|
296
|
+
|
|
297
|
+
Rendering performs no file reads, Git queries or full-session scans. Streaming
|
|
298
|
+
refresh requests are coalesced to 100 ms. Narrow terminals shorten model/path
|
|
299
|
+
labels, then hide cost, cache/token counters, the thinking suffix, Git and directory in
|
|
300
|
+
that order, retaining model/context where possible. The secondary status row
|
|
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
|
|
304
|
+
release timers and subscriptions. RPC, JSON and print mode do not install UI
|
|
305
|
+
components or start polling.
|
|
306
|
+
|
|
307
|
+
## Development and rollback
|
|
308
|
+
|
|
309
|
+
```sh
|
|
310
|
+
npm run check --workspace @erichll/pi-statusline
|
|
311
|
+
npm test --workspace @erichll/pi-statusline
|
|
312
|
+
# Optional: POSIX PTY integration, using an offline simulated model
|
|
313
|
+
npm run smoke --workspace @erichll/pi-statusline
|
|
314
|
+
```
|
|
315
|
+
|
|
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
|
|
323
|
+
installation, disable this extension in `pi config`, re-enable the previous
|
|
324
|
+
footer and reload Pi. `/statusline off` restores the native footer, not another
|
|
325
|
+
extension's editor or widgets.
|
|
326
|
+
|
|
327
|
+
The initial implementation is independently written, with design references to
|
|
328
|
+
[pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer).
|
|
329
|
+
|
|
330
|
+
MIT licensed.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"preset": "powerline",
|
|
5
|
+
"icons": "nerd",
|
|
6
|
+
"separator": "powerline",
|
|
7
|
+
"pathMode": "basename",
|
|
8
|
+
"modelDisplay": "name",
|
|
9
|
+
"contextDisplay": "bar",
|
|
10
|
+
"segments": [
|
|
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
|
+
}
|
|
71
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-statusline",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Configurable Pi statusline with live preview, Git and session usage",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": ["pi-package", "pi-extension", "pi", "statusline"],
|
|
8
|
+
"repository": {"type": "git", "url": "git+https://github.com/erichll/pi-packages.git", "directory": "packages/pi-statusline"},
|
|
9
|
+
"publishConfig": {"access": "public"},
|
|
10
|
+
"engines": {"node": ">=22.19.0"},
|
|
11
|
+
"files": ["src", "README.md", "CHANGELOG.md", "LICENSE", "examples"],
|
|
12
|
+
"exports": {".": "./src/index.ts"},
|
|
13
|
+
"pi": {"extensions": ["./src/index.ts"]},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
16
|
+
"test": "node --experimental-strip-types --test test/*.test.ts",
|
|
17
|
+
"smoke": "python3 test/pty-smoke.py"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-ai": "*",
|
|
21
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
22
|
+
"@earendil-works/pi-tui": "*"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@earendil-works/pi-ai": "^1.0.0",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "^1.0.0",
|
|
27
|
+
"@earendil-works/pi-tui": "^1.0.0",
|
|
28
|
+
"@types/node": "^26.6.4",
|
|
29
|
+
"typescript": "^7.0.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -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
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { mkdir, open, readFile, rename, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import type { ColorValue, StatuslineConfig } from "./types.ts";
|
|
5
|
+
import { SEGMENT_IDS } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
// A deliberately small, documented set of Pi semantic foreground colors.
|
|
8
|
+
export const COLORS = ["text", "accent", "muted", "dim", "success", "warning", "error", "border", "borderAccent", "borderMuted", "thinkingOff", "thinkingMinimal", "thinkingLow", "thinkingMedium", "thinkingHigh", "thinkingXhigh", "thinkingMax"] as const;
|
|
9
|
+
export function validColor(value: unknown): value is ColorValue {
|
|
10
|
+
return typeof value === "string" && (/^#[\da-f]{6}$/i.test(value) || (COLORS as readonly string[]).includes(value));
|
|
11
|
+
}
|
|
12
|
+
export function presetConfig(): StatuslineConfig {
|
|
13
|
+
const enabled = new Set<string>(["model", "thinking", "directory", "git", "context", "tokens", "statuses"]);
|
|
14
|
+
return {
|
|
15
|
+
version: 1, enabled: true, preset: "powerline", icons: "nerd",
|
|
16
|
+
separator: "powerline", pathMode: "basename", modelDisplay: "name", contextDisplay: "bar",
|
|
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, retainSummary: true },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function choice<T extends string>(value: unknown, choices: readonly T[], name: string): T {
|
|
25
|
+
if (typeof value !== "string" || !choices.includes(value as T)) throw new Error(`Invalid ${name}`);
|
|
26
|
+
return value as T;
|
|
27
|
+
}
|
|
28
|
+
function boolean(value: unknown, name: string): boolean {
|
|
29
|
+
if (typeof value !== "boolean") throw new Error(`Invalid ${name}`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
export function parseConfig(raw: unknown): StatuslineConfig {
|
|
33
|
+
if (!record(raw)) throw new Error("Configuration must be an object");
|
|
34
|
+
if (raw.version !== undefined && raw.version !== 1) throw new Error("Unsupported configuration version");
|
|
35
|
+
if (raw.preset !== undefined) choice(raw.preset, ["powerline"] as const, "preset");
|
|
36
|
+
const config = presetConfig();
|
|
37
|
+
if (raw.enabled !== undefined) config.enabled = boolean(raw.enabled, "enabled");
|
|
38
|
+
if (raw.icons !== undefined) config.icons = choice(raw.icons, ["unicode", "nerd", "ascii"], "icons");
|
|
39
|
+
if (raw.separator !== undefined) config.separator = choice(raw.separator, ["pipe", "dot", "space", "powerline"], "separator");
|
|
40
|
+
if (raw.pathMode !== undefined) config.pathMode = choice(raw.pathMode, ["basename", "abbreviated", "full"], "pathMode");
|
|
41
|
+
if (raw.modelDisplay !== undefined) config.modelDisplay = choice(raw.modelDisplay, ["name", "last"], "modelDisplay");
|
|
42
|
+
if (raw.contextDisplay !== undefined) config.contextDisplay = choice(raw.contextDisplay, ["text", "bar"], "contextDisplay");
|
|
43
|
+
for (const group of ["runtime"] as const) {
|
|
44
|
+
if (raw[group] === undefined) continue;
|
|
45
|
+
if (!record(raw[group])) throw new Error(`Invalid ${group}`);
|
|
46
|
+
for (const key of Object.keys(config[group])) {
|
|
47
|
+
const value = raw[group][key];
|
|
48
|
+
if (value === undefined) continue;
|
|
49
|
+
if (key === "noContentSeconds") {
|
|
50
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > 3600) throw new Error("Invalid runtime.noContentSeconds (1–3600)");
|
|
51
|
+
config.runtime.noContentSeconds = value;
|
|
52
|
+
} else (config[group] as unknown as Record<string, unknown>)[key] = boolean(value, `${group}.${key}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (raw.segments !== undefined) {
|
|
56
|
+
if (!Array.isArray(raw.segments)) throw new Error("segments must be an array");
|
|
57
|
+
const seen = new Set<string>();
|
|
58
|
+
config.segments = raw.segments.map((item: unknown) => {
|
|
59
|
+
if (!record(item)) throw new Error("Invalid segment");
|
|
60
|
+
const id = choice(item.id, SEGMENT_IDS, "segment id");
|
|
61
|
+
if (seen.has(id)) throw new Error(`Duplicate segment: ${id}`);
|
|
62
|
+
seen.add(id);
|
|
63
|
+
const result = { id, enabled: item.enabled === undefined ? true : boolean(item.enabled, `${id}.enabled`) } as StatuslineConfig["segments"][number];
|
|
64
|
+
if (item.icon !== undefined) {
|
|
65
|
+
if (typeof item.icon !== "string" || item.icon.length > 32 || /[\p{Cc}\u202a-\u202e\u2066-\u2069]/u.test(item.icon)) throw new Error(`Invalid icon: ${id}`);
|
|
66
|
+
result.icon = item.icon;
|
|
67
|
+
}
|
|
68
|
+
for (const key of ["color", "iconColor", "background"] as const) {
|
|
69
|
+
if (item[key] !== undefined) {
|
|
70
|
+
if (!validColor(item[key])) throw new Error(`Invalid ${id}.${key}`);
|
|
71
|
+
result[key] = item[key];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
});
|
|
76
|
+
// An explicit list is authoritative. Omitted segments remain editable but disabled.
|
|
77
|
+
for (const id of SEGMENT_IDS) if (!seen.has(id)) config.segments.push({ id, enabled: false });
|
|
78
|
+
}
|
|
79
|
+
return config;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class ConfigStore {
|
|
83
|
+
readonly file: string;
|
|
84
|
+
private lastWarning = "";
|
|
85
|
+
constructor(file: string) { this.file = file; }
|
|
86
|
+
async load(previous = presetConfig()): Promise<{ config: StatuslineConfig; warning?: string }> {
|
|
87
|
+
try {
|
|
88
|
+
const config = parseConfig(JSON.parse(await readFile(this.file, "utf8")));
|
|
89
|
+
this.lastWarning = "";
|
|
90
|
+
return { config };
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
93
|
+
this.lastWarning = "";
|
|
94
|
+
return { config: presetConfig() };
|
|
95
|
+
}
|
|
96
|
+
const warning = `Could not load ${this.file}: ${error instanceof Error ? error.message : String(error)}`;
|
|
97
|
+
const notify = warning !== this.lastWarning;
|
|
98
|
+
this.lastWarning = warning;
|
|
99
|
+
return { config: previous, ...(notify ? { warning } : {}) };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
async save(config: StatuslineConfig): Promise<void> {
|
|
103
|
+
const validated = parseConfig(config);
|
|
104
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
105
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
106
|
+
try {
|
|
107
|
+
const handle = await open(temporary, "wx", 0o600);
|
|
108
|
+
try { await handle.writeFile(`${JSON.stringify(validated, null, 2)}\n`); await handle.sync(); }
|
|
109
|
+
finally { await handle.close(); }
|
|
110
|
+
await rename(temporary, this.file);
|
|
111
|
+
this.lastWarning = "";
|
|
112
|
+
} finally {
|
|
113
|
+
await unlink(temporary).catch((error: NodeJS.ErrnoException) => { if (error.code !== "ENOENT") throw error; });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|