@reedchan/statusline 1.5.0 → 1.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/README.md CHANGED
@@ -8,8 +8,8 @@ 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.1% 471k / 1.0M Input 194k · Output 89k · Cache hit 99.9% · Cost $0.229
12
- ~/.pi (master) deepseek-flash · Effort high · TTFT 482ms · 729 tok/s
11
+ Context █████████▍░░░░░░░░░░ 47% 471k / 1.0M Input 194k · Output 89k | Cache hit 99.9% | Cost $0.229 · Today $1.63
12
+ ~/.pi (master) deepseek-flash · Effort high | TTFT 482ms · Avg TTFT 612ms | Last 729 tok/s · Avg 512 tok/s
13
13
  LSP Active: typescript
14
14
  ```
15
15
 
@@ -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 | Meaning |
23
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
24
- | `Context` + meter + `47.1%` | 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
- | Effort | The active thinking level |
30
- | `TTFT` | Time from request dispatch to the first streamed token |
31
- | `tok/s` | Decode throughput, i.e. output tokens per second of decode time |
32
- | Last line | Other extensions' `ctx.ui.setStatus()` entries, so they do not silently disappear |
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
 
package/README_CN.md CHANGED
@@ -7,8 +7,8 @@
7
7
  替换 pi 的 footer,改成带文字标签的两行。每个值都带一个词,不需要靠符号猜、也不需要记图例。
8
8
 
9
9
  ```
10
- Context █████████▍░░░░░░░░░░ 47.1% 471k / 1.0M Input 194k · Output 89k · Cache hit 99.9% · Cost $0.229
11
- ~/.pi (master) deepseek-flash · Effort high · TTFT 482ms · 729 tok/s
10
+ Context █████████▍░░░░░░░░░░ 47% 471k / 1.0M Input 194k · Output 89k | Cache hit 99.9% | Cost $0.229 · Today $1.63
11
+ ~/.pi (master) deepseek-flash · Effort high | TTFT 482ms · Avg TTFT 612ms | Last 729 tok/s · Avg 512 tok/s
12
12
  LSP Active: typescript
13
13
  ```
14
14
 
@@ -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 数 |
@@ -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
 
@@ -36,6 +37,7 @@ import {
36
37
  formatCwd,
37
38
  formatLatency,
38
39
  formatTps,
40
+ avgMs,
39
41
  isQuietStatus,
40
42
  pair,
41
43
  cachedRates,
@@ -62,6 +64,10 @@ const FALLBACK_TOKENS_PER_CHAR = 0.25
62
64
  const CONFIG_PATH = join(homedir(), '.pi', 'agent', 'statusline.json')
63
65
  /** Free, keyless, and one request returns every currency — so the cache serves instant switching. */
64
66
  const RATES_URL = 'https://open.er-api.com/v6/latest/USD'
67
+ /** Every session on this machine, for the day-cost total that spans projects and models. */
68
+ const SESSIONS_DIR = join(homedir(), '.pi', 'agent', 'sessions')
69
+ /** Where throughput metrics wait out a /reload: keyed by session file, so a reload restores. */
70
+ const STATE_PATH = join(homedir(), '.pi', 'agent', 'statusline-state.json')
65
71
  const FETCH_TIMEOUT_MS = 5000
66
72
 
67
73
  function today(): string {
@@ -97,6 +103,83 @@ async function fetchRates(): Promise<Record<string, number> | null> {
97
103
  * Called once per session on purpose: a footer that stat'ed a file on every frame would be its own
98
104
  * bug. Editing the file therefore takes effect on `/reload`.
99
105
  */
106
+ function startOfToday(): number {
107
+ const start = new Date()
108
+ start.setHours(0, 0, 0, 0)
109
+ return start.getTime()
110
+ }
111
+
112
+ function isRecord(value: unknown): value is Record<string, unknown> {
113
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
114
+ }
115
+
116
+ /** Coerces a persisted counter back to a non-negative finite number, or 0. */
117
+ function num(value: unknown): number {
118
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : 0
119
+ }
120
+
121
+ /** Today's provider cost of one session-file line, or null when the line bills nothing today. */
122
+ function entryCost(line: string, since: number): number | null {
123
+ if (!line.includes('"usage"')) return null
124
+ let entry: unknown
125
+ try {
126
+ entry = JSON.parse(line)
127
+ } catch {
128
+ return null
129
+ }
130
+ if (!isRecord(entry) || typeof entry.timestamp !== 'string') return null
131
+ if (Date.parse(entry.timestamp) < since) return null
132
+ if (!isRecord(entry.usage) || !isRecord(entry.usage.cost)) return null
133
+ const total = entry.usage.cost.total
134
+ return typeof total === 'number' && Number.isFinite(total) ? total : null
135
+ }
136
+
137
+ /**
138
+ * Today's cost across every other session on this machine.
139
+ *
140
+ * Files not touched today are skipped unread, and this session's own file is skipped because its
141
+ * cost is already live in totals.cost — counting both would double a resumed session.
142
+ */
143
+ async function sumOtherTodaysCost(currentFile: string | null, since: number): Promise<number> {
144
+ let dirs: string[] = []
145
+ try {
146
+ dirs = await readdir(SESSIONS_DIR)
147
+ } catch {
148
+ return 0
149
+ }
150
+ const lists = await Promise.all(
151
+ dirs.map(async (dir) => {
152
+ const dirPath = join(SESSIONS_DIR, dir)
153
+ let names: string[] = []
154
+ try {
155
+ names = await readdir(dirPath)
156
+ } catch {
157
+ return []
158
+ }
159
+ return names
160
+ .map((name) => join(dirPath, name))
161
+ .filter((path) => path.endsWith('.jsonl') && path !== currentFile)
162
+ }),
163
+ )
164
+ const costs = await Promise.all(
165
+ lists.flat().map(async (path) => {
166
+ try {
167
+ if ((await stat(path)).mtimeMs < since) return 0
168
+ } catch {
169
+ return 0
170
+ }
171
+ const text = await readFile(path, 'utf8').catch(() => '')
172
+ let total = 0
173
+ for (const line of text.split('\n')) {
174
+ const cost = entryCost(line, since)
175
+ if (cost !== null) total += cost
176
+ }
177
+ return total
178
+ }),
179
+ )
180
+ return costs.reduce((sum, value) => sum + value, 0)
181
+ }
182
+
100
183
  async function loadCurrency(notify: (message: string) => void): Promise<Currency> {
101
184
  let text: string | null = null
102
185
  try {
@@ -167,6 +250,7 @@ interface Totals {
167
250
  cacheWrite: number
168
251
  cost: number
169
252
  cacheHitRate: number | null
253
+ todayCost: number
170
254
  }
171
255
 
172
256
  /** The usage fields this footer totals, read structurally so no cast is needed. */
@@ -178,7 +262,7 @@ type UsageTotals = {
178
262
  cost: { total: number }
179
263
  }
180
264
 
181
- function collectTotals(ctx: ExtensionContext): Totals {
265
+ function collectTotals(ctx: ExtensionContext, since: number): Totals {
182
266
  const totals: Totals = {
183
267
  input: 0,
184
268
  output: 0,
@@ -186,29 +270,38 @@ function collectTotals(ctx: ExtensionContext): Totals {
186
270
  cacheWrite: 0,
187
271
  cost: 0,
188
272
  cacheHitRate: null,
273
+ todayCost: 0,
189
274
  }
190
275
  // `usage` stays optional here even though an assistant message always carries it:
191
276
  // a truncated or hand-edited session file is the case this guard is for.
192
- const add = (usage: UsageTotals | undefined, assistant: boolean): void => {
277
+ const add = (usage: UsageTotals | undefined, assistant: boolean, isToday: boolean): void => {
193
278
  if (!usage) return
194
279
  totals.input += usage.input
195
280
  totals.output += usage.output
196
281
  totals.cacheRead += usage.cacheRead
197
282
  totals.cacheWrite += usage.cacheWrite
198
283
  totals.cost += usage.cost.total
284
+ if (isToday) totals.todayCost += usage.cost.total
199
285
  if (!assistant) return
200
286
  const prompt = usage.input + usage.cacheRead + usage.cacheWrite
201
287
  if (prompt > 0) totals.cacheHitRate = (usage.cacheRead / prompt) * 100
202
288
  }
203
289
  for (const entry of ctx.sessionManager.getEntries()) {
290
+ const stamp =
291
+ 'timestamp' in entry && typeof entry.timestamp === 'string'
292
+ ? Date.parse(entry.timestamp)
293
+ : Number.NaN
294
+ const isToday = Number.isFinite(stamp) && stamp >= since
204
295
  if (entry.type === 'message') {
205
296
  const { message } = entry
206
- if (message.role === 'assistant') add(message.usage, true)
207
- else if (message.role === 'toolResult') add(message.usage, false)
297
+ if (message.role === 'assistant') add(message.usage, true, isToday)
298
+ else if (message.role === 'toolResult') add(message.usage, false, isToday)
208
299
  continue
209
300
  }
210
301
  // Compaction and branch summaries bill a model call of their own.
211
- if (entry.type === 'compaction' || entry.type === 'branch_summary') add(entry.usage, false)
302
+ if (entry.type === 'compaction' || entry.type === 'branch_summary') {
303
+ add(entry.usage, false, isToday)
304
+ }
212
305
  }
213
306
  return totals
214
307
  }
@@ -240,6 +333,10 @@ export default function (pi: ExtensionAPI) {
240
333
  let requestAt: number | null = null
241
334
  let firstTokenAt: number | null = null
242
335
  let totalDecodeMs = 0
336
+ let totalMeasuredOutput = 0
337
+ let totalTtftMs = 0
338
+ let ttftCount = 0
339
+ let todayBase = 0
243
340
  let ticker: ReturnType<typeof setInterval> | null = null
244
341
  let windowAt = 0
245
342
  let windowTokens = 0
@@ -278,6 +375,48 @@ export default function (pi: ExtensionAPI) {
278
375
  }, TICK_MS)
279
376
  }
280
377
 
378
+ /**
379
+ * Writes the throughput metrics so a /reload can restore them.
380
+ *
381
+ * Decode timing only exists in live stream events — pi records nothing per message — so without
382
+ * this file the session's averages reset to zero every time the extension re-loads.
383
+ */
384
+ function persist(sessionFile: string | null): void {
385
+ if (sessionFile === null) return
386
+ const state = {
387
+ sessionFile,
388
+ totalDecodeMs,
389
+ totalMeasuredOutput,
390
+ totalTtftMs,
391
+ ttftCount,
392
+ last: reading,
393
+ }
394
+ void writeFile(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`).catch(() => {})
395
+ }
396
+
397
+ /** Restores what persist wrote, but only for this exact session file. */
398
+ async function restore(sessionFile: string | null): Promise<void> {
399
+ if (sessionFile === null) return
400
+ let state: unknown
401
+ try {
402
+ state = JSON.parse(await readFile(STATE_PATH, 'utf8'))
403
+ } catch {
404
+ return
405
+ }
406
+ if (!isRecord(state) || state.sessionFile !== sessionFile) return
407
+ totalDecodeMs = num(state.totalDecodeMs)
408
+ totalMeasuredOutput = num(state.totalMeasuredOutput)
409
+ totalTtftMs = num(state.totalTtftMs)
410
+ ttftCount = num(state.ttftCount)
411
+ if (isRecord(state.last) && typeof state.last.rate === 'number') {
412
+ reading = {
413
+ rate: state.last.rate,
414
+ exact: state.last.exact === true,
415
+ ttftMs: typeof state.last.ttftMs === 'number' ? state.last.ttftMs : null,
416
+ }
417
+ }
418
+ }
419
+
281
420
  function installFooter(ctx: ExtensionContext): void {
282
421
  ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
283
422
  requestRender = () => tui.requestRender()
@@ -289,8 +428,8 @@ export default function (pi: ExtensionAPI) {
289
428
  },
290
429
  render(width: number): string[] {
291
430
  const usage = ctx.getContextUsage()
292
- const totals = collectTotals(ctx)
293
- const avg = avgTokPerSec(totals.output, totalDecodeMs)
431
+ const totals = collectTotals(ctx, startOfToday())
432
+ const avg = avgTokPerSec(totalMeasuredOutput, totalDecodeMs)
294
433
  const row1 = contextRow(
295
434
  theme,
296
435
  width,
@@ -302,23 +441,36 @@ export default function (pi: ExtensionAPI) {
302
441
  output: totals.output,
303
442
  cacheHitRate: totals.cacheHitRate,
304
443
  cost: totals.cost,
444
+ todayCost: todayBase + totals.todayCost,
305
445
  },
306
446
  currency,
307
447
  )
308
448
 
309
- // Row 2: model and the latest turn's timing on the right, path on the left.
449
+ // Row 2: model and the latest turn's timing on the right, path on the left. Three
450
+ // groups — identity, first token, throughput — separated by a wall instead of another
451
+ // dot, because a run of similar-looking pairs is what made the old footer unreadable.
310
452
  const model = ctx.model?.id ?? 'no model'
311
- const row2Parts = [theme.fg('accent', model)]
312
- if (ctx.thinkingLevel) row2Parts.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
453
+ const separator = theme.fg('dim', ' · ')
454
+ const wall = theme.fg('dim', ' | ')
455
+ const identity = [theme.fg('accent', model)]
456
+ if (ctx.thinkingLevel) identity.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
457
+
458
+ const ttft: string[] = []
313
459
  const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
314
460
  if (waiting !== null) {
315
461
  // The clock is running: this wait has no reading yet, so the previous turn's
316
- // throughput would only be mistaken for the current one.
317
- row2Parts.push(pair(theme, 'TTFT', waiting.text, 'muted'))
462
+ // numbers would only be mistaken for the current one.
463
+ ttft.push(pair(theme, 'TTFT', waiting.text, 'muted'))
318
464
  } else if (reading) {
319
465
  if (reading.ttftMs !== null)
320
- row2Parts.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
321
- row2Parts.push(
466
+ ttft.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
467
+ }
468
+ const avgTtft = avgMs(totalTtftMs, ttftCount)
469
+ if (avgTtft !== null) ttft.push(pair(theme, 'Avg TTFT', formatLatency(avgTtft), 'muted'))
470
+
471
+ const throughput: string[] = []
472
+ if (reading) {
473
+ throughput.push(
322
474
  pair(
323
475
  theme,
324
476
  'Last',
@@ -327,9 +479,12 @@ export default function (pi: ExtensionAPI) {
327
479
  ),
328
480
  )
329
481
  }
330
- if (avg !== null) row2Parts.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s`, 'muted'))
331
- const row2Right = row2Parts.join(theme.fg('dim', ' · '))
482
+ if (avg !== null) throughput.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s`, 'muted'))
332
483
 
484
+ const row2Right = [identity, ttft, throughput]
485
+ .filter((group) => group.length > 0)
486
+ .map((group) => group.join(separator))
487
+ .join(wall)
333
488
  const branch = footerData.getGitBranch()
334
489
  const path = formatCwd(ctx.cwd)
335
490
  const branchSuffix = branch ? ` (${branch})` : ''
@@ -388,9 +543,13 @@ export default function (pi: ExtensionAPI) {
388
543
  reading = null
389
544
  requestAt = null
390
545
  totalDecodeMs = 0
546
+ totalMeasuredOutput = 0
391
547
  stopTicker()
392
548
  resetStream()
393
549
  currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
550
+ const file = ctx.sessionManager.getSessionFile()
551
+ await restore(file ?? null)
552
+ todayBase = await sumOtherTodaysCost(file ?? null, startOfToday())
394
553
  installFooter(ctx)
395
554
  })
396
555
 
@@ -455,7 +614,7 @@ export default function (pi: ExtensionAPI) {
455
614
  publish(rate, false, ttftMs(requestAt, firstTokenAt))
456
615
  })
457
616
 
458
- pi.on('message_end', async (event) => {
617
+ pi.on('message_end', async (event, ctx) => {
459
618
  if (event.message.role !== 'assistant') return
460
619
 
461
620
  const message = event.message as { content: unknown; usage?: { output?: number } }
@@ -470,8 +629,14 @@ export default function (pi: ExtensionAPI) {
470
629
  const measured = ttftMs(requestAt, firstTokenAt)
471
630
  const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
472
631
  if (measured !== null && decodeMs >= MIN_SAMPLE_MS) {
473
- // Session-average numerator lives in collectTotals; this is its denominator.
632
+ // The average's numerator and denominator must cover the same messages: totals.output spans
633
+ // the whole session (a reload replays none of it), so pairing it with the partial denominator
634
+ // below once produced an Avg of 4324 tok/s.
474
635
  totalDecodeMs += decodeMs
636
+ if (output > 0) totalMeasuredOutput += output
637
+ totalTtftMs += measured
638
+ ttftCount += 1
639
+ persist(ctx.sessionManager.getSessionFile() ?? null)
475
640
  publish((tokens / decodeMs) * 1000, output > 0, measured)
476
641
  }
477
642
  // Null it with the stream: a request that has produced its message is no longer in flight, and
@@ -296,6 +296,11 @@ export function avgTokPerSec(outputTokens: number, decodeMs: number): number | n
296
296
  return outputTokens / (decodeMs / 1000)
297
297
  }
298
298
 
299
+ /** Mean of `count` durations totalling `totalMs`; null when nothing was measured. */
300
+ export function avgMs(totalMs: number, count: number): number | null {
301
+ return count > 0 ? totalMs / count : null
302
+ }
303
+
299
304
  /**
300
305
  * The config file's text with the day's rates recorded in it, so the next session starts warm.
301
306
  *
@@ -397,6 +402,8 @@ export interface ContextRowParts {
397
402
  output: number
398
403
  cacheHitRate: number | null
399
404
  cost: number
405
+ /** Today's spend across every session on this machine, or 0 to hide the slot. */
406
+ todayCost: number
400
407
  }
401
408
 
402
409
  /**
@@ -421,7 +428,8 @@ export function contextRow(
421
428
  const fraction = parts.percent === null ? 0 : parts.percent / 100
422
429
  const percent = theme.fg(
423
430
  percentColor(parts.percent),
424
- parts.percent === null ? '?' : `${parts.percent.toFixed(1)}%`,
431
+ // Whole percents: the meter carries the precision, and a decimal here is noise.
432
+ parts.percent === null ? '?' : `${Math.round(parts.percent)}%`,
425
433
  )
426
434
  const meter = `${theme.fg('dim', 'Context')} ${bar(theme, BAR_CELLS, fraction)} ${percent}`
427
435
  const detail =
@@ -432,15 +440,28 @@ export function contextRow(
432
440
  const volumes: string[] = []
433
441
  if (parts.input > 0) volumes.push(pair(theme, 'Input', formatTokens(parts.input)))
434
442
  if (parts.output > 0) volumes.push(pair(theme, 'Output', formatTokens(parts.output)))
435
- const outcomes: string[] = []
443
+ const hit: string[] = []
436
444
  if (parts.cacheHitRate !== null) {
437
- outcomes.push(pair(theme, 'Cache hit', `${parts.cacheHitRate.toFixed(1)}%`))
445
+ hit.push(pair(theme, 'Cache hit', `${parts.cacheHitRate.toFixed(1)}%`))
446
+ }
447
+ const money: string[] = []
448
+ if (parts.cost > 0) money.push(pair(theme, 'Cost', formatCost(parts.cost, currency)))
449
+ if (parts.todayCost > 0) {
450
+ money.push(pair(theme, 'Today', formatCost(parts.todayCost, currency)))
438
451
  }
439
- if (parts.cost > 0) outcomes.push(pair(theme, 'Cost', formatCost(parts.cost, currency)))
440
452
 
441
453
  const separator = theme.fg('dim', ' · ')
442
- const full = [...volumes, ...outcomes].join(separator)
443
- const core = outcomes.join(separator)
454
+ // Groups, not a flat run of dots: volumes | cache | money. A wall between different kinds of
455
+ // number reads faster than another dot between similar-looking ones.
456
+ const groups: string[] = []
457
+ if (volumes.length > 0) groups.push(volumes.join(separator))
458
+ if (hit.length > 0) groups.push(hit.join(separator))
459
+ if (money.length > 0) groups.push(money.join(separator))
460
+ const full = groups.join(theme.fg('dim', ' | '))
461
+ const core =
462
+ hit.length > 0 && money.length > 0
463
+ ? hit.join(separator) + theme.fg('dim', ' | ') + money.join(separator)
464
+ : [...hit, ...money].join(separator)
444
465
  const fits = (left: string, right: string): boolean =>
445
466
  right === '' || visibleWidth(left) + 2 + visibleWidth(right) <= width
446
467
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reedchan/statusline",
3
- "version": "1.5.0",
3
+ "version": "1.7.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",