useful-pi-extensions 1.1.0 → 1.1.2

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.1.0",
3
+ "version": "1.1.2",
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",
@@ -18,6 +18,7 @@
18
18
  * stepStartTime; decodeMs = completedTime - firstTokenTime; tok/s = usage.output / (decodeMs / 1000)
19
19
  */
20
20
 
21
+ import { readFile, stat } from 'node:fs/promises'
21
22
  import { homedir } from 'node:os'
22
23
  import { join } from 'node:path'
23
24
 
@@ -38,6 +39,7 @@ import {
38
39
  pair,
39
40
  row,
40
41
  shortenPath,
42
+ ttftMs,
41
43
  USD,
42
44
  type Currency,
43
45
  } from './render.ts'
@@ -58,8 +60,19 @@ const CONFIG_PATH = join(homedir(), '.pi', 'agent', 'statusline.json')
58
60
  * bug. Editing the file therefore takes effect on `/reload`.
59
61
  */
60
62
  async function loadCurrency(notify: (message: string) => void): Promise<Currency> {
61
- const file = Bun.file(CONFIG_PATH)
62
- const text = (await file.exists()) ? await file.text() : null
63
+ try {
64
+ await stat(CONFIG_PATH)
65
+ } catch {
66
+ // No config file is the normal case, and it means USD.
67
+ return USD
68
+ }
69
+ let text: string
70
+ try {
71
+ text = await readFile(CONFIG_PATH, 'utf8')
72
+ } catch {
73
+ notify('statusline.json exists but could not be read, showing USD')
74
+ return USD
75
+ }
63
76
  const { currency, problem } = currencyFromConfig(text)
64
77
  if (problem !== null) notify(problem)
65
78
  return currency
@@ -173,7 +186,7 @@ export default function (pi: ExtensionAPI) {
173
186
  let reading: { rate: number; exact: boolean; ttftMs: number | null } | null = null
174
187
 
175
188
  let chars = 0
176
- let stepStartedAt = 0
189
+ let requestAt: number | null = null
177
190
  let firstTokenAt: number | null = null
178
191
  let windowAt = 0
179
192
  let windowTokens = 0
@@ -187,8 +200,8 @@ export default function (pi: ExtensionAPI) {
187
200
  renderedAt = 0
188
201
  }
189
202
 
190
- function publish(rate: number, exact: boolean, ttftMs: number | null): void {
191
- reading = { rate, exact, ttftMs }
203
+ function publish(rate: number, exact: boolean, ttft: number | null): void {
204
+ reading = { rate, exact, ttftMs: ttft }
192
205
  requestRender?.()
193
206
  }
194
207
 
@@ -274,6 +287,7 @@ export default function (pi: ExtensionAPI) {
274
287
  pi.on('session_start', async (_event, ctx) => {
275
288
  ratio = seedRatio(ctx)
276
289
  reading = null
290
+ requestAt = null
277
291
  resetStream()
278
292
  currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
279
293
  installFooter(ctx)
@@ -282,7 +296,17 @@ export default function (pi: ExtensionAPI) {
282
296
  pi.on('message_start', async (event) => {
283
297
  if (event.message.role !== 'assistant') return
284
298
  resetStream()
285
- stepStartedAt = Date.now()
299
+ })
300
+
301
+ // TTFT's request anchor, from the agent loop in pi's docs: turn_start opens the LLM call and
302
+ // before_provider_request is the last thing pi does before the wire. message_start is not that
303
+ // moment, and anchoring there made a first token that arrived early read as a clamped 0ms.
304
+ pi.on('turn_start', async () => {
305
+ requestAt = null
306
+ })
307
+
308
+ pi.on('before_provider_request', async () => {
309
+ requestAt = Date.now()
286
310
  })
287
311
 
288
312
  pi.on('message_update', async (event) => {
@@ -314,7 +338,7 @@ export default function (pi: ExtensionAPI) {
314
338
  const rate =
315
339
  windowMs > 0 ? ((tokens - windowTokens) / windowMs) * 1000 : (tokens / decodeMs) * 1000
316
340
  renderedAt = now
317
- publish(rate, false, firstTokenAt - stepStartedAt)
341
+ publish(rate, false, ttftMs(requestAt, firstTokenAt))
318
342
  })
319
343
 
320
344
  pi.on('message_end', async (event) => {
@@ -329,10 +353,10 @@ export default function (pi: ExtensionAPI) {
329
353
  }
330
354
 
331
355
  const decodeMs = firstTokenAt !== null ? Date.now() - firstTokenAt : 0
332
- const ttftMs = firstTokenAt !== null ? firstTokenAt - stepStartedAt : null
356
+ const measured = ttftMs(requestAt, firstTokenAt)
333
357
  const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
334
- if (ttftMs !== null && decodeMs >= MIN_SAMPLE_MS)
335
- publish((tokens / decodeMs) * 1000, output > 0, ttftMs)
358
+ if (measured !== null && decodeMs >= MIN_SAMPLE_MS)
359
+ publish((tokens / decodeMs) * 1000, output > 0, measured)
336
360
  resetStream()
337
361
  })
338
362
  }
@@ -66,6 +66,21 @@ export function formatLatency(ms: number): string {
66
66
  return `${seconds < 10 ? Math.round(seconds * 10) / 10 : Math.round(seconds)}s`
67
67
  }
68
68
 
69
+ /**
70
+ * TTFT from its two wall-clock anchors: the moment the request was dispatched, and the first
71
+ * streamed delta.
72
+ *
73
+ * The guard is the point. Both anchors come from extension events whose ordering around a stream is
74
+ * not guaranteed, and a first token that appears to precede its own request yields a negative that
75
+ * `formatLatency` would clamp to `0ms` — a confident fake where an honest absence belongs.
76
+ *
77
+ * @returns The milliseconds between the two, or null when they do not line up.
78
+ */
79
+ export function ttftMs(requestAt: number | null, firstTokenAt: number | null): number | null {
80
+ if (requestAt === null || firstTokenAt === null) return null
81
+ return firstTokenAt > requestAt ? firstTokenAt - requestAt : null
82
+ }
83
+
69
84
  /** Home-relative path, or the absolute path when it is outside the home directory. */
70
85
  export function formatCwd(cwd: string): string {
71
86
  const home = homedir()