@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 +1 -1
- package/extensions/statusline/index.ts +49 -9
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
463
|
+
state = JSON.parse(await readFile(statePath(sessionFile), 'utf8'))
|
|
426
464
|
} catch {
|
|
427
465
|
return
|
|
428
466
|
}
|
|
429
|
-
if (!isRecord(state)
|
|
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
|
|
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.
|
|
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",
|