useful-pi-extensions 1.1.2 → 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.2",
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,7 @@ import {
39
39
  pair,
40
40
  row,
41
41
  shortenPath,
42
+ ttftDisplay,
42
43
  ttftMs,
43
44
  USD,
44
45
  type Currency,
@@ -47,6 +48,8 @@ import {
47
48
  const LIVE_RENDER_MS = 200
48
49
  const WINDOW_MS = 2000
49
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
50
53
  /** Pi's own estimateTokens() heuristic, used only until a real ratio is known. */
51
54
  const FALLBACK_TOKENS_PER_CHAR = 0.25
52
55
 
@@ -188,6 +191,7 @@ export default function (pi: ExtensionAPI) {
188
191
  let chars = 0
189
192
  let requestAt: number | null = null
190
193
  let firstTokenAt: number | null = null
194
+ let ticker: ReturnType<typeof setInterval> | null = null
191
195
  let windowAt = 0
192
196
  let windowTokens = 0
193
197
  let renderedAt = 0
@@ -205,6 +209,26 @@ export default function (pi: ExtensionAPI) {
205
209
  requestRender?.()
206
210
  }
207
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
+
208
232
  function installFooter(ctx: ExtensionContext): void {
209
233
  ctx.ui.setFooter((tui, theme, footerData: ReadonlyFooterDataProvider) => {
210
234
  requestRender = () => tui.requestRender()
@@ -236,7 +260,12 @@ export default function (pi: ExtensionAPI) {
236
260
  const model = ctx.model?.id ?? 'no model'
237
261
  const row2Parts = [theme.fg('accent', model)]
238
262
  if (ctx.thinkingLevel) row2Parts.push(pair(theme, 'Effort', ctx.thinkingLevel, 'muted'))
239
- 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) {
240
269
  if (reading.ttftMs !== null)
241
270
  row2Parts.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs), 'muted'))
242
271
  row2Parts.push(
@@ -284,10 +313,28 @@ export default function (pi: ExtensionAPI) {
284
313
  })
285
314
  }
286
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
+
287
333
  pi.on('session_start', async (_event, ctx) => {
288
334
  ratio = seedRatio(ctx)
289
335
  reading = null
290
336
  requestAt = null
337
+ stopTicker()
291
338
  resetStream()
292
339
  currency = await loadCurrency((message) => ctx.ui.notify(message, 'warning'))
293
340
  installFooter(ctx)
@@ -309,6 +356,16 @@ export default function (pi: ExtensionAPI) {
309
356
  requestAt = Date.now()
310
357
  })
311
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()
367
+ })
368
+
312
369
  pi.on('message_update', async (event) => {
313
370
  const delta = event.assistantMessageEvent
314
371
  // Narrowing on the discriminant is what keeps the payload typed; membership in
@@ -322,7 +379,10 @@ export default function (pi: ExtensionAPI) {
322
379
  }
323
380
 
324
381
  const now = Date.now()
325
- if (firstTokenAt === null) firstTokenAt = now
382
+ if (firstTokenAt === null) {
383
+ firstTokenAt = now
384
+ stopTicker()
385
+ }
326
386
  chars += delta.delta.length
327
387
 
328
388
  const tokens = chars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
@@ -357,6 +417,9 @@ export default function (pi: ExtensionAPI) {
357
417
  const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
358
418
  if (measured !== null && decodeMs >= MIN_SAMPLE_MS)
359
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
360
423
  resetStream()
361
424
  })
362
425
  }
@@ -81,6 +81,36 @@ export function ttftMs(requestAt: number | null, firstTokenAt: number | null): n
81
81
  return firstTokenAt > requestAt ? firstTokenAt - requestAt : null
82
82
  }
83
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
+
84
114
  /** Home-relative path, or the absolute path when it is outside the home directory. */
85
115
  export function formatCwd(cwd: string): string {
86
116
  const home = homedir()