@hank-warren/pi-statusline 0.7.2 → 0.8.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 +26 -0
- package/README.md +166 -1
- package/cache-celebration.ts +3 -3
- package/celebration-preview.ts +1 -1
- package/celebration-styles.ts +3 -3
- package/custom-setup.ts +41 -0
- package/custom.ts +486 -0
- package/index.ts +91 -1
- package/package.json +4 -2
- package/redraw.ts +1 -1
- package/settings-menu.ts +177 -1
- package/settings.ts +46 -12
- package/usage.ts +9 -6
- package/worktrees.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @hank-warren/pi-statusline
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1035138: Add custom items: user-defined command segments rendered after the usage meters.
|
|
8
|
+
|
|
9
|
+
Each entry in the new `customItems` setting runs a shell command, hands it JSON
|
|
10
|
+
about the session on stdin, and renders the first line of its stdout as a
|
|
11
|
+
statusline segment — the same contract Claude Code's `statusLine` uses, so a
|
|
12
|
+
script written for it ports over. Items refresh at session start, at each turn
|
|
13
|
+
end, and on an optional `refreshInterval` timer; one run per item at a time,
|
|
14
|
+
with a timeout, a failure grace period before a stale value is dropped, and
|
|
15
|
+
output sanitized to SGR colors so a command cannot corrupt the frame.
|
|
16
|
+
|
|
17
|
+
`/statusline` gains a **Custom items** toggle and a **Custom item list** submenu
|
|
18
|
+
that enables or disables each item and shows why one is not rendering. Commands
|
|
19
|
+
stay in the settings file, and entries this version cannot parse — an unknown
|
|
20
|
+
`type`, an unrecognised key — are preserved verbatim on write rather than
|
|
21
|
+
dropped, so toggling an unrelated setting can never delete a configured item.
|
|
22
|
+
|
|
23
|
+
## 0.7.3
|
|
24
|
+
|
|
25
|
+
### Patch Changes
|
|
26
|
+
|
|
27
|
+
- d0c46a5: Resolve `auth.json`, the usage cache and `statusline-settings.json` through Pi's agent directory (`PI_CODING_AGENT_DIR`) instead of a hardcoded `~/.pi/agent`. Behaviour is unchanged for a normal install; a session started with a scratch agent dir no longer reads the real credentials or writes the real usage cache. `engines.node` now states Pi's own floor, `>=22.19.0`.
|
|
28
|
+
|
|
3
29
|
## 0.7.2
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ gpt-5.6-sol | pi-extensions:main* ⇣1 | 40k/1.0m | 97·54 80
|
|
|
10
10
|
|
|
11
11
|
## What it shows
|
|
12
12
|
|
|
13
|
-
- **Line 1** — active model ID, optionally the provider of that model, current directory basename and Git branch, current context usage/window,
|
|
13
|
+
- **Line 1** — active model ID, optionally the provider of that model, current directory basename and Git branch, current context usage/window, subscription usage headroom (see below), and any [custom items](#custom-items) you configure. A yellow `*` marks a dirty checkout and `⇣N` shows how many commits it is behind its locally known upstream ref. Unknown context usage is rendered as `?/<window>` until Pi can provide an estimate. Exceptional prompt-cache hits trigger the celebration described below.
|
|
14
14
|
- **Worktree lines** — when the session works in or sends tool calls into linked worktrees, one line shows the same branch/dirty/behind state for each worktree plus its associated PR number.
|
|
15
15
|
- **Final line** — the full Pi session ID.
|
|
16
16
|
|
|
@@ -22,6 +22,7 @@ Colors come from a selectable [theme](#themes), with context warning thresholds.
|
|
|
22
22
|
|
|
23
23
|
- **Theme** — the color palette, cycled with Enter or Space. See [Themes](#themes).
|
|
24
24
|
- **Cache celebration** — `off` or one of five badge animations, cycled with Enter or Space and previewed live in the statusline below. See [Animation styles](#animation-styles).
|
|
25
|
+
- **Custom item list** — below the alias row: enables or disables each configured item, shows what it is currently doing, and **Add custom item…** hands the job to the agent. A **Custom items** `on`/`off` row for the whole segment appears once at least one item is configured. See [Custom items](#custom-items).
|
|
25
26
|
- **Model**, **Provider**, **Directory & git**, **Context**, **Subscription usage**, **Worktree line**, **Session ID line** — `on`/`off`, cycled with Enter or Space. **Provider** is the only one that starts `off`; it shows the provider id exactly as Pi reports it, so a [pi-multi-login](../pi-multi-login) alias renders as `anthropic-team` and names the login actually spending — something a model id like `claude-opus-5` never carries. With no model, or a model reporting no provider, the segment is simply absent. Disabled segments are dropped from line 1 without leaving a stray ` | ` separator; hiding the worktree line also stops its `git`/`gh` polling, and hiding usage stops the usage poller. With every element off the footer collapses to a single blank row.
|
|
26
27
|
- **Worktree root** — the directory whose immediate children are tracked as session worktrees (default `~/repos/worktrees`). `~` and `$HOME` are expanded; a relative path is rejected and the previous value kept.
|
|
27
28
|
- **Repo aliases** — short display names for repositories on the worktree line. Enter edits the selected `repo → alias` pair, `d` deletes it, and `Add alias…` creates one from a `repo=alias` line.
|
|
@@ -101,6 +102,166 @@ An account answering `429` is parked for fifteen minutes (tracked per account, s
|
|
|
101
102
|
|
|
102
103
|
Requires a Nerd Font new enough to include the codicon brand glyphs (v3.5.0+); older fonts render them as replacement boxes.
|
|
103
104
|
|
|
105
|
+
## Custom items
|
|
106
|
+
|
|
107
|
+
Everything above is built in. **Custom items** are the escape hatch: each one runs a shell command, and its output becomes a segment on line 1, after the usage meters and before the worktree line. This is how a personal metric — a self-hosted quota pool, a deploy status, an on-call flag — gets onto the statusline without being packaged for everybody else.
|
|
108
|
+
|
|
109
|
+
The contract is deliberately [Claude Code's status line](https://docs.claude.com/en/docs/claude-code/statusline) contract: a command, JSON about the session on **stdin**, one line on **stdout**, ANSI colors passed through. A script written for Claude Code runs here mostly unchanged (see [differences](#differences-from-claude-codes-status-line)).
|
|
110
|
+
|
|
111
|
+
There is no default item, and with an empty list the feature costs nothing: no process is spawned and no timer runs.
|
|
112
|
+
|
|
113
|
+
### Adding one
|
|
114
|
+
|
|
115
|
+
The quickest way is `/statusline` → **Custom item list** → **Add custom item…**. Type one line describing what the item should show (or press Enter and let the agent ask), and the menu closes and sends the agent a message carrying the whole contract below plus the settings path. The agent writes the script, tests it the way the statusline will run it, adds the entry, and tells you to reopen `/statusline`. That message is the only place the contract is injected — it costs nothing until you ask for an item, which is why this package ships no skill for it.
|
|
116
|
+
|
|
117
|
+
By hand, items live under `customItems` in `~/.pi/agent/statusline-settings.json`. The file is not created for you until a setting is changed, so write it if it is absent:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"customItems": [
|
|
122
|
+
{
|
|
123
|
+
"id": "cpa",
|
|
124
|
+
"command": "~/bin/cpa-quota --statusline",
|
|
125
|
+
"refreshInterval": 60,
|
|
126
|
+
"timeout": 5
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
| Field | Required | Meaning |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| `command` | yes | Shell command line. Run through `sh -c` (`cmd /d /s /c` on Windows), so pipes, `$VARS`, and `~` work. |
|
|
135
|
+
| `id` | no | Stable name, used by the `/statusline` submenu and in error messages. Defaults to `item-1`, `item-2`, …; duplicates get a `#2` suffix. |
|
|
136
|
+
| `refreshInterval` | no | Seconds between forced re-runs, on top of the event-driven ones. Omit for event-driven only. |
|
|
137
|
+
| `timeout` | no | Seconds before the command is killed. Default `5`, capped at `30`. |
|
|
138
|
+
| `enabled` | no | `false` hides the item and stops it running. This is the one field `/statusline` writes. |
|
|
139
|
+
| `type` | no | Accepted for entries pasted from Claude Code, where it is `"command"`. Any other value is preserved but not run. |
|
|
140
|
+
|
|
141
|
+
Items render in configuration order, each as its own ` | `-separated segment.
|
|
142
|
+
|
|
143
|
+
**The file is read when a session starts and whenever `/statusline` opens.** After editing it by hand, open `/statusline` and press Esc to load the change into the running session; nothing watches the file.
|
|
144
|
+
|
|
145
|
+
### When a command runs
|
|
146
|
+
|
|
147
|
+
- at session start,
|
|
148
|
+
- at the end of every turn,
|
|
149
|
+
- every `refreshInterval` seconds, if set.
|
|
150
|
+
|
|
151
|
+
A run is skipped while that item's previous run is still going, so a slow command degrades to a lower refresh rate instead of piling up processes. Two runs of the same item are never closer than one second, whatever triggers them. The timer only exists if some enabled item asked for one, ticks at the shortest interval among them, and is `unref`ed and stopped with the footer — turning **Custom items** off in `/statusline` stops all of it.
|
|
152
|
+
|
|
153
|
+
Use `refreshInterval` for anything whose value moves on a wall clock rather than on your turns: a quota pool refills while you are reading a diff, and an idle session would otherwise show the number from your last turn.
|
|
154
|
+
|
|
155
|
+
### What the command receives
|
|
156
|
+
|
|
157
|
+
One JSON object on stdin, and `COLUMNS` in the environment (the footer's current width, exactly as Claude Code provides it):
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"version": 1,
|
|
162
|
+
"session_id": "019fafa7-29c0-7e99-9f82-5794d5721848",
|
|
163
|
+
"cwd": "/home/hank/repos/pi-extensions",
|
|
164
|
+
"model": { "id": "gpt-5.6-sol", "provider": "openai-codex" },
|
|
165
|
+
"git": { "branch": "main", "dirty": false, "behind": 0 },
|
|
166
|
+
"context_window": { "used_tokens": 40000, "context_window_size": 1000000, "used_percentage": 4 },
|
|
167
|
+
"usage_remaining": {
|
|
168
|
+
"claude": { "five_hour": 97, "seven_day": 54, "scoped_weekly": 24 },
|
|
169
|
+
"codex": { "five_hour": 92, "weekly": 99 }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`git` is `null` outside a repository, `usage_remaining.claude` / `.codex` are `null` without credentials for that provider, and `context_window.used_tokens` is `null` before Pi can estimate it. Handle absence rather than assuming a field.
|
|
175
|
+
|
|
176
|
+
### What the command should print
|
|
177
|
+
|
|
178
|
+
The **first line of stdout** becomes the segment. Anything after it is ignored — this is a segment on a shared line, not a row the item owns.
|
|
179
|
+
|
|
180
|
+
- **Colors work.** SGR escapes (`\033[32m`) pass through. Every other escape sequence is stripped, because a cursor move or an erase-line would corrupt the frame the footer is drawn into. Control characters go too, and tabs become spaces.
|
|
181
|
+
- **Print nothing to hide.** Empty output is a valid answer, not a failure: it is how an item shows itself only when it has something to say.
|
|
182
|
+
- **Output is capped** at 120 characters before the statusline's own truncation.
|
|
183
|
+
- **Exit non-zero to signal failure.** The first line of stderr is kept and shown in `/statusline`.
|
|
184
|
+
|
|
185
|
+
### When a command fails
|
|
186
|
+
|
|
187
|
+
Failures never reach the agent — the statusline is best-effort and stays silent. A failing item keeps its last good value for up to three consecutive failures, then drops it. That grace is deliberate in both directions: one blip (a laptop between networks) should not blank a working display, and a value that has quietly gone stale is worse than an empty slot, because the number stays plausible while describing a world that has moved on.
|
|
188
|
+
|
|
189
|
+
To see what an item is doing, open `/statusline` → **Custom item list**. Each row shows its current value, or why there isn't one: `disabled`, `missing command`, `exit 3: …`, `timed out after 5s`, `empty output`, or `no value yet`. Enter toggles an item on or off; commands themselves are edited in the file.
|
|
190
|
+
|
|
191
|
+
### Keep it fast
|
|
192
|
+
|
|
193
|
+
The command runs on the footer's schedule, so treat it like a prompt segment. Do slow work elsewhere — a systemd timer, a cron job, a background daemon — and let the item read the result:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{ "id": "quota", "command": "cat /run/user/1000/quota.txt 2>/dev/null", "refreshInterval": 30 }
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If the item must do the work itself, cache it keyed by `session_id` from the payload (a PID changes every run and defeats the cache).
|
|
200
|
+
|
|
201
|
+
### Examples
|
|
202
|
+
|
|
203
|
+
A clock, the smallest possible item:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "id": "clock", "command": "date +%H:%M", "refreshInterval": 30 }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Kubernetes context, colored, hidden when unset:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{ "id": "k8s", "command": "kubectl config current-context 2>/dev/null | sed 's/.*/\\x1b[35m&\\x1b[0m/'", "refreshInterval": 300 }
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Pooled subscription headroom across several accounts behind a self-hosted gateway — the case this feature was built for, where the built-in meters cannot help because they read *this* session's credential, not a round-robin pool:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
#!/usr/bin/env bash
|
|
219
|
+
# ~/bin/cpa-quota --statusline
|
|
220
|
+
curl -sf -m 3 -H "Authorization: Bearer $CPAMP_ADMIN_KEY" \
|
|
221
|
+
"$CPAMP_URL/v0/management/auth-files" |
|
|
222
|
+
jq -r '[.files[] | select(.disabled != true) | .quota.signals]
|
|
223
|
+
| map(select(."X-Codex-Primary-Used-Percent"))
|
|
224
|
+
| if length == 0 then empty else
|
|
225
|
+
"\u001b[36mcpa\u001b[0m " +
|
|
226
|
+
(map(100 - (."X-Codex-Primary-Used-Percent"|tonumber)) | add / length | floor | tostring) + "/" +
|
|
227
|
+
(map(100 - (."X-Codex-Secondary-Used-Percent"|tonumber)) | add / length | floor | tostring)
|
|
228
|
+
end'
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Using the session payload — warn only when this session's model is on a nearly exhausted account:
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{ "id": "low", "command": "jq -r '.usage_remaining.codex.five_hour // 100 | if . < 15 then \"\\u001b[31mLOW \\(.)%\\u001b[0m\" else empty end'" }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Testing an item
|
|
238
|
+
|
|
239
|
+
The command is an ordinary program, so run it the way the statusline does:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
echo '{"model":{"id":"gpt-5.6-sol"},"usage_remaining":{"codex":{"five_hour":22,"weekly":55}}}' \
|
|
243
|
+
| COLUMNS=120 sh -c '~/bin/cpa-quota --statusline'
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
If that prints one short line, the item will render.
|
|
247
|
+
|
|
248
|
+
### Differences from Claude Code's status line
|
|
249
|
+
|
|
250
|
+
| | Claude Code | pi-statusline |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| Scope | one command owns the whole status line | many items, each a segment after the built-in ones |
|
|
253
|
+
| Config | `statusLine` object in `settings.json` | `customItems` array in `statusline-settings.json` |
|
|
254
|
+
| Multi-line output | each line becomes a row | only the first line is used |
|
|
255
|
+
| Rate-limit fields | `rate_limits.*.used_percentage` | `usage_remaining.*` — **remaining**, the inverse |
|
|
256
|
+
| Refresh | every assistant message, 300 ms debounce | session start, turn end, optional `refreshInterval` |
|
|
257
|
+
| Width | `COLUMNS` and `LINES` | `COLUMNS` |
|
|
258
|
+
|
|
259
|
+
The naming of `usage_remaining` is the one difference worth checking when porting: reading a *remaining* percentage as a *used* one silently inverts the meaning, and a green bar that means "nearly out" is worse than no bar.
|
|
260
|
+
|
|
261
|
+
### A note on trust
|
|
262
|
+
|
|
263
|
+
An item is a command that runs automatically in every TUI session, so `customItems` is executable configuration, exactly like Claude Code's `statusLine` or a shell rc file. Write there is code execution: keep `~/.pi/agent/statusline-settings.json` under your own account (Pi writes it `0600`), and treat an item copied from the internet with the same suspicion as a shell script from the internet.
|
|
264
|
+
|
|
104
265
|
## Cache-hit celebration
|
|
105
266
|
|
|
106
267
|
Whenever one assistant response reaches a prompt-cache hit rate of at least 96%, a temporary module is appended after context usage for about two seconds:
|
|
@@ -168,6 +329,10 @@ Both repair layers are skipped entirely in regular TUI mode, which reprints its
|
|
|
168
329
|
pi install npm:@hank-warren/pi-statusline
|
|
169
330
|
```
|
|
170
331
|
|
|
332
|
+
## Changelog
|
|
333
|
+
|
|
334
|
+
See [CHANGELOG.md](CHANGELOG.md) for release history.
|
|
335
|
+
|
|
171
336
|
## License
|
|
172
337
|
|
|
173
338
|
MIT — see [LICENSE](LICENSE).
|
package/cache-celebration.ts
CHANGED
|
@@ -2,7 +2,7 @@ export const CACHE_HIT_THRESHOLD = 0.96;
|
|
|
2
2
|
export const CACHE_CELEBRATION_FRAME_INTERVAL_MS = 60;
|
|
3
3
|
export const CACHE_CELEBRATION_DURATION_MS = 2_000;
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface CacheUsage {
|
|
6
6
|
input: number;
|
|
7
7
|
cacheRead: number;
|
|
8
8
|
cacheWrite: number;
|
|
@@ -13,11 +13,11 @@ export interface CacheCelebrationSnapshot {
|
|
|
13
13
|
frame: number;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface CacheCelebrationTarget {
|
|
17
17
|
start(percent: number): void;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
interface CacheCelebrationControllerOptions {
|
|
21
21
|
frameIntervalMs?: number;
|
|
22
22
|
durationMs?: number;
|
|
23
23
|
now?: () => number;
|
package/celebration-preview.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { SettingsListTheme } from "@earendil-works/pi-tui";
|
|
|
7
7
|
/** Percentage shown by the settings-menu preview badge. */
|
|
8
8
|
export const PREVIEW_PERCENT = 96;
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
interface CelebrationPreviewOptions {
|
|
11
11
|
frameIntervalMs?: number;
|
|
12
12
|
schedule?: (callback: () => void, intervalMs: number) => unknown;
|
|
13
13
|
cancel?: (handle: unknown) => void;
|
package/celebration-styles.ts
CHANGED
|
@@ -11,13 +11,13 @@ import type { StatuslinePalette } from "./themes.ts";
|
|
|
11
11
|
const RESET = "\x1b[0m";
|
|
12
12
|
const BOLD = "\x1b[1m";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
interface Rgb {
|
|
15
15
|
r: number;
|
|
16
16
|
g: number;
|
|
17
17
|
b: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const fgCode = ({ r, g, b }: Rgb): string =>
|
|
21
21
|
`\x1b[38;2;${Math.round(r)};${Math.round(g)};${Math.round(b)}m`;
|
|
22
22
|
|
|
23
23
|
/** Parse an SGR true-colour prefix back into channels; anything else reads as white. */
|
|
@@ -68,7 +68,7 @@ function hue(degrees: number): Rgb {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/** Colour every character of `badge` for one frame. */
|
|
71
|
-
|
|
71
|
+
type CelebrationStyle = (badge: string, frame: number, palette: StatuslinePalette) => string;
|
|
72
72
|
|
|
73
73
|
const uniform = (badge: string, colour: Rgb): string => `${BOLD}${fgCode(colour)}${badge}${RESET}`;
|
|
74
74
|
|
package/custom-setup.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DEFAULT_TIMEOUT_MS, MAX_OUTPUT_WIDTH, MAX_TIMEOUT_MS } from "./custom.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The message `/statusline` → Custom item list → "Add custom item…" sends on the
|
|
5
|
+
* user's behalf.
|
|
6
|
+
*
|
|
7
|
+
* This is how the contract reaches the agent. It is not a skill on purpose: a
|
|
8
|
+
* skill's description line sits in every system prompt of every session with
|
|
9
|
+
* the statusline loaded, to serve a task most users do once. Injecting the
|
|
10
|
+
* contract from the menu costs nothing until someone asks for an item, and it
|
|
11
|
+
* is discoverable where the feature is — the same shape Claude Code's own
|
|
12
|
+
* `/statusline` takes.
|
|
13
|
+
*
|
|
14
|
+
* Everything the agent needs is in the message, because the README's install
|
|
15
|
+
* path is not guessable from inside a session (`~/.pi/agent/npm/node_modules/…`
|
|
16
|
+
* for an npm install, a git checkout for a git one).
|
|
17
|
+
*/
|
|
18
|
+
export function buildCustomItemSetupPrompt(settingsPath: string, request: string): string {
|
|
19
|
+
const want = request.trim();
|
|
20
|
+
const ask =
|
|
21
|
+
want.length > 0
|
|
22
|
+
? `I want a custom statusline item that shows: ${want}`
|
|
23
|
+
: "I want to add a custom statusline item. Ask me what it should show, then set it up.";
|
|
24
|
+
return [
|
|
25
|
+
ask,
|
|
26
|
+
"",
|
|
27
|
+
"Set it up for pi-statusline. The contract is Claude Code's `statusLine` contract, per item:",
|
|
28
|
+
"",
|
|
29
|
+
// An instruction, not a fact: given "items live in <path>", a weaker model
|
|
30
|
+
// in a canary treated the path as a hint and went looking for the "real"
|
|
31
|
+
// file with `find /`. The path is the one this session is actually using.
|
|
32
|
+
`- Write the entry to exactly \`${settingsPath}\` under \`customItems\` (an array). That is the file this session reads; do not search for or edit any other settings file. Create the file or the key if absent, and preserve everything else in it.`,
|
|
33
|
+
"- Each entry: `{ \"id\": \"<short-name>\", \"command\": \"<shell line>\", \"refreshInterval\": <seconds, optional>, \"timeout\": <seconds, optional> }`. The command runs through `sh -c`, so pipes, `$VARS`, and `~` work.",
|
|
34
|
+
"- The command receives one JSON object on **stdin** and `COLUMNS` in its environment. Fields: `session_id`, `cwd`, `model.id`, `model.provider`, `git` (`{branch, dirty, behind}` or `null`), `context_window` (`{used_tokens, context_window_size, used_percentage}`, `used_tokens` may be `null`), `usage_remaining.claude` (`{five_hour, seven_day, scoped_weekly}` or `null`) and `usage_remaining.codex` (`{five_hour, weekly}` or `null`). Those percentages are **remaining** headroom, not used.",
|
|
35
|
+
`- The **first line of stdout** becomes the segment, capped at ${MAX_OUTPUT_WIDTH} characters. SGR colour escapes pass through; every other escape sequence is stripped. **Empty output hides the item** — it is a valid answer, not a failure. Exit non-zero to report a failure; the first line of stderr is shown in \`/statusline\`.`,
|
|
36
|
+
"- It runs at session start, at the end of every turn, and every `refreshInterval` seconds if set. Use `refreshInterval` for values that move on a wall clock rather than on turns. One run per item at a time; a slow command degrades to a lower refresh rate.",
|
|
37
|
+
`- Default timeout ${DEFAULT_TIMEOUT_MS / 1000}s, maximum ${MAX_TIMEOUT_MS / 1000}s. Keep it fast: do slow work in a cron job or daemon and have the item read the result; if the item must do the work itself, cache keyed by \`session_id\`.`,
|
|
38
|
+
"",
|
|
39
|
+
"Steps: write the script if one is needed (make it executable), test it exactly as the statusline runs it — `echo '{\"model\":{\"id\":\"x\"},\"usage_remaining\":{\"codex\":{\"five_hour\":22,\"weekly\":55}}}' | COLUMNS=120 sh -c '<command>'` — and confirm it prints one short line. Then add the entry to the settings file. When done, tell me to run `/statusline` to reload it; the file is read when that menu opens, and the **Custom item list** submenu shows each item's value or why it is not rendering.",
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|