@hank-warren/pi-statusline 0.5.0 → 0.7.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 +16 -0
- package/README.md +5 -3
- package/index.ts +14 -2
- package/package.json +1 -1
- package/settings-menu.ts +1 -0
- package/settings.ts +3 -0
- package/themes.ts +7 -0
- package/usage.ts +38 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @hank-warren/pi-statusline
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 70c8ef9: Add an optional provider segment. A new `Provider` toggle in `/statusline` renders the active model's provider id between the model and the directory (`claude-opus-5 | anthropic-team | pi-extensions:main | 0/1.0m`), so a `pi-multi-login` alias names the login actually spending — something a model id never carries. It is off by default, has its own colour role in every theme, and is omitted entirely when the model reports no provider.
|
|
8
|
+
|
|
9
|
+
## 0.6.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 01994c7: Show the Codex 5-hour window alongside the weekly one. Codex has restored a 5-hour limit next to the weekly one on some plans, and the parser already received that window and deliberately discarded it — it kept only the largest window spanning at least a day — so a real limit went unshown. Both are now rendered like the Claude meter: 5-hour, dim separator, weekly.
|
|
14
|
+
|
|
15
|
+
Which windows exist is a property of the account, so the two slots are filled from each window's span rather than from the plan name: the shortest sub-day window fills the 5-hour slot and the longest multi-day window fills the other. A plan with no 5-hour limit keeps rendering its single weekly number, and a free plan's lone 30-day window keeps rendering in the same place. A payload carrying only a sub-day window now reports it as the 5-hour value instead of mislabelling it as weekly.
|
|
16
|
+
|
|
17
|
+
The host-wide usage cache changes additively: an entry written by an earlier version has no 5-hour value and renders weekly-only until the next poll, and an earlier version ignores the extra one.
|
|
18
|
+
|
|
3
19
|
## 0.5.0
|
|
4
20
|
|
|
5
21
|
### Minor 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, current directory basename and Git branch, current context usage/window, and subscription usage headroom (see below). 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.
|
|
13
|
+
- **Line 1** — active model ID, optionally the provider of that model, current directory basename and Git branch, current context usage/window, and subscription usage headroom (see below). 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,7 +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
|
-
- **Model**, **Directory & git**, **Context**, **Subscription usage**, **Worktree line**, **Session ID line** — `on`/`off`, cycled with Enter or Space. 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.
|
|
25
|
+
- **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
26
|
- **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
27
|
- **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.
|
|
28
28
|
|
|
@@ -71,7 +71,7 @@ Up to 0.2.x this package hardcoded five alias pairs and stripped a `platform-` p
|
|
|
71
71
|
When Pi's `~/.pi/agent/auth.json` contains OAuth credentials for Anthropic (Claude subscription) and/or OpenAI Codex, line 1 shows **percent remaining** for each rate-limit window after the context meter:
|
|
72
72
|
|
|
73
73
|
- ` 97·54` — Claude 5-hour, then weekly remaining percent (Nerd Font `nf-cod-claude` icon). Subscriptions with a model-scoped weekly limit (e.g. Fable) show it as a third number — ` 97·54·24` — and it is omitted when the account has none.
|
|
74
|
-
- `
|
|
74
|
+
- ` 92·99` — Codex 5-hour, then weekly remaining percent (`nf-cod-openai` icon). Which windows exist is a property of the plan, so the slots are filled from the payload's window spans rather than from its plan name: a plan with no 5-hour limit shows the weekly number alone — ` 45` — and a free plan, whose single window is monthly, shows that one number in the same place.
|
|
75
75
|
|
|
76
76
|
Numbers are colored by remaining headroom: green above 60, yellow 41–60, orange 16–40, red at 15 and below.
|
|
77
77
|
|
|
@@ -87,6 +87,8 @@ When [`@hank-warren/pi-multi-login`](../pi-multi-login/README.md) has registered
|
|
|
87
87
|
|
|
88
88
|
Polling is host-wide, not per-session. Usage percentages describe the account rather than the session, and a busy machine runs dozens of pi processes, so every process shares `~/.pi/agent/statusline-usage.json`, written atomically via a temp file and rename. It is keyed by **credential id**, one entry per account, each holding that account's last good values plus the time its last poll was *started*. A session adopts the cached values for its selected accounts on first refresh — so the meters are populated before it has issued a single request, and switching back to an account polled earlier repaints with no request at all — and only polls an account whose timestamp is older than the interval. Keying by account rather than by provider family is what lets two sessions on two different Anthropic logins coexist: keyed by family, each looked like an account switch to the other, so they evicted each other's values and re-polled every cycle.
|
|
89
89
|
|
|
90
|
+
Cache entries stay compatible in both directions: a Codex entry written by a statusline older than 0.6.0 carries no 5-hour value, so a newer one renders it as a single weekly number until its own next poll fills the second slot, and an older reader ignores the extra value entirely.
|
|
91
|
+
|
|
90
92
|
An account answering `429` is parked for fifteen minutes (tracked per account, so a rate-limited Anthropic never stops codex from updating) and stops counting as pending, since retrying harder is what earns the rate limit in the first place. Every cache entry is keyed to a fingerprint (a sha256 prefix, never the token itself) of the credential that fetched it: switching accounts — or rotating a token — discards that entry's numbers and backoff and polls immediately, so an exhausted old account's meters never masquerade as the new account's. A logged-out account fails the same check, so the file garbage-collects itself.
|
|
91
93
|
|
|
92
94
|
Requires a Nerd Font new enough to include the codicon brand glyphs (v3.5.0+); older fonts render them as replacement boxes.
|
package/index.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
|
|
41
41
|
export interface StatuslineData {
|
|
42
42
|
model: string;
|
|
43
|
+
/** Provider id of the active model; absent when there is no model. */
|
|
44
|
+
provider?: string;
|
|
43
45
|
cwd: string;
|
|
44
46
|
cwdGit: GitRepositoryStatus | null;
|
|
45
47
|
contextTokens: number | null;
|
|
@@ -71,15 +73,21 @@ export function renderUsageSegment(
|
|
|
71
73
|
palette: StatuslinePalette = DEFAULT_PALETTE,
|
|
72
74
|
): string | undefined {
|
|
73
75
|
const percent = (remaining: number) => styled(bandColor(remaining, palette), `${remaining}`);
|
|
76
|
+
const dot = styled(palette.dim, "\u00b7");
|
|
74
77
|
const parts: string[] = [];
|
|
75
78
|
if (usage.claude) {
|
|
76
|
-
const dot = styled(palette.dim, "\u00b7");
|
|
77
79
|
let claude = `${percent(usage.claude.fiveHour)}${dot}${percent(usage.claude.sevenDay)}`;
|
|
78
80
|
if (usage.claude.scopedWeekly !== undefined) claude += `${dot}${percent(usage.claude.scopedWeekly)}`;
|
|
79
81
|
parts.push(`${styled(palette.text, CLAUDE_ICON)} ${claude}`);
|
|
80
82
|
}
|
|
81
83
|
if (usage.codex) {
|
|
82
|
-
|
|
84
|
+
// Shortest window first, mirroring Claude. Plans without a 5-hour limit
|
|
85
|
+
// leave that slot empty and keep rendering the single long-window number.
|
|
86
|
+
const codex = [usage.codex.fiveHour, usage.codex.weekly]
|
|
87
|
+
.filter((remaining): remaining is number => remaining !== undefined)
|
|
88
|
+
.map(percent)
|
|
89
|
+
.join(dot);
|
|
90
|
+
if (codex.length > 0) parts.push(`${styled(palette.text, OPENAI_ICON)} ${codex}`);
|
|
83
91
|
}
|
|
84
92
|
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
85
93
|
}
|
|
@@ -171,6 +179,9 @@ export function renderStatusline(
|
|
|
171
179
|
const usageSegment = settings.showUsage && data.usage ? renderUsageSegment(data.usage, palette) : undefined;
|
|
172
180
|
const segments = [
|
|
173
181
|
settings.showModel ? styled(palette.model, data.model) : undefined,
|
|
182
|
+
// No provider is a missing segment, not a placeholder: the model id already
|
|
183
|
+
// says "no-model" in that state, and a second one would only add noise.
|
|
184
|
+
settings.showProvider && data.provider ? styled(palette.provider, data.provider) : undefined,
|
|
174
185
|
settings.showDirectory
|
|
175
186
|
? data.cwdGit
|
|
176
187
|
? renderRepository(data.cwd, data.cwdGit, palette)
|
|
@@ -418,6 +429,7 @@ export default function statuslineExtension(pi: ExtensionAPI): void {
|
|
|
418
429
|
renderStatusline(
|
|
419
430
|
{
|
|
420
431
|
model,
|
|
432
|
+
provider: ctx.model?.provider,
|
|
421
433
|
cwd,
|
|
422
434
|
cwdGit,
|
|
423
435
|
contextTokens: usage?.tokens ?? null,
|
package/package.json
CHANGED
package/settings-menu.ts
CHANGED
|
@@ -32,6 +32,7 @@ export interface BooleanRow {
|
|
|
32
32
|
/** Toggle rows, in statusline render order. */
|
|
33
33
|
export const BOOLEAN_ROWS: readonly BooleanRow[] = [
|
|
34
34
|
{ id: "showModel", label: "Model", description: "Show the active model id." },
|
|
35
|
+
{ id: "showProvider", label: "Provider", description: "Show the provider of the active model." },
|
|
35
36
|
{ id: "showDirectory", label: "Directory & git", description: "Show the working directory and its git branch." },
|
|
36
37
|
{ id: "showContext", label: "Context", description: "Show context tokens used against the window." },
|
|
37
38
|
{ id: "showUsage", label: "Subscription usage", description: "Show Claude/Codex remaining-headroom meters." },
|
package/settings.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { DEFAULT_THEME, isThemeName, type StatuslineThemeName } from "./themes.t
|
|
|
12
12
|
/** Toggle keys, in the order the `/statusline` menu lists them. */
|
|
13
13
|
export const BOOLEAN_SETTING_KEYS = [
|
|
14
14
|
"showModel",
|
|
15
|
+
"showProvider",
|
|
15
16
|
"showDirectory",
|
|
16
17
|
"showContext",
|
|
17
18
|
"showUsage",
|
|
@@ -40,6 +41,8 @@ export function defaultWorktreeRoot(home: string = homedir()): string {
|
|
|
40
41
|
export function defaultSettings(home: string = homedir()): StatuslineSettings {
|
|
41
42
|
return {
|
|
42
43
|
showModel: true,
|
|
44
|
+
// Opt-in: the provider is redundant for anyone with a single login per family.
|
|
45
|
+
showProvider: false,
|
|
43
46
|
showDirectory: true,
|
|
44
47
|
showContext: true,
|
|
45
48
|
showUsage: true,
|
package/themes.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export interface StatuslinePalette {
|
|
6
6
|
/** Active model id. */
|
|
7
7
|
model: string;
|
|
8
|
+
/** Provider id of the active model. */
|
|
9
|
+
provider: string;
|
|
8
10
|
/** Repository and directory names. */
|
|
9
11
|
path: string;
|
|
10
12
|
/** Git branch names. */
|
|
@@ -35,6 +37,7 @@ const rgb = (hex: string): string => {
|
|
|
35
37
|
/** The palette this package shipped before themes existed. */
|
|
36
38
|
const DEFAULT: StatuslinePalette = {
|
|
37
39
|
model: rgb("#0099ff"),
|
|
40
|
+
provider: rgb("#7aa2c8"),
|
|
38
41
|
path: rgb("#dcdcdc"),
|
|
39
42
|
branch: rgb("#56b6c2"),
|
|
40
43
|
text: rgb("#dcdcdc"),
|
|
@@ -50,6 +53,7 @@ const DEFAULT: StatuslinePalette = {
|
|
|
50
53
|
/** Dracula, with the pink branch colour from Hank's Herdr sidebar config. */
|
|
51
54
|
const DRACULA: StatuslinePalette = {
|
|
52
55
|
model: rgb("#bd93f9"),
|
|
56
|
+
provider: rgb("#9580c9"),
|
|
53
57
|
path: rgb("#f8f8f2"),
|
|
54
58
|
branch: rgb("#ff79c6"),
|
|
55
59
|
text: rgb("#f8f8f2"),
|
|
@@ -64,6 +68,7 @@ const DRACULA: StatuslinePalette = {
|
|
|
64
68
|
|
|
65
69
|
const GITHUB_DARK: StatuslinePalette = {
|
|
66
70
|
model: rgb("#58a6ff"),
|
|
71
|
+
provider: rgb("#6e8bb5"),
|
|
67
72
|
path: rgb("#c9d1d9"),
|
|
68
73
|
branch: rgb("#39c5cf"),
|
|
69
74
|
text: rgb("#c9d1d9"),
|
|
@@ -78,6 +83,7 @@ const GITHUB_DARK: StatuslinePalette = {
|
|
|
78
83
|
|
|
79
84
|
const CATPPUCCIN_MOCHA: StatuslinePalette = {
|
|
80
85
|
model: rgb("#cba6f7"),
|
|
86
|
+
provider: rgb("#a58fc4"),
|
|
81
87
|
path: rgb("#cdd6f4"),
|
|
82
88
|
branch: rgb("#89dceb"),
|
|
83
89
|
text: rgb("#cdd6f4"),
|
|
@@ -93,6 +99,7 @@ const CATPPUCCIN_MOCHA: StatuslinePalette = {
|
|
|
93
99
|
/** No colour at all: white text, dimmed punctuation, a grey badge flash. */
|
|
94
100
|
const WHITE: StatuslinePalette = {
|
|
95
101
|
model: rgb("#ffffff"),
|
|
102
|
+
provider: rgb("#ffffff"),
|
|
96
103
|
path: rgb("#ffffff"),
|
|
97
104
|
branch: rgb("#ffffff"),
|
|
98
105
|
text: rgb("#ffffff"),
|
package/usage.ts
CHANGED
|
@@ -7,7 +7,12 @@ import { pid } from "node:process";
|
|
|
7
7
|
/** Remaining (not used) integer percents per provider window. */
|
|
8
8
|
export interface UsageSnapshot {
|
|
9
9
|
claude?: { fiveHour: number; sevenDay: number; scopedWeekly?: number };
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Codex reports one or two windows depending on the plan: a short (5-hour)
|
|
12
|
+
* window, a long one, or both. Every field is optional because which of them
|
|
13
|
+
* exists is a property of the account, not of the payload shape.
|
|
14
|
+
*/
|
|
15
|
+
codex?: { fiveHour?: number; weekly?: number };
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
const CLAUDE_USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
@@ -79,7 +84,19 @@ interface CodexWindow {
|
|
|
79
84
|
limit_window_seconds?: unknown;
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
/**
|
|
87
|
+
/** A window whose span is known, so it can be sorted into a slot. */
|
|
88
|
+
interface SpannedCodexWindow extends CodexWindow {
|
|
89
|
+
limit_window_seconds: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Parse the Codex usage payload into the short (sub-day) and long (≥ 1 day)
|
|
94
|
+
* windows. Which ones exist depends on the plan — $20 plans report a 5-hour
|
|
95
|
+
* window alongside the weekly one, larger plans have reported weekly only, and
|
|
96
|
+
* free plans report a single 30-day window that lands in the long slot. So the
|
|
97
|
+
* slots are filled from `limit_window_seconds` rather than from `plan_type`,
|
|
98
|
+
* and an absent window simply leaves its slot empty.
|
|
99
|
+
*/
|
|
83
100
|
export function parseCodexUsage(json: unknown): UsageSnapshot["codex"] | undefined {
|
|
84
101
|
if (typeof json !== "object" || json === null) return undefined;
|
|
85
102
|
const rateLimit = (json as { rate_limit?: unknown }).rate_limit;
|
|
@@ -88,14 +105,27 @@ export function parseCodexUsage(json: unknown): UsageSnapshot["codex"] | undefin
|
|
|
88
105
|
primary_window?: CodexWindow | null;
|
|
89
106
|
secondary_window?: CodexWindow | null;
|
|
90
107
|
};
|
|
108
|
+
// A window without a usable span cannot be placed in either slot.
|
|
91
109
|
const windows = [primary_window, secondary_window].filter(
|
|
92
|
-
(window): window is
|
|
110
|
+
(window): window is SpannedCodexWindow =>
|
|
111
|
+
typeof window === "object" &&
|
|
112
|
+
window !== null &&
|
|
113
|
+
typeof window.limit_window_seconds === "number" &&
|
|
114
|
+
Number.isFinite(window.limit_window_seconds),
|
|
93
115
|
);
|
|
94
|
-
const
|
|
95
|
-
.filter((window) =>
|
|
96
|
-
.sort((a, b) =>
|
|
97
|
-
const
|
|
98
|
-
|
|
116
|
+
const shortest = windows
|
|
117
|
+
.filter((window) => window.limit_window_seconds < ONE_DAY_SECONDS)
|
|
118
|
+
.sort((a, b) => a.limit_window_seconds - b.limit_window_seconds)[0];
|
|
119
|
+
const longest = windows
|
|
120
|
+
.filter((window) => window.limit_window_seconds >= ONE_DAY_SECONDS)
|
|
121
|
+
.sort((a, b) => b.limit_window_seconds - a.limit_window_seconds)[0];
|
|
122
|
+
const fiveHour = toRemaining(shortest?.used_percent);
|
|
123
|
+
const weekly = toRemaining(longest?.used_percent);
|
|
124
|
+
if (fiveHour === undefined && weekly === undefined) return undefined;
|
|
125
|
+
const result: UsageSnapshot["codex"] = {};
|
|
126
|
+
if (fiveHour !== undefined) result.fiveHour = fiveHour;
|
|
127
|
+
if (weekly !== undefined) result.weekly = weekly;
|
|
128
|
+
return result;
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
/** Color band for a remaining percent: >60 green, >40 yellow, >15 orange, else red. */
|