useful-pi-extensions 1.1.1 → 1.2.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.1.1",
3
+ "version": "1.2.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",
@@ -39,6 +39,8 @@ import {
39
39
  pair,
40
40
  row,
41
41
  shortenPath,
42
+ ttftDisplay,
43
+ ttftMs,
42
44
  USD,
43
45
  type Currency,
44
46
  } from './render.ts'
@@ -46,6 +48,8 @@ import {
46
48
  const LIVE_RENDER_MS = 200
47
49
  const WINDOW_MS = 2000
48
50
  const MIN_SAMPLE_MS = 200
51
+ /** How often the waiting TTFT slot ticks while a request is in flight. */
52
+ const TICK_MS = 250
49
53
  /** Pi's own estimateTokens() heuristic, used only until a real ratio is known. */
50
54
  const FALLBACK_TOKENS_PER_CHAR = 0.25
51
55
 
@@ -185,8 +189,9 @@ export default function (pi: ExtensionAPI) {
185
189
  let reading: { rate: number; exact: boolean; ttftMs: number | null } | null = null
186
190
 
187
191
  let chars = 0
188
- let stepStartedAt = 0
192
+ let requestAt: number | null = null
189
193
  let firstTokenAt: number | null = null
194
+ let ticker: ReturnType<typeof setInterval> | null = null
190
195
  let windowAt = 0
191
196
  let windowTokens = 0
192
197
  let renderedAt = 0
@@ -199,11 +204,31 @@ export default function (pi: ExtensionAPI) {
199
204
  renderedAt = 0
200
205
  }
201
206
 
202
- function publish(rate: number, exact: boolean, ttftMs: number | null): void {
203
- reading = { rate, exact, ttftMs }
207
+ function publish(rate: number, exact: boolean, ttft: number | null): void {
208
+ reading = { rate, exact, ttftMs: ttft }
204
209
  requestRender?.()
205
210
  }
206
211
 
212
+ /** Stops the live count; a frozen TTFT needs no clock. */
213
+ function stopTicker(): void {
214
+ if (ticker !== null) {
215
+ clearInterval(ticker)
216
+ ticker = null
217
+ }
218
+ }
219
+
220
+ /** Ticks the footer while a request is in flight, until the first token lands or the turn ends. */
221
+ function startTicker(): void {
222
+ if (ticker !== null) return
223
+ ticker = setInterval(() => {
224
+ if (requestAt === null || firstTokenAt !== null) {
225
+ stopTicker()
226
+ return
227
+ }
228
+ requestRender?.()
229
+ }, TICK_MS)
230
+ }
231
+
207
232
  function installFooter(ctx: ExtensionContext): void {
208
233
  ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
209
234
  requestRender = () => tui.requestRender()
@@ -235,7 +260,12 @@ export default function (pi: ExtensionAPI) {
235
260
  const model = ctx.model?.id ?? 'no model'
236
261
  const row2Parts = [theme.fg('accent', model)]
237
262
  if (ctx.thinkingLevel) row2Parts.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
238
- if (reading) {
263
+ const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
264
+ if (waiting !== null) {
265
+ // The clock is running: this wait has no reading yet, so the previous turn's
266
+ // throughput would only be mistaken for the current one.
267
+ row2Parts.push(pair(theme, 'TTFT', waiting.text, 'muted'))
268
+ } else if (reading) {
239
269
  if (reading.ttftMs !== null)
240
270
  row2Parts.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
241
271
  row2Parts.push(
@@ -283,9 +313,28 @@ export default function (pi: ExtensionAPI) {
283
313
  })
284
314
  }
285
315
 
316
+ // A run that ends without a first token (abort, provider error) must not leave a clock counting
317
+ // against a request that is no longer in flight.
318
+ pi.on('turn_end', async () => {
319
+ requestAt = null
320
+ stopTicker()
321
+ })
322
+
323
+ pi.on('agent_end', async () => {
324
+ requestAt = null
325
+ stopTicker()
326
+ })
327
+
328
+ pi.on('agent_settled', async () => {
329
+ requestAt = null
330
+ stopTicker()
331
+ })
332
+
286
333
  pi.on('session_start', async (_event, ctx) => {
287
334
  ratio = seedRatio(ctx)
288
335
  reading = null
336
+ requestAt = null
337
+ stopTicker()
289
338
  resetStream()
290
339
  currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
291
340
  installFooter(ctx)
@@ -294,7 +343,27 @@ export default function (pi: ExtensionAPI) {
294
343
  pi.on('message_start', async (event) => {
295
344
  if (event.message.role !== 'assistant') return
296
345
  resetStream()
297
- stepStartedAt = Date.now()
346
+ })
347
+
348
+ // TTFT's request anchor, from the agent loop in pi's docs: turn_start opens the LLM call and
349
+ // before_provider_request is the last thing pi does before the wire. message_start is not that
350
+ // moment, and anchoring there made a first token that arrived early read as a clamped 0ms.
351
+ pi.on('turn_start', async () => {
352
+ requestAt = null
353
+ })
354
+
355
+ pi.on('before_provider_request', async () => {
356
+ requestAt = Date.now()
357
+ })
358
+
359
+ pi.on('turn_start', async () => {
360
+ requestAt = null
361
+ stopTicker()
362
+ })
363
+
364
+ pi.on('before_provider_request', async () => {
365
+ requestAt = Date.now()
366
+ startTicker()
298
367
  })
299
368
 
300
369
  pi.on('message_update', async (event) => {
@@ -310,7 +379,10 @@ export default function (pi: ExtensionAPI) {
310
379
  }
311
380
 
312
381
  const now = Date.now()
313
- if (firstTokenAt === null) firstTokenAt = now
382
+ if (firstTokenAt === null) {
383
+ firstTokenAt = now
384
+ stopTicker()
385
+ }
314
386
  chars += delta.delta.length
315
387
 
316
388
  const tokens = chars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
@@ -326,7 +398,7 @@ export default function (pi: ExtensionAPI) {
326
398
  const rate =
327
399
  windowMs > 0 ? ((tokens - windowTokens) / windowMs) * 1000 : (tokens / decodeMs) * 1000
328
400
  renderedAt = now
329
- publish(rate, false, firstTokenAt - stepStartedAt)
401
+ publish(rate, false, ttftMs(requestAt, firstTokenAt))
330
402
  })
331
403
 
332
404
  pi.on('message_end', async (event) => {
@@ -341,10 +413,13 @@ export default function (pi: ExtensionAPI) {
341
413
  }
342
414
 
343
415
  const decodeMs = firstTokenAt !== null ? Date.now() - firstTokenAt : 0
344
- const ttftMs = firstTokenAt !== null ? firstTokenAt - stepStartedAt : null
416
+ const measured = ttftMs(requestAt, firstTokenAt)
345
417
  const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
346
- if (ttftMs !== null && decodeMs >= MIN_SAMPLE_MS)
347
- publish((tokens / decodeMs) * 1000, output > 0, ttftMs)
418
+ if (measured !== null && decodeMs >= MIN_SAMPLE_MS)
419
+ publish((tokens / decodeMs) * 1000, output > 0, measured)
420
+ // Null it with the stream: a request that has produced its message is no longer in flight, and
421
+ // a stale anchor would let the live branch count against nothing until the next turn.
422
+ requestAt = null
348
423
  resetStream()
349
424
  })
350
425
  }
@@ -66,6 +66,51 @@ 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
+
84
+ /** What the TTFT slot renders, and whether the clock is still running. */
85
+ export interface TtftDisplay {
86
+ text: string
87
+ live: boolean
88
+ }
89
+
90
+ /**
91
+ * The TTFT slot, for all three phases of a request.
92
+ *
93
+ * While the request is in flight the slot counts up (`~2s`, the `~` marking an unfinished wait, the
94
+ * same convention as the tok/s estimate) — a slow first byte is something you watch happen, not a
95
+ * number you are told about afterwards. The moment the first token lands the count freezes into the
96
+ * exact value. With no request in flight there is nothing to show.
97
+ *
98
+ * @param now - The clock, passed in so every branch stays a function of its arguments.
99
+ */
100
+ export function ttftDisplay(
101
+ requestAt: number | null,
102
+ firstTokenAt: number | null,
103
+ now: number,
104
+ ): TtftDisplay | null {
105
+ if (requestAt === null) return null
106
+ if (firstTokenAt === null) {
107
+ if (now < requestAt) return null
108
+ return { text: `~${formatLatency(now - requestAt)}`, live: true }
109
+ }
110
+ const measured = ttftMs(requestAt, firstTokenAt)
111
+ return measured === null ? null : { text: formatLatency(measured), live: false }
112
+ }
113
+
69
114
  /** Home-relative path, or the absolute path when it is outside the home directory. */
70
115
  export function formatCwd(cwd: string): string {
71
116
  const home = homedir()