@reedchan/statusline 1.8.0 → 1.9.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
@@ -55,7 +55,7 @@ Configuration lives at the top of [`render.ts`](render.ts):
55
55
 
56
56
  pi prices every model in USD and its `cost` field carries no unit at all, so the footer cannot know
57
57
  what you were actually billed. The currency and the rate live in `~/.pi/agent/statusline/config.json` — everything this
58
- extension keeps (config plus the reload-proof state file) sits in that one folder:
58
+ extension keeps (config plus the per-session state files that survive /reload) sits in that one folder:
59
59
 
60
60
  ```json
61
61
  {
@@ -18,10 +18,9 @@
18
18
  * stepStartTime; decodeMs = completedTime - firstTokenTime; tok/s = usage.output / (decodeMs / 1000)
19
19
  */
20
20
 
21
- import { mkdir, readFile, readdir, rename, writeFile } from 'node:fs/promises'
22
- import { stat } from 'node:fs/promises'
21
+ import { mkdir, readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises'
23
22
  import { homedir } from 'node:os'
24
- import { dirname, join } from 'node:path'
23
+ import { basename, dirname, join } from 'node:path'
25
24
 
26
25
  import type {
27
26
  ExtensionAPI,
@@ -71,8 +70,11 @@ const RATES_URL = 'https://open.er-api.com/v6/latest/USD'
71
70
  /** Every session on this machine, for the day-cost total that spans projects and models. */
72
71
  const SESSIONS_DIR = join(homedir(), '.pi', 'agent', 'sessions')
73
72
  /** Where throughput metrics wait out a /reload: keyed by session file, so a reload restores. */
74
- const STATE_PATH = join(STATUSLINE_DIR, 'state.json')
73
+ /** One state file per session, so two concurrent sessions cannot clobber each other. */
74
+ const STATE_DIR = join(STATUSLINE_DIR, 'state')
75
75
  const LEGACY_STATE = join(homedir(), '.pi', 'agent', 'statusline-state.json')
76
+ /** The 1.8 single-file state: one bucket every session shared, also migrated to per-session. */
77
+ const LEGACY_SHARED_STATE = join(STATUSLINE_DIR, 'state.json')
76
78
  const FETCH_TIMEOUT_MS = 5000
77
79
 
78
80
  function today(): string {
@@ -133,6 +135,40 @@ async function migrateLegacyFile(legacy: string, current: string): Promise<void>
133
135
  }
134
136
  }
135
137
 
138
+ /** Per-session state path: the session file's name is unique and stable across reloads. */
139
+ function statePath(sessionFile: string): string {
140
+ return join(STATE_DIR, `${basename(sessionFile)}.json`)
141
+ }
142
+
143
+ /** Moves a shared-bucket state file to its per-session name; garbage in, ignored out. */
144
+ async function migrateLegacyState(legacy: string): Promise<void> {
145
+ try {
146
+ const parsed: unknown = JSON.parse(await readFile(legacy, 'utf8'))
147
+ if (!isRecord(parsed) || typeof parsed.sessionFile !== 'string') return
148
+ await mkdir(STATE_DIR, { recursive: true })
149
+ await rename(legacy, statePath(parsed.sessionFile))
150
+ } catch {
151
+ // Nothing to migrate, or the file was not ours: leave it alone.
152
+ }
153
+ }
154
+
155
+ /** Drops per-session state untouched for a week; older files restore nothing useful. */
156
+ async function pruneState(): Promise<void> {
157
+ try {
158
+ const files = await readdir(STATE_DIR)
159
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000
160
+ await Promise.all(
161
+ files.map(async (name) => {
162
+ const filePath = join(STATE_DIR, name)
163
+ const stats = await stat(filePath).catch(() => null)
164
+ if (stats !== null && stats.mtimeMs < cutoff) await unlink(filePath).catch(() => {})
165
+ }),
166
+ )
167
+ } catch {
168
+ // No state dir yet: nothing to prune.
169
+ }
170
+ }
171
+
136
172
  /** Today's provider cost of one session-file line, or null when the line bills nothing today. */
137
173
  function entryCost(line: string, since: number): number | null {
138
174
  if (!line.includes('"usage"')) return null
@@ -414,19 +450,21 @@ export default function (pi: ExtensionAPI) {
414
450
  ttftCount,
415
451
  last: reading,
416
452
  }
417
- void writeFile(STATE_PATH, `${JSON.stringify(state, null, 2)}\n`).catch(() => {})
453
+ void mkdir(STATE_DIR, { recursive: true })
454
+ .then(() => writeFile(statePath(sessionFile), `${JSON.stringify(state, null, 2)}\n`))
455
+ .catch(() => {})
418
456
  }
419
457
 
420
- /** Restores what persist wrote, but only for this exact session file. */
458
+ /** Restores this session's own state file, written by persist before the reload. */
421
459
  async function restore(sessionFile: string | null): Promise<void> {
422
460
  if (sessionFile === null) return
423
461
  let state: unknown
424
462
  try {
425
- state = JSON.parse(await readFile(STATE_PATH, 'utf8'))
463
+ state = JSON.parse(await readFile(statePath(sessionFile), 'utf8'))
426
464
  } catch {
427
465
  return
428
466
  }
429
- if (!isRecord(state) || state.sessionFile !== sessionFile) return
467
+ if (!isRecord(state)) return
430
468
  totalDecodeMs = num(state.totalDecodeMs)
431
469
  totalMeasuredOutput = num(state.totalMeasuredOutput)
432
470
  totalTtftMs = num(state.totalTtftMs)
@@ -579,7 +617,9 @@ export default function (pi: ExtensionAPI) {
579
617
  currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
580
618
  lastKnownUsage = null
581
619
  await migrateLegacyFile(LEGACY_CONFIG, CONFIG_PATH)
582
- await migrateLegacyFile(LEGACY_STATE, STATE_PATH)
620
+ await migrateLegacyState(LEGACY_STATE)
621
+ await migrateLegacyState(LEGACY_SHARED_STATE)
622
+ void pruneState()
583
623
  const file = ctx.sessionManager.getSessionFile()
584
624
  await restore(file ?? null)
585
625
  todayBase = await sumOtherTodaysCost(file ?? null, startOfToday())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reedchan/statusline",
3
- "version": "1.8.0",
3
+ "version": "1.9.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",