@reedchan/statusline 1.4.0 → 1.6.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 +32 -26
- package/README_CN.md +18 -13
- package/extensions/statusline/index.ts +103 -9
- package/extensions/statusline/render.ts +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Replaces pi's footer with a labelled two-row one. Every value carries a word, so
|
|
|
8
8
|
decoded from a symbol or remembered from a legend.
|
|
9
9
|
|
|
10
10
|
```
|
|
11
|
-
Context █████████▍░░░░░░░░░░ 47
|
|
11
|
+
Context █████████▍░░░░░░░░░░ 47% 471k / 1.0M Input 194k · Output 89k · Cache hit 99.9% · Cost $0.229 · Today $1.63
|
|
12
12
|
~/.pi (master) deepseek-flash · Effort high · TTFT 482ms · 729 tok/s
|
|
13
13
|
LSP Active: typescript
|
|
14
14
|
```
|
|
@@ -19,17 +19,18 @@ columns only the meter is left. Every step is a whole value — a number is neve
|
|
|
19
19
|
|
|
20
20
|
## What each part is
|
|
21
21
|
|
|
22
|
-
| Part
|
|
23
|
-
|
|
|
24
|
-
| `Context` + meter + `47
|
|
25
|
-
| `471k / 1.0M`
|
|
26
|
-
| `Input` / `Output`
|
|
27
|
-
| `Cache hit`
|
|
28
|
-
| `Cost`
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
| `
|
|
32
|
-
|
|
|
22
|
+
| Part | Meaning |
|
|
23
|
+
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `Context` + meter + `47%` | Share of the model's context window in use. The fill turns `warning` above 70% and `error` above 90%, the same thresholds pi's shipped footer uses, and the percentage changes color with it |
|
|
25
|
+
| `471k / 1.0M` | Absolute context tokens over the window size. The first thing dropped when the terminal is narrow |
|
|
26
|
+
| `Input` / `Output` | Session prompt and completion tokens. `Input` counts the prompt tokens that were neither read from nor written to cache, because pi reports those two separately in the same `usage` object |
|
|
27
|
+
| `Cache hit` | The latest turn's cache hit rate, `cacheRead / (input + cacheRead + cacheWrite)` |
|
|
28
|
+
| `Cost` | Session cost, in USD unless a config file names another currency (see below) |
|
|
29
|
+
| `Today` | Today's running cost across every project, session and model on this machine |
|
|
30
|
+
| Effort | The active thinking level |
|
|
31
|
+
| `TTFT` | Time from request dispatch to the first streamed token |
|
|
32
|
+
| `tok/s` | Decode throughput, i.e. output tokens per second of decode time |
|
|
33
|
+
| Last line | Other extensions' `ctx.ui.setStatus()` entries, so they do not silently disappear |
|
|
33
34
|
|
|
34
35
|
## The meter
|
|
35
36
|
|
|
@@ -53,29 +54,34 @@ Configuration lives at the top of [`render.ts`](render.ts):
|
|
|
53
54
|
## Currency
|
|
54
55
|
|
|
55
56
|
pi prices every model in USD and its `cost` field carries no unit at all, so the footer cannot know
|
|
56
|
-
what you were actually billed.
|
|
57
|
-
by a market feed, which is why the rate is configured rather than fetched.
|
|
58
|
-
|
|
59
|
-
Create `~/.pi/agent/statusline.json`:
|
|
57
|
+
what you were actually billed. The currency and the rate live in `~/.pi/agent/statusline.json`:
|
|
60
58
|
|
|
61
59
|
```json
|
|
62
60
|
{
|
|
63
61
|
"currency": {
|
|
64
|
-
"code": "CNY"
|
|
65
|
-
"perUsd": 7.12
|
|
62
|
+
"code": "CNY"
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
65
|
```
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
Two ways to set the rate:
|
|
68
|
+
|
|
69
|
+
- **Follow the market.** A `code` with no rate fetches the day's table from open.er-api.com on the
|
|
70
|
+
first session of each day and caches the whole table in the file (`rates` + `fetchedAt`), so the
|
|
71
|
+
next session starts warm and switching codes is instant and offline.
|
|
72
|
+
- **Pin it.** Add `"perUsd": 7.12` and the footer always uses that rate, never touching the network
|
|
73
|
+
— the right choice when your platform bills in RMB directly, because a domestic price list is not
|
|
74
|
+
the dollar list times a market rate.
|
|
75
|
+
|
|
76
|
+
`code` picks the symbol: `¥` for `CNY` and `RMB`, `JP¥` for `JPY` so the two never trade places, and
|
|
77
|
+
the table also knows `USD`, `EUR`, `GBP`, `HKD`, `TWD`, `SGD`, `KRW`, `INR`, `AUD`, `CAD`, `NZD` and
|
|
78
|
+
`CHF`; an unknown code is printed as it is. `symbol` overrides all of that.
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
Switching is editing `code`, or `make currency CODE=JPY` from a checkout. The file is read once per
|
|
81
|
+
session, so finish with `/reload`. A file that is there but unusable says so in a notification,
|
|
82
|
+
rather than silently showing dollars with nothing to explain why.
|
|
83
|
+
|
|
84
|
+
## Metrics
|
|
79
85
|
|
|
80
86
|
## Metrics
|
|
81
87
|
|
package/README_CN.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
替换 pi 的 footer,改成带文字标签的两行。每个值都带一个词,不需要靠符号猜、也不需要记图例。
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
Context █████████▍░░░░░░░░░░ 47
|
|
10
|
+
Context █████████▍░░░░░░░░░░ 47% 471k / 1.0M Input 194k · Output 89k · Cache hit 99.9% · Cost $0.229 · Today $1.63
|
|
11
11
|
~/.pi (master) deepseek-flash · Effort high · TTFT 482ms · 729 tok/s
|
|
12
12
|
LSP Active: typescript
|
|
13
13
|
```
|
|
@@ -25,6 +25,7 @@ LSP Active: typescript
|
|
|
25
25
|
| `Input` / `Output` | 会话的输入与输出 token 量。`Input` 指既没命中缓存、也没写入缓存的那部分 prompt token,因为 pi 把这两种情况在同一个 `usage` 对象里分开报 |
|
|
26
26
|
| `Cache hit` | 最近一轮的缓存命中率,`cacheRead / (input + cacheRead + cacheWrite)` |
|
|
27
27
|
| `Cost` | 会话花费,默认美元;配置了其他币种则换算显示(见下) |
|
|
28
|
+
| `Today` | 当天跨项目、跨会话、跨模型的累计花费 |
|
|
28
29
|
| Effort | 当前思考等级 |
|
|
29
30
|
| `TTFT` | 从发出请求到第一个流式 token 的耗时 |
|
|
30
31
|
| `tok/s` | 解码吞吐,即每秒解码时间产出的输出 token 数 |
|
|
@@ -51,27 +52,31 @@ LSP Active: typescript
|
|
|
51
52
|
## 币种
|
|
52
53
|
|
|
53
54
|
pi 里所有模型价格都是美元,而且它的 `cost` 字段**不带任何单位**,所以 footer 无从知道你实际是按什么币种付的。
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
创建 `~/.pi/agent/statusline.json`:
|
|
55
|
+
币种与汇率放在 `~/.pi/agent/statusline.json`:
|
|
57
56
|
|
|
58
57
|
```json
|
|
59
58
|
{
|
|
60
59
|
"currency": {
|
|
61
|
-
"code": "CNY"
|
|
62
|
-
"perUsd": 7.12
|
|
60
|
+
"code": "CNY"
|
|
63
61
|
}
|
|
64
62
|
}
|
|
65
63
|
```
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
- `
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
汇率有两种设法:
|
|
66
|
+
|
|
67
|
+
- **跟随市场。** 只写 `code` 不写汇率时,每天第一个会话会从 open.er-api.com 拉取当日汇率表,
|
|
68
|
+
并把整张表缓存进文件(`rates` + `fetchedAt`),下个会话热启动;切换币种即时生效、离线也能切。
|
|
69
|
+
- **钉死。** 加上 `"perUsd": 7.12`,footer 永远用这个汇率、绝不联网——如果你的平台直接按人民币计费,
|
|
70
|
+
这是正确选择:国内价目表不是美元价目表乘以市场汇率。
|
|
71
|
+
|
|
72
|
+
`code` 决定符号:`CNY` 和 `RMB` 得到 `¥`,`JPY` 得到 `JP¥`(两者永不混用),符号表还认 `USD`、
|
|
73
|
+
`EUR`、`GBP`、`HKD`、`TWD`、`SGD`、`KRW`、`INR`、`AUD`、`CAD`、`NZD`、`CHF`;表里没有的代码原样打印。
|
|
74
|
+
`symbol` 可覆盖这一切。
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
切换就是改 `code`,或者在检出里执行 `make currency CODE=JPY`。该文件每个会话只读一次,改完需 `/reload`。
|
|
77
|
+
文件存在但不可用时会给出通知,而不是静默继续显示美元。
|
|
78
|
+
|
|
79
|
+
## 指标口径
|
|
75
80
|
|
|
76
81
|
## 指标口径
|
|
77
82
|
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* stepStartTime; decodeMs = completedTime - firstTokenTime; tok/s = usage.output / (decodeMs / 1000)
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { readFile, writeFile } from 'node:fs/promises'
|
|
21
|
+
import { readFile, readdir, writeFile } from 'node:fs/promises'
|
|
22
|
+
import { stat } from 'node:fs/promises'
|
|
22
23
|
import { homedir } from 'node:os'
|
|
23
24
|
import { join } from 'node:path'
|
|
24
25
|
|
|
@@ -62,6 +63,8 @@ const FALLBACK_TOKENS_PER_CHAR = 0.25
|
|
|
62
63
|
const CONFIG_PATH = join(homedir(), '.pi', 'agent', 'statusline.json')
|
|
63
64
|
/** Free, keyless, and one request returns every currency — so the cache serves instant switching. */
|
|
64
65
|
const RATES_URL = 'https://open.er-api.com/v6/latest/USD'
|
|
66
|
+
/** Every session on this machine, for the day-cost total that spans projects and models. */
|
|
67
|
+
const SESSIONS_DIR = join(homedir(), '.pi', 'agent', 'sessions')
|
|
65
68
|
const FETCH_TIMEOUT_MS = 5000
|
|
66
69
|
|
|
67
70
|
function today(): string {
|
|
@@ -97,6 +100,78 @@ async function fetchRates(): Promise<Record<string, number> | null> {
|
|
|
97
100
|
* Called once per session on purpose: a footer that stat'ed a file on every frame would be its own
|
|
98
101
|
* bug. Editing the file therefore takes effect on `/reload`.
|
|
99
102
|
*/
|
|
103
|
+
function startOfToday(): number {
|
|
104
|
+
const start = new Date()
|
|
105
|
+
start.setHours(0, 0, 0, 0)
|
|
106
|
+
return start.getTime()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
110
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Today's provider cost of one session-file line, or null when the line bills nothing today. */
|
|
114
|
+
function entryCost(line: string, since: number): number | null {
|
|
115
|
+
if (!line.includes('"usage"')) return null
|
|
116
|
+
let entry: unknown
|
|
117
|
+
try {
|
|
118
|
+
entry = JSON.parse(line)
|
|
119
|
+
} catch {
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
if (!isRecord(entry) || typeof entry.timestamp !== 'string') return null
|
|
123
|
+
if (Date.parse(entry.timestamp) < since) return null
|
|
124
|
+
if (!isRecord(entry.usage) || !isRecord(entry.usage.cost)) return null
|
|
125
|
+
const total = entry.usage.cost.total
|
|
126
|
+
return typeof total === 'number' && Number.isFinite(total) ? total : null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Today's cost across every other session on this machine.
|
|
131
|
+
*
|
|
132
|
+
* Files not touched today are skipped unread, and this session's own file is skipped because its
|
|
133
|
+
* cost is already live in totals.cost — counting both would double a resumed session.
|
|
134
|
+
*/
|
|
135
|
+
async function sumOtherTodaysCost(currentFile: string | null, since: number): Promise<number> {
|
|
136
|
+
let dirs: string[] = []
|
|
137
|
+
try {
|
|
138
|
+
dirs = await readdir(SESSIONS_DIR)
|
|
139
|
+
} catch {
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
const lists = await Promise.all(
|
|
143
|
+
dirs.map(async (dir) => {
|
|
144
|
+
const dirPath = join(SESSIONS_DIR, dir)
|
|
145
|
+
let names: string[] = []
|
|
146
|
+
try {
|
|
147
|
+
names = await readdir(dirPath)
|
|
148
|
+
} catch {
|
|
149
|
+
return []
|
|
150
|
+
}
|
|
151
|
+
return names
|
|
152
|
+
.map((name) => join(dirPath, name))
|
|
153
|
+
.filter((path) => path.endsWith('.jsonl') && path !== currentFile)
|
|
154
|
+
}),
|
|
155
|
+
)
|
|
156
|
+
const costs = await Promise.all(
|
|
157
|
+
lists.flat().map(async (path) => {
|
|
158
|
+
try {
|
|
159
|
+
if ((await stat(path)).mtimeMs < since) return 0
|
|
160
|
+
} catch {
|
|
161
|
+
return 0
|
|
162
|
+
}
|
|
163
|
+
const text = await readFile(path, 'utf8').catch(() => '')
|
|
164
|
+
let total = 0
|
|
165
|
+
for (const line of text.split('\n')) {
|
|
166
|
+
const cost = entryCost(line, since)
|
|
167
|
+
if (cost !== null) total += cost
|
|
168
|
+
}
|
|
169
|
+
return total
|
|
170
|
+
}),
|
|
171
|
+
)
|
|
172
|
+
return costs.reduce((sum, value) => sum + value, 0)
|
|
173
|
+
}
|
|
174
|
+
|
|
100
175
|
async function loadCurrency(notify: (message: string) => void): Promise<Currency> {
|
|
101
176
|
let text: string | null = null
|
|
102
177
|
try {
|
|
@@ -167,6 +242,7 @@ interface Totals {
|
|
|
167
242
|
cacheWrite: number
|
|
168
243
|
cost: number
|
|
169
244
|
cacheHitRate: number | null
|
|
245
|
+
todayCost: number
|
|
170
246
|
}
|
|
171
247
|
|
|
172
248
|
/** The usage fields this footer totals, read structurally so no cast is needed. */
|
|
@@ -178,7 +254,7 @@ type UsageTotals = {
|
|
|
178
254
|
cost: { total: number }
|
|
179
255
|
}
|
|
180
256
|
|
|
181
|
-
function collectTotals(ctx: ExtensionContext): Totals {
|
|
257
|
+
function collectTotals(ctx: ExtensionContext, since: number): Totals {
|
|
182
258
|
const totals: Totals = {
|
|
183
259
|
input: 0,
|
|
184
260
|
output: 0,
|
|
@@ -186,29 +262,38 @@ function collectTotals(ctx: ExtensionContext): Totals {
|
|
|
186
262
|
cacheWrite: 0,
|
|
187
263
|
cost: 0,
|
|
188
264
|
cacheHitRate: null,
|
|
265
|
+
todayCost: 0,
|
|
189
266
|
}
|
|
190
267
|
// `usage` stays optional here even though an assistant message always carries it:
|
|
191
268
|
// a truncated or hand-edited session file is the case this guard is for.
|
|
192
|
-
const add = (usage: UsageTotals | undefined, assistant: boolean): void => {
|
|
269
|
+
const add = (usage: UsageTotals | undefined, assistant: boolean, isToday: boolean): void => {
|
|
193
270
|
if (!usage) return
|
|
194
271
|
totals.input += usage.input
|
|
195
272
|
totals.output += usage.output
|
|
196
273
|
totals.cacheRead += usage.cacheRead
|
|
197
274
|
totals.cacheWrite += usage.cacheWrite
|
|
198
275
|
totals.cost += usage.cost.total
|
|
276
|
+
if (isToday) totals.todayCost += usage.cost.total
|
|
199
277
|
if (!assistant) return
|
|
200
278
|
const prompt = usage.input + usage.cacheRead + usage.cacheWrite
|
|
201
279
|
if (prompt > 0) totals.cacheHitRate = (usage.cacheRead / prompt) * 100
|
|
202
280
|
}
|
|
203
281
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
282
|
+
const stamp =
|
|
283
|
+
'timestamp' in entry && typeof entry.timestamp === 'string'
|
|
284
|
+
? Date.parse(entry.timestamp)
|
|
285
|
+
: Number.NaN
|
|
286
|
+
const isToday = Number.isFinite(stamp) && stamp >= since
|
|
204
287
|
if (entry.type === 'message') {
|
|
205
288
|
const { message } = entry
|
|
206
|
-
if (message.role === 'assistant') add(message.usage, true)
|
|
207
|
-
else if (message.role === 'toolResult') add(message.usage, false)
|
|
289
|
+
if (message.role === 'assistant') add(message.usage, true, isToday)
|
|
290
|
+
else if (message.role === 'toolResult') add(message.usage, false, isToday)
|
|
208
291
|
continue
|
|
209
292
|
}
|
|
210
293
|
// Compaction and branch summaries bill a model call of their own.
|
|
211
|
-
if (entry.type === 'compaction' || entry.type === 'branch_summary')
|
|
294
|
+
if (entry.type === 'compaction' || entry.type === 'branch_summary') {
|
|
295
|
+
add(entry.usage, false, isToday)
|
|
296
|
+
}
|
|
212
297
|
}
|
|
213
298
|
return totals
|
|
214
299
|
}
|
|
@@ -240,6 +325,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
325
|
let requestAt: number | null = null
|
|
241
326
|
let firstTokenAt: number | null = null
|
|
242
327
|
let totalDecodeMs = 0
|
|
328
|
+
let totalMeasuredOutput = 0
|
|
329
|
+
let todayBase = 0
|
|
243
330
|
let ticker: ReturnType<typeof setInterval> | null = null
|
|
244
331
|
let windowAt = 0
|
|
245
332
|
let windowTokens = 0
|
|
@@ -289,8 +376,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
289
376
|
},
|
|
290
377
|
render(width: number): string[] {
|
|
291
378
|
const usage = ctx.getContextUsage()
|
|
292
|
-
const totals = collectTotals(ctx)
|
|
293
|
-
const avg = avgTokPerSec(
|
|
379
|
+
const totals = collectTotals(ctx, startOfToday())
|
|
380
|
+
const avg = avgTokPerSec(totalMeasuredOutput, totalDecodeMs)
|
|
294
381
|
const row1 = contextRow(
|
|
295
382
|
theme,
|
|
296
383
|
width,
|
|
@@ -302,6 +389,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
302
389
|
output: totals.output,
|
|
303
390
|
cacheHitRate: totals.cacheHitRate,
|
|
304
391
|
cost: totals.cost,
|
|
392
|
+
todayCost: todayBase + totals.todayCost,
|
|
305
393
|
},
|
|
306
394
|
currency,
|
|
307
395
|
)
|
|
@@ -388,9 +476,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
388
476
|
reading = null
|
|
389
477
|
requestAt = null
|
|
390
478
|
totalDecodeMs = 0
|
|
479
|
+
totalMeasuredOutput = 0
|
|
391
480
|
stopTicker()
|
|
392
481
|
resetStream()
|
|
393
482
|
currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
|
|
483
|
+
const file = ctx.sessionManager.getSessionFile()
|
|
484
|
+
todayBase = await sumOtherTodaysCost(file ?? null, startOfToday())
|
|
394
485
|
installFooter(ctx)
|
|
395
486
|
})
|
|
396
487
|
|
|
@@ -470,8 +561,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
470
561
|
const measured = ttftMs(requestAt, firstTokenAt)
|
|
471
562
|
const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
|
|
472
563
|
if (measured !== null && decodeMs >= MIN_SAMPLE_MS) {
|
|
473
|
-
//
|
|
564
|
+
// The average's numerator and denominator must cover the same messages: totals.output spans
|
|
565
|
+
// the whole session (a reload replays none of it), so pairing it with the partial denominator
|
|
566
|
+
// below once produced an Avg of 4324 tok/s.
|
|
474
567
|
totalDecodeMs += decodeMs
|
|
568
|
+
if (output > 0) totalMeasuredOutput += output
|
|
475
569
|
publish((tokens / decodeMs) * 1000, output > 0, measured)
|
|
476
570
|
}
|
|
477
571
|
// Null it with the stream: a request that has produced its message is no longer in flight, and
|
|
@@ -143,13 +143,18 @@ export const USD: Currency = { symbol: '$', perUsd: 1 }
|
|
|
143
143
|
|
|
144
144
|
/** Currencies the config can name by `code` alone. Anything else needs an explicit `symbol`. */
|
|
145
145
|
const CURRENCY_SYMBOLS: Record<string, string> = {
|
|
146
|
+
AUD: 'A$',
|
|
147
|
+
CAD: 'CA$',
|
|
148
|
+
CHF: 'CHF',
|
|
146
149
|
CNY: '¥',
|
|
147
150
|
EUR: '€',
|
|
148
151
|
GBP: '£',
|
|
149
152
|
HKD: 'HK$',
|
|
150
153
|
INR: '₹',
|
|
151
|
-
|
|
154
|
+
// ¥ is CNY's by default; JPY wears the prefixed form so the two never trade places.
|
|
155
|
+
JPY: 'JP¥',
|
|
152
156
|
KRW: '₩',
|
|
157
|
+
NZD: 'NZ$',
|
|
153
158
|
RMB: '¥',
|
|
154
159
|
SGD: 'S$',
|
|
155
160
|
TWD: 'NT$',
|
|
@@ -392,6 +397,8 @@ export interface ContextRowParts {
|
|
|
392
397
|
output: number
|
|
393
398
|
cacheHitRate: number | null
|
|
394
399
|
cost: number
|
|
400
|
+
/** Today's spend across every session on this machine, or 0 to hide the slot. */
|
|
401
|
+
todayCost: number
|
|
395
402
|
}
|
|
396
403
|
|
|
397
404
|
/**
|
|
@@ -416,7 +423,8 @@ export function contextRow(
|
|
|
416
423
|
const fraction = parts.percent === null ? 0 : parts.percent / 100
|
|
417
424
|
const percent = theme.fg(
|
|
418
425
|
percentColor(parts.percent),
|
|
419
|
-
|
|
426
|
+
// Whole percents: the meter carries the precision, and a decimal here is noise.
|
|
427
|
+
parts.percent === null ? '?' : `${Math.round(parts.percent)}%`,
|
|
420
428
|
)
|
|
421
429
|
const meter = `${theme.fg('dim', 'Context')} ${bar(theme, BAR_CELLS, fraction)} ${percent}`
|
|
422
430
|
const detail =
|
|
@@ -432,6 +440,9 @@ export function contextRow(
|
|
|
432
440
|
outcomes.push(pair(theme, 'Cache hit', `${parts.cacheHitRate.toFixed(1)}%`))
|
|
433
441
|
}
|
|
434
442
|
if (parts.cost > 0) outcomes.push(pair(theme, 'Cost', formatCost(parts.cost, currency)))
|
|
443
|
+
if (parts.todayCost > 0) {
|
|
444
|
+
outcomes.push(pair(theme, 'Today', formatCost(parts.todayCost, currency)))
|
|
445
|
+
}
|
|
435
446
|
|
|
436
447
|
const separator = theme.fg('dim', ' · ')
|
|
437
448
|
const full = [...volumes, ...outcomes].join(separator)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reedchan/statusline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Replaces pi's footer with a labelled two-row status line: context pressure as a fixed-size meter, cache and cost, the model and effort level, and the latest turn's TTFT and decode throughput in tokens/second.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|