@narumitw/pi-statusline 0.35.0 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,14 +8,15 @@ without setup and keeps useful context visible as the terminal narrows.
8
8
  A representative uncolored layout:
9
9
 
10
10
  ```text
11
- ░▒▓ 🤖 sonnet-4 🧠 high 📁 pi-extensions 🌿 main ~2 🪟 ctx 42% 💸 $0.184
11
+ ░▒▓ 🤖 sonnet-4 🧠 high 📁 pi-extensions 🌿 main ~2 🪟 ctx 42.0%/200k 🕒 16:42
12
12
  ```
13
13
 
14
14
  ## ✨ Features
15
15
 
16
- - **Zero-config default:** model, thinking, workspace, Git/PR state, context use, and cost.
16
+ - **Zero-config default:** model, thinking, workspace, Git/PR state, context use, and local time.
17
17
  - **Responsive:** removes lower-priority segments before important information gets clipped.
18
18
  - **Quiet when idle:** activity appears only while Pi is streaming or running tools.
19
+ - **Native-aligned usage:** optional token, prompt-cache, subscription, and cost details.
19
20
  - **Easy choices:** three information levels and seven previewable color presets.
20
21
  - **Still flexible:** custom layouts, multiline rows, colors, labels, separators, and status icons stay
21
22
  available under **Advanced**.
@@ -73,11 +74,12 @@ preserved.
73
74
  | Level | Included segments |
74
75
  | --- | --- |
75
76
  | **Minimal** | `model cwd branch context` |
76
- | **Balanced** (default) | `model thinking cwd branch tools context cost` |
77
- | **Detailed** | `provider model thinking cwd branch tools context tokens cost time` |
77
+ | **Balanced** (default) | `model thinking cwd branch tools context time` |
78
+ | **Detailed** | `provider model thinking cwd branch tools context tokens cache cost time` |
78
79
  | **Custom** | Any other segment order, including explicit line breaks |
79
80
 
80
- The `tools` segment takes no space while idle.
81
+ The `tools` segment takes no space while idle. `cache` takes no space when Pi has reported no cache
82
+ reads or writes.
81
83
 
82
84
  ## 💬 Commands
83
85
 
@@ -100,7 +102,7 @@ lowest-priority segment, recomputes the powerline transitions, and repeats until
100
102
  Retention priority is highest to lowest:
101
103
 
102
104
  ```text
103
- context model branch tools cwd thinking cost provider tokens time turn brand
105
+ context model branch tools cwd thinking cost provider cache tokens time turn brand
104
106
  ```
105
107
 
106
108
  Explicit `line_break` entries remain row boundaries. A single segment that is still too wide is
@@ -117,6 +119,20 @@ ANSI-safely truncated.
117
119
  - Context color changes to warning at 70% and error at 90%.
118
120
  - Git state is cached outside footer rendering and stale session results are ignored.
119
121
 
122
+ ### Usage and context
123
+
124
+ - `context` renders one-decimal current usage and the model window, such as `2.4%/272k`. After
125
+ compaction it can temporarily render `?/272k` until the next valid assistant response.
126
+ - `tokens`, `cache`, and `cost` total every usage-bearing session entry, matching Pi's native footer:
127
+ assistant messages, nested-LLM tool results, compactions, and branch summaries, including abandoned
128
+ branches retained in the session.
129
+ - Cache tokens are `R<read>`, `W<write>`, and `CH<rate>`. `R` and `W` are cumulative; `CH` uses only
130
+ the latest assistant prompt: `cacheRead / (input + cacheRead + cacheWrite) * 100`.
131
+ - Subscription-backed OAuth models and `kimi-coding` append `(sub)` to cost. The dollar value is
132
+ usage cost, not proof of an amount billed under a subscription.
133
+ - Pi's public extension API does not expose the current auto-compaction toggle, so this footer cannot
134
+ reliably show the native `(auto)` marker.
135
+
120
136
  ## ⚙️ Settings
121
137
 
122
138
  The extension uses one user-level file:
@@ -156,7 +172,7 @@ A compact customization example:
156
172
  "palettePreset": "ocean",
157
173
  "density": "compact",
158
174
  "separator": "dot",
159
- "segments": ["model", "thinking", "cwd", "branch", "context", "cost"],
175
+ "segments": ["model", "thinking", "cwd", "branch", "context", "cache", "cost"],
160
176
  "segmentText": {
161
177
  "context": { "prefix": "ctx ", "suffix": "" }
162
178
  },
@@ -218,7 +234,7 @@ Every successful change saves and applies immediately; closing the screen does n
218
234
  Available data segments:
219
235
 
220
236
  ```text
221
- brand provider model thinking cwd branch tools context tokens cost time turn
237
+ brand provider model thinking cwd branch tools context tokens cache cost time turn
222
238
  ```
223
239
 
224
240
  Data segments must be unique. `line_break` may repeat when data segments separate occurrences, but
@@ -278,6 +294,7 @@ extensions/pi-statusline/
278
294
  │ ├── index.ts
279
295
  │ ├── statusline.ts
280
296
  │ ├── render.ts
297
+ │ ├── usage.ts
281
298
  │ ├── powerline.ts
282
299
  │ ├── information-profiles.ts
283
300
  │ ├── commands.ts
@@ -299,7 +316,7 @@ extensions/pi-statusline/
299
316
  ## 🔎 Keywords
300
317
 
301
318
  Pi extension, Pi coding agent, statusline, Tokyo Night, powerline, responsive terminal footer,
302
- context usage, model status.
319
+ context usage, prompt cache, cache hit rate, model status.
303
320
 
304
321
  ## 📄 License
305
322
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-statusline",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "Pi extension that replaces the footer with an information-rich statusline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/commands.ts CHANGED
@@ -51,6 +51,7 @@ const SEGMENT_DESCRIPTIONS: Record<SegmentName, string> = {
51
51
  tools: "Current tool and streaming activity",
52
52
  context: "Current context-window usage",
53
53
  tokens: "Session token totals",
54
+ cache: "Prompt-cache reads, writes, and latest hit rate",
54
55
  cost: "Session cost",
55
56
  time: "Current local time",
56
57
  turn: "Current session turn count",
@@ -8,7 +8,7 @@ export const INFORMATION_PROFILES: Readonly<
8
8
  Record<InformationProfileName, readonly SegmentName[]>
9
9
  > = {
10
10
  minimal: ["model", "cwd", "branch", "context"],
11
- balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "cost"],
11
+ balanced: ["model", "thinking", "cwd", "branch", "tools", "context", "time"],
12
12
  detailed: [
13
13
  "provider",
14
14
  "model",
@@ -18,6 +18,7 @@ export const INFORMATION_PROFILES: Readonly<
18
18
  "tools",
19
19
  "context",
20
20
  "tokens",
21
+ "cache",
21
22
  "cost",
22
23
  "time",
23
24
  ],
package/src/powerline.ts CHANGED
@@ -49,6 +49,7 @@ const SEGMENT_RETENTION_PRIORITY: Readonly<Record<RenderSegment["name"], number>
49
49
  thinking: 70,
50
50
  cost: 60,
51
51
  provider: 50,
52
+ cache: 45,
52
53
  tokens: 40,
53
54
  time: 30,
54
55
  turn: 20,
@@ -37,6 +37,7 @@ const SEGMENT_BLOCKS: Record<SegmentName, PowerlineBlockName> = {
37
37
  tools: "runtime",
38
38
  context: "runtime",
39
39
  tokens: "runtime",
40
+ cache: "runtime",
40
41
  cost: "meter",
41
42
  time: "meter",
42
43
  turn: "meter",
package/src/render.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  type SegmentName,
22
22
  type StatuslineConfig,
23
23
  } from "./types.js";
24
+ import { type FooterUsageSummary, summarizeFooterUsage } from "./usage.js";
24
25
 
25
26
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
26
27
  export interface RuntimeState extends ExtensionStatusRuntime {
@@ -31,11 +32,6 @@ export interface RuntimeState extends ExtensionStatusRuntime {
31
32
  gitStatus?: GitStatusSummary;
32
33
  requestRender?: () => void;
33
34
  }
34
- interface TokenTotals {
35
- input: number;
36
- output: number;
37
- cost: number;
38
- }
39
35
  const GITHUB_PR_KEY = "github-pr";
40
36
  const GITHUB_PR_STATUS_KEYS = new Set([GITHUB_PR_KEY]);
41
37
  export function renderStatusline(
@@ -48,6 +44,7 @@ export function renderStatusline(
48
44
  ): string {
49
45
  if (width <= 0) return "";
50
46
 
47
+ const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
51
48
  const rows: Array<{ configuredSegments: number; segments: RenderSegment[] }> = [
52
49
  { configuredSegments: 0, segments: [] },
53
50
  ];
@@ -60,7 +57,7 @@ export function renderStatusline(
60
57
  const row = rows.at(-1);
61
58
  if (!row) continue;
62
59
  row.configuredSegments += 1;
63
- const rendered = buildSegment(name, ctx, footerData, config, runtime);
60
+ const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
64
61
  if (rendered && rendered.text.length > 0) row.segments.push(rendered);
65
62
  }
66
63
 
@@ -103,6 +100,7 @@ function buildSegment(
103
100
  footerData: ReadonlyFooterDataProvider,
104
101
  config: StatuslineConfig,
105
102
  runtime: RuntimeState,
103
+ usageSummary: FooterUsageSummary,
106
104
  ): RenderSegment | undefined {
107
105
  switch (name) {
108
106
  case "brand":
@@ -138,25 +136,41 @@ function buildSegment(
138
136
  }
139
137
  case "context": {
140
138
  const usage = ctx.getContextUsage();
141
- const value =
139
+ const percentage =
142
140
  usage?.percent === null || usage?.percent === undefined
143
141
  ? "?"
144
- : `${usage.percent.toFixed(0)}%`;
145
- return segment(name, value, config, contextColor(usage?.percent), "runtime");
142
+ : `${usage.percent.toFixed(1)}%`;
143
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
144
+ return segment(
145
+ name,
146
+ `${percentage}/${formatCount(contextWindow)}`,
147
+ config,
148
+ contextColor(usage?.percent),
149
+ "runtime",
150
+ );
146
151
  }
147
152
  case "tokens": {
148
- const totals = getTokenTotals(ctx);
149
153
  const value =
150
- totals.input === 0 && totals.output === 0
154
+ usageSummary.input === 0 && usageSummary.output === 0
151
155
  ? "tok 0"
152
- : `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
156
+ : `↑${formatCount(usageSummary.input)} ↓${formatCount(usageSummary.output)}`;
153
157
  return segment(name, value, config, "accent", "runtime");
154
158
  }
159
+ case "cache": {
160
+ if (usageSummary.cacheRead === 0 && usageSummary.cacheWrite === 0) return undefined;
161
+ const values: string[] = [];
162
+ if (usageSummary.cacheRead > 0) values.push(`R${formatCount(usageSummary.cacheRead)}`);
163
+ if (usageSummary.cacheWrite > 0) values.push(`W${formatCount(usageSummary.cacheWrite)}`);
164
+ if (usageSummary.latestCacheHitRate !== undefined) {
165
+ values.push(`CH${usageSummary.latestCacheHitRate.toFixed(1)}%`);
166
+ }
167
+ return segment(name, values.join(" "), config, "accent", "runtime");
168
+ }
155
169
  case "cost": {
156
- const totals = getTokenTotals(ctx);
170
+ const subscription = isSubscriptionBacked(ctx) ? " (sub)" : "";
157
171
  return segment(
158
172
  name,
159
- totals.cost.toFixed(totals.cost >= 1 ? 2 : 3),
173
+ `${usageSummary.cost.toFixed(usageSummary.cost >= 1 ? 2 : 3)}${subscription}`,
160
174
  config,
161
175
  "accent",
162
176
  "meter",
@@ -268,26 +282,12 @@ function compactPrState(value: string): string | undefined {
268
282
  return undefined;
269
283
  }
270
284
 
271
- function getTokenTotals(ctx: ExtensionContext): TokenTotals {
272
- const totals: TokenTotals = { input: 0, output: 0, cost: 0 };
273
-
274
- for (const entry of ctx.sessionManager.getBranch()) {
275
- if (entry.type !== "message" || entry.message.role !== "assistant") continue;
276
-
277
- const usage = entry.message.usage as
278
- | {
279
- input?: number;
280
- output?: number;
281
- cost?: { total?: number };
282
- }
283
- | undefined;
284
-
285
- totals.input += usage?.input ?? 0;
286
- totals.output += usage?.output ?? 0;
287
- totals.cost += usage?.cost?.total ?? 0;
288
- }
289
-
290
- return totals;
285
+ function isSubscriptionBacked(ctx: ExtensionContext): boolean {
286
+ const model = ctx.model;
287
+ return (
288
+ model !== undefined &&
289
+ (model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(model))
290
+ );
291
291
  }
292
292
 
293
293
  export function formatCount(value: number): string {
package/src/settings.ts CHANGED
@@ -67,6 +67,7 @@ export const DEFAULT_STATUSLINE_CONFIG: StatuslineConfig = {
67
67
  tools: { prefix: "", suffix: "" },
68
68
  context: { prefix: "🪟 ctx ", suffix: "" },
69
69
  tokens: { prefix: "🔢 ", suffix: "" },
70
+ cache: { prefix: "📦 ", suffix: "" },
70
71
  cost: { prefix: "💸 $", suffix: "" },
71
72
  time: { prefix: "🕒 ", suffix: "" },
72
73
  turn: { prefix: "🔁 #", suffix: "" },
package/src/types.ts CHANGED
@@ -10,6 +10,7 @@ export const SEGMENT_NAMES = [
10
10
  "tools",
11
11
  "context",
12
12
  "tokens",
13
+ "cache",
13
14
  "cost",
14
15
  "time",
15
16
  "turn",
package/src/usage.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
+
3
+ interface UsageLike {
4
+ input?: number;
5
+ output?: number;
6
+ cacheRead?: number;
7
+ cacheWrite?: number;
8
+ cost?: { total?: number };
9
+ }
10
+
11
+ export interface FooterUsageSummary {
12
+ input: number;
13
+ output: number;
14
+ cacheRead: number;
15
+ cacheWrite: number;
16
+ cost: number;
17
+ latestCacheHitRate?: number;
18
+ }
19
+
20
+ export function summarizeFooterUsage(entries: readonly SessionEntry[]): FooterUsageSummary {
21
+ const totals: FooterUsageSummary = {
22
+ input: 0,
23
+ output: 0,
24
+ cacheRead: 0,
25
+ cacheWrite: 0,
26
+ cost: 0,
27
+ };
28
+
29
+ for (const entry of entries) {
30
+ let usage: UsageLike | undefined;
31
+ if (entry.type === "message" && entry.message.role === "assistant") {
32
+ usage = entry.message.usage;
33
+ const input = usage.input ?? 0;
34
+ const cacheRead = usage.cacheRead ?? 0;
35
+ const cacheWrite = usage.cacheWrite ?? 0;
36
+ const promptTokens = input + cacheRead + cacheWrite;
37
+ totals.latestCacheHitRate = promptTokens > 0 ? (cacheRead / promptTokens) * 100 : undefined;
38
+ } else if (entry.type === "message" && entry.message.role === "toolResult") {
39
+ usage = entry.message.usage;
40
+ } else if (entry.type === "compaction" || entry.type === "branch_summary") {
41
+ usage = entry.usage;
42
+ }
43
+ if (!usage) continue;
44
+
45
+ totals.input += usage.input ?? 0;
46
+ totals.output += usage.output ?? 0;
47
+ totals.cacheRead += usage.cacheRead ?? 0;
48
+ totals.cacheWrite += usage.cacheWrite ?? 0;
49
+ totals.cost += usage.cost?.total ?? 0;
50
+ }
51
+
52
+ return totals;
53
+ }