useful-pi-extensions 1.6.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "useful-pi-extensions",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A small collection of pi extensions, installed with one command — a labelled status line with context pressure, cache, cost, effort, TTFT and tokens/sec.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -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% 471k / 1.0M Input 194k · Output 89k
|
|
12
|
-
~/.pi (master)
|
|
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
|
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
替换 pi 的 footer,改成带文字标签的两行。每个值都带一个词,不需要靠符号猜、也不需要记图例。
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
Context █████████▍░░░░░░░░░░ 47% 471k / 1.0M Input 194k · Output 89k
|
|
11
|
-
~/.pi (master)
|
|
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
|
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
formatCwd,
|
|
38
38
|
formatLatency,
|
|
39
39
|
formatTps,
|
|
40
|
+
avgMs,
|
|
40
41
|
isQuietStatus,
|
|
41
42
|
pair,
|
|
42
43
|
cachedRates,
|
|
@@ -65,6 +66,8 @@ const CONFIG_PATH = join(homedir(), '.pi', 'agent', 'statusline.json')
|
|
|
65
66
|
const RATES_URL = 'https://open.er-api.com/v6/latest/USD'
|
|
66
67
|
/** Every session on this machine, for the day-cost total that spans projects and models. */
|
|
67
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')
|
|
68
71
|
const FETCH_TIMEOUT_MS = 5000
|
|
69
72
|
|
|
70
73
|
function today(): string {
|
|
@@ -110,6 +113,11 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
110
113
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
111
114
|
}
|
|
112
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
|
+
|
|
113
121
|
/** Today's provider cost of one session-file line, or null when the line bills nothing today. */
|
|
114
122
|
function entryCost(line: string, since: number): number | null {
|
|
115
123
|
if (!line.includes('"usage"')) return null
|
|
@@ -326,6 +334,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
326
334
|
let firstTokenAt: number | null = null
|
|
327
335
|
let totalDecodeMs = 0
|
|
328
336
|
let totalMeasuredOutput = 0
|
|
337
|
+
let totalTtftMs = 0
|
|
338
|
+
let ttftCount = 0
|
|
329
339
|
let todayBase = 0
|
|
330
340
|
let ticker: ReturnType<typeof setInterval> | null = null
|
|
331
341
|
let windowAt = 0
|
|
@@ -365,6 +375,48 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
375
|
}, TICK_MS)
|
|
366
376
|
}
|
|
367
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
|
+
|
|
368
420
|
function installFooter(ctx: ExtensionContext): void {
|
|
369
421
|
ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
|
|
370
422
|
requestRender = () => tui.requestRender()
|
|
@@ -394,19 +446,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
394
446
|
currency,
|
|
395
447
|
)
|
|
396
448
|
|
|
397
|
-
// 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.
|
|
398
452
|
const model = ctx.model?.id ?? 'no model'
|
|
399
|
-
const
|
|
400
|
-
|
|
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[] = []
|
|
401
459
|
const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
|
|
402
460
|
if (waiting !== null) {
|
|
403
461
|
// The clock is running: this wait has no reading yet, so the previous turn's
|
|
404
|
-
//
|
|
405
|
-
|
|
462
|
+
// numbers would only be mistaken for the current one.
|
|
463
|
+
ttft.push(pair(theme, 'TTFT', waiting.text, 'muted'))
|
|
406
464
|
} else if (reading) {
|
|
407
465
|
if (reading.ttftMs !== null)
|
|
408
|
-
|
|
409
|
-
|
|
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(
|
|
410
474
|
pair(
|
|
411
475
|
theme,
|
|
412
476
|
'Last',
|
|
@@ -415,9 +479,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
415
479
|
),
|
|
416
480
|
)
|
|
417
481
|
}
|
|
418
|
-
if (avg !== null)
|
|
419
|
-
const row2Right = row2Parts.join(theme.fg('dim', ' · '))
|
|
482
|
+
if (avg !== null) throughput.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s`, 'muted'))
|
|
420
483
|
|
|
484
|
+
const row2Right = [identity, ttft, throughput]
|
|
485
|
+
.filter((group) => group.length > 0)
|
|
486
|
+
.map((group) => group.join(separator))
|
|
487
|
+
.join(wall)
|
|
421
488
|
const branch = footerData.getGitBranch()
|
|
422
489
|
const path = formatCwd(ctx.cwd)
|
|
423
490
|
const branchSuffix = branch ? ` (${branch})` : ''
|
|
@@ -481,6 +548,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
481
548
|
resetStream()
|
|
482
549
|
currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
|
|
483
550
|
const file = ctx.sessionManager.getSessionFile()
|
|
551
|
+
await restore(file ?? null)
|
|
484
552
|
todayBase = await sumOtherTodaysCost(file ?? null, startOfToday())
|
|
485
553
|
installFooter(ctx)
|
|
486
554
|
})
|
|
@@ -546,7 +614,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
546
614
|
publish(rate, false, ttftMs(requestAt, firstTokenAt))
|
|
547
615
|
})
|
|
548
616
|
|
|
549
|
-
pi.on('message_end', async (event) => {
|
|
617
|
+
pi.on('message_end', async (event, ctx) => {
|
|
550
618
|
if (event.message.role !== 'assistant') return
|
|
551
619
|
|
|
552
620
|
const message = event.message as { content: unknown; usage?: { output?: number } }
|
|
@@ -566,6 +634,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
566
634
|
// below once produced an Avg of 4324 tok/s.
|
|
567
635
|
totalDecodeMs += decodeMs
|
|
568
636
|
if (output > 0) totalMeasuredOutput += output
|
|
637
|
+
totalTtftMs += measured
|
|
638
|
+
ttftCount += 1
|
|
639
|
+
persist(ctx.sessionManager.getSessionFile() ?? null)
|
|
569
640
|
publish((tokens / decodeMs) * 1000, output > 0, measured)
|
|
570
641
|
}
|
|
571
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
|
*
|
|
@@ -435,18 +440,28 @@ export function contextRow(
|
|
|
435
440
|
const volumes: string[] = []
|
|
436
441
|
if (parts.input > 0) volumes.push(pair(theme, 'Input', formatTokens(parts.input)))
|
|
437
442
|
if (parts.output > 0) volumes.push(pair(theme, 'Output', formatTokens(parts.output)))
|
|
438
|
-
const
|
|
443
|
+
const hit: string[] = []
|
|
439
444
|
if (parts.cacheHitRate !== null) {
|
|
440
|
-
|
|
445
|
+
hit.push(pair(theme, 'Cache hit', `${parts.cacheHitRate.toFixed(1)}%`))
|
|
441
446
|
}
|
|
442
|
-
|
|
447
|
+
const money: string[] = []
|
|
448
|
+
if (parts.cost > 0) money.push(pair(theme, 'Cost', formatCost(parts.cost, currency)))
|
|
443
449
|
if (parts.todayCost > 0) {
|
|
444
|
-
|
|
450
|
+
money.push(pair(theme, 'Today', formatCost(parts.todayCost, currency)))
|
|
445
451
|
}
|
|
446
452
|
|
|
447
453
|
const separator = theme.fg('dim', ' · ')
|
|
448
|
-
|
|
449
|
-
|
|
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)
|
|
450
465
|
const fits = (left: string, right: string): boolean =>
|
|
451
466
|
right === '' || visibleWidth(left) + 2 + visibleWidth(right) <= width
|
|
452
467
|
|