useful-pi-extensions 1.11.1 → 1.11.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.11.1",
3
+ "version": "1.11.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, plus an opt-in context breakdown panel.",
5
5
  "keywords": [
6
6
  "bun",
@@ -38,10 +38,10 @@ import {
38
38
  formatCwd,
39
39
  formatLatency,
40
40
  formatTps,
41
+ frozenThroughput,
41
42
  avgMs,
42
43
  isQuietStatus,
43
44
  messagesTokens,
44
- pair,
45
45
  cachedRates,
46
46
  ratesFromPayload,
47
47
  cacheIsFresh,
@@ -51,6 +51,7 @@ import {
51
51
  row,
52
52
  shortenPath,
53
53
  splitToolTokens,
54
+ timingRow,
54
55
  ttftDisplay,
55
56
  ttftMs,
56
57
  USD,
@@ -400,8 +401,9 @@ export default function (pi: ExtensionAPI) {
400
401
  let requestRender: (() => void) | null = null
401
402
  let currency: Currency = USD
402
403
 
403
- // Latest turn reading: { rate, exact, ttftMs }
404
- let reading: { rate: number; exact: boolean; ttftMs: number | null } | null = null
404
+ // Latest turn reading: { rate, exact, ttftMs }. rate is null when the message arrived as one
405
+ // burst: the honest TTFT stays, the unmeasurable throughput goes.
406
+ let reading: { rate: number | null; exact: boolean; ttftMs: number | null } | null = null
405
407
 
406
408
  // Breakdown-line inputs, refreshed once per turn rather than per frame. The prompt and the
407
409
  // tool list change only across turns (before_agent_start re-captures both), and the message
@@ -495,7 +497,7 @@ export default function (pi: ExtensionAPI) {
495
497
  renderedAt = 0
496
498
  }
497
499
 
498
- function publish(rate: number, exact: boolean, ttft: number | null): void {
500
+ function publish(rate: number | null, exact: boolean, ttft: number | null): void {
499
501
  reading = { rate, exact, ttftMs: ttft }
500
502
  requestRender?.()
501
503
  }
@@ -555,9 +557,9 @@ export default function (pi: ExtensionAPI) {
555
557
  totalMeasuredOutput = num(state.totalMeasuredOutput)
556
558
  totalTtftMs = num(state.totalTtftMs)
557
559
  ttftCount = num(state.ttftCount)
558
- if (isRecord(state.last) && typeof state.last.rate === 'number') {
560
+ if (isRecord(state.last)) {
559
561
  reading = {
560
- rate: state.last.rate,
562
+ rate: typeof state.last.rate === 'number' ? state.last.rate : null,
561
563
  exact: state.last.exact === true,
562
564
  ttftMs: typeof state.last.ttftMs === 'number' ? state.last.ttftMs : null,
563
565
  }
@@ -620,40 +622,30 @@ export default function (pi: ExtensionAPI) {
620
622
  })
621
623
  }
622
624
 
623
- // Row 2: model and the latest turn's timing on the right, path on the left. Three
624
- // groups — identity, first token, throughput — separated by a wall instead of another
625
- // dot, because a run of similar-looking pairs is what made the old footer unreadable.
626
- const model = ctx.model?.id ?? 'no model'
627
- const separator = theme.fg('dim', ' · ')
628
- const wall = theme.fg('dim', ' | ')
629
- const identity = [theme.fg('accent', model)]
630
- if (ctx.thinkingLevel) identity.push(pair(theme, 'Effort', ctx.thinkingLevel))
631
-
632
- const ttft: string[] = []
625
+ // Row 2: model and the latest turn's timing on the right, path on the left. The right
626
+ // side assembles through timingRow's drop ladder, so a narrow terminal loses whole
627
+ // slots — never a half-rendered one.
633
628
  const waiting = ttftDisplay(requestAt, firstTokenAt, Date.now())
634
- if (waiting !== null) {
635
- // The clock is running: this wait has no reading yet, so the previous turn's
636
- // numbers would only be mistaken for the current one.
637
- ttft.push(pair(theme, 'TTFT', waiting.text))
638
- } else if (reading) {
639
- if (reading.ttftMs !== null)
640
- ttft.push(pair(theme, 'TTFT', formatLatency(reading.ttftMs)))
641
- }
629
+ // The clock is running: this wait has no reading yet, so the previous turn's numbers
630
+ // would only be mistaken for the current one.
631
+ const ttft =
632
+ waiting !== null
633
+ ? waiting.text
634
+ : reading && reading.ttftMs !== null
635
+ ? formatLatency(reading.ttftMs)
636
+ : null
642
637
  const avgTtft = avgMs(totalTtftMs, ttftCount)
643
- if (avgTtft !== null) ttft.push(pair(theme, 'Avg TTFT', formatLatency(avgTtft)))
644
-
645
- const throughput: string[] = []
646
- if (reading) {
647
- throughput.push(
648
- pair(theme, 'Last', `${reading.exact ? '' : '~'}${formatTps(reading.rate)} tok/s`),
649
- )
650
- }
651
- if (avg !== null) throughput.push(pair(theme, 'Avg', `${formatTps(avg)} tok/s`))
652
-
653
- const row2Right = [identity, ttft, throughput]
654
- .filter((group) => group.length > 0)
655
- .map((group) => group.join(separator))
656
- .join(wall)
638
+ const row2Right = timingRow(theme, width, {
639
+ model: ctx.model?.id ?? 'no model',
640
+ effort: ctx.thinkingLevel ?? null,
641
+ ttft,
642
+ avgTtft: avgTtft !== null ? formatLatency(avgTtft) : null,
643
+ last:
644
+ reading && reading.rate !== null
645
+ ? `${reading.exact ? '' : '~'}${formatTps(reading.rate)} tok/s`
646
+ : null,
647
+ avg: avg !== null ? `${formatTps(avg)} tok/s` : null,
648
+ })
657
649
  const branch = footerData.getGitBranch()
658
650
  const path = formatCwd(ctx.cwd)
659
651
  const branchSuffix = branch ? ` (${branch})` : ''
@@ -828,7 +820,8 @@ export default function (pi: ExtensionAPI) {
828
820
  const decodeMs = firstTokenAt !== null ? Date.now() - firstTokenAt : 0
829
821
  const measured = ttftMs(requestAt, firstTokenAt)
830
822
  const tokens = output > 0 ? output : totalChars * (ratio ?? FALLBACK_TOKENS_PER_CHAR)
831
- if (measured !== null && decodeMs >= MIN_SAMPLE_MS) {
823
+ const rate = frozenThroughput(decodeMs, tokens, MIN_SAMPLE_MS)
824
+ if (measured !== null && rate !== null) {
832
825
  // The average's numerator and denominator must cover the same messages: totals.output spans
833
826
  // the whole session (a reload replays none of it), so pairing it with the partial denominator
834
827
  // below once produced an Avg of 4324 tok/s.
@@ -837,11 +830,14 @@ export default function (pi: ExtensionAPI) {
837
830
  totalTtftMs += measured
838
831
  ttftCount += 1
839
832
  persist(ctx.sessionManager.getSessionFile() ?? null)
840
- publish((tokens / decodeMs) * 1000, output > 0, measured)
841
- } else if (measured !== null && decodeMs > 0) {
842
- // The frozen Last must be this message's whole-message rate, never the last live
843
- // sliding-window sample: a buffered burst of chunks mid-stream reads triple.
844
- publish((tokens / decodeMs) * 1000, output > 0, measured)
833
+ publish(rate, output > 0, measured)
834
+ } else if (measured !== null) {
835
+ // A sub-floor window is a burst arrival: the message landed in one or two chunks and there
836
+ // is no decode phase to measure — dividing by those milliseconds once printed Last 82000
837
+ // tok/s. Keep the honest TTFT, publish no rate; the live sliding-window sample is no
838
+ // fallback either, a buffered burst mid-stream reads triple there too.
839
+ publish(null, false, measured)
840
+ persist(ctx.sessionManager.getSessionFile() ?? null)
845
841
  }
846
842
  // Null it with the stream: a request that has produced its message is no longer in flight, and
847
843
  // a stale anchor would let the live branch count against nothing until the next turn.
@@ -85,6 +85,29 @@ export function ttftMs(requestAt: number | null, firstTokenAt: number | null): n
85
85
  return firstTokenAt > requestAt ? firstTokenAt - requestAt : null
86
86
  }
87
87
 
88
+ /**
89
+ * The frozen Last throughput for a finished message: its whole-message rate, output tokens over the
90
+ * entire decode window.
91
+ *
92
+ * A decode window below the sample floor is a burst arrival — the whole message landed in one or
93
+ * two chunks, so there is no decode phase to measure. Dividing by those couple of milliseconds once
94
+ * printed `Last 82000 tok/s` for an 82-token greeting; like the negative TTFT above, an
95
+ * unmeasurable rate reports absence rather than a confident fake.
96
+ *
97
+ * @param decodeMs - Milliseconds from the first streamed delta to message end.
98
+ * @param tokens - Output tokens for the message (provider-reported when available).
99
+ * @param minSampleMs - The shortest window considered a real decode phase.
100
+ * @returns The tokens/second rate, or null when the window is too short to measure.
101
+ */
102
+ export function frozenThroughput(
103
+ decodeMs: number,
104
+ tokens: number,
105
+ minSampleMs: number,
106
+ ): number | null {
107
+ if (decodeMs < minSampleMs) return null
108
+ return (tokens / decodeMs) * 1000
109
+ }
110
+
88
111
  /** What the TTFT slot renders, and whether the clock is still running. */
89
112
  export interface TtftDisplay {
90
113
  text: string
@@ -419,12 +442,13 @@ export interface ContextRowParts {
419
442
  * Row 1: context pressure on the left, session totals on the right.
420
443
  *
421
444
  * Space is given up in a fixed order rather than all at once, because this row carries numbers that
422
- * appear nowhere else in the footer. A narrower terminal should cost the least load-bearing of them
423
- * visibly rather than silently dropping the whole group:
445
+ * appear nowhere else in the footer. A narrower terminal costs, in turn:
424
446
  *
425
447
  * 1. The absolute `tokens / window` detail,
426
- * 2. The input/output volumes, which are informative but not cumulative bills,
427
- * 3. The right group entirely (`row` then keeps the left alone).
448
+ * 2. The input/output volumes, which are informative but not bills,
449
+ * 3. The cache-hit rate, which is diagnostic rather than money,
450
+ * 4. Today's running total (the session's own bill outlives it),
451
+ * 5. The bar's own cells, down to a stub — the percent survives everything.
428
452
  *
429
453
  * @param currency - Used for the cost; defaults to USD so the row renders without a config file.
430
454
  */
@@ -441,7 +465,9 @@ export function contextRow(
441
465
  parts.percent === null ? '--' : `${Math.round(parts.percent)}%`,
442
466
  )
443
467
  // The title is Claude Code's: "Context window", so the two panels read the same at a glance.
444
- const meter = `${theme.fg('dim', 'Context window')} ${bar(theme, BAR_CELLS, fraction)} ${percent}`
468
+ const meterWith = (cells: number): string =>
469
+ `${theme.fg('dim', 'Context window')} ${bar(theme, cells, fraction)} ${percent}`
470
+ const meter = meterWith(BAR_CELLS)
445
471
  const detail =
446
472
  parts.window > 0 ? `${formatTokens(parts.tokens)} / ${formatTokens(parts.window)}` : ''
447
473
  const withDetail = detail === '' ? meter : `${meter} ${theme.fg('dim', detail)}`
@@ -475,9 +501,97 @@ export function contextRow(
475
501
  const fits = (left: string, right: string): boolean =>
476
502
  right === '' || visibleWidth(left) + 2 + visibleWidth(right) <= width
477
503
 
478
- if (fits(withDetail, full)) return row(theme, width, withDetail, full)
479
- if (fits(meter, full)) return row(theme, width, meter, full)
480
- return row(theme, width, meter, core)
504
+ const moneyOnly = money.join(separator)
505
+ // The session's own bill outlives today's running total.
506
+ const ownBill = money.length > 0 ? (money[0] ?? '') : ''
507
+
508
+ // The ladder, highest fidelity first: the detail, the volumes, the cache diagnostics and
509
+ // today's total each give up their seat before the bar itself shrinks — twenty cells of bar
510
+ // are a luxury the bills should not have to pay for.
511
+ const compact = 10
512
+ const rungs: Array<{ left: string; right: string }> = [
513
+ { left: withDetail, right: full },
514
+ { left: meter, right: full },
515
+ { left: meter, right: core },
516
+ { left: meter, right: moneyOnly },
517
+ { left: meterWith(compact), right: moneyOnly },
518
+ { left: meterWith(compact), right: ownBill },
519
+ ]
520
+ for (const { left, right } of rungs) {
521
+ if (right === '' ? visibleWidth(left) <= width : fits(left, right)) {
522
+ return row(theme, width, left, right)
523
+ }
524
+ }
525
+
526
+ // Floor: the percent is the one number this row exists to show — the bar shrinks to a stub
527
+ // before the percent is ever truncated, and past that the label goes, never the number.
528
+ const stub = Math.min(compact, width - 22)
529
+ if (stub >= 4) return row(theme, width, meterWith(stub), '')
530
+ if (width >= 18) {
531
+ return row(theme, width, `${theme.fg('dim', 'Context window')} ${percent}`, '')
532
+ }
533
+ return truncateToWidth(percent, width, theme.fg('dim', '…'))
534
+ }
535
+
536
+ /** The row-2 right side, the caller pre-formatting each slot's display text. */
537
+ export interface TimingRowParts {
538
+ model: string
539
+ /** Thinking level label, or null to hide the slot. */
540
+ effort: string | null
541
+ /** The live TTFT clock or the frozen reading, or null to hide the slot. */
542
+ ttft: string | null
543
+ /** Session-average TTFT, or null to hide the slot. */
544
+ avgTtft: string | null
545
+ /** Last turn's decode rate, or null to hide the slot. */
546
+ last: string | null
547
+ /** Session-average decode rate, or null to hide the slot. */
548
+ avg: string | null
549
+ }
550
+
551
+ /**
552
+ * Row 2's right side: the model, the effort level, and the latest turn's timing.
553
+ *
554
+ * Like row 1, space is given up in a fixed order rather than half rendered. A narrower terminal
555
+ * costs, in turn: the throughput average, the TTFT average, the effort level, the last rate —
556
+ * aggregates before live numbers, static config before measured history. The model and the TTFT
557
+ * clock are the floor; past them the line truncates, because a model id is the one string here with
558
+ * no drop-in substitute.
559
+ *
560
+ * @returns The assembled right side, at most `width` cells wide.
561
+ */
562
+ export function timingRow(theme: Theme, width: number, parts: TimingRowParts): string {
563
+ const separator = theme.fg('dim', ' · ')
564
+ const wall = theme.fg('dim', ' | ')
565
+ const slots = {
566
+ model: theme.fg('accent', parts.model),
567
+ effort: parts.effort !== null ? pair(theme, 'Effort', parts.effort) : null,
568
+ ttft: parts.ttft !== null ? pair(theme, 'TTFT', parts.ttft) : null,
569
+ avgTtft: parts.avgTtft !== null ? pair(theme, 'Avg TTFT', parts.avgTtft) : null,
570
+ last: parts.last !== null ? pair(theme, 'Last', parts.last) : null,
571
+ avg: parts.avg !== null ? pair(theme, 'Avg', parts.avg) : null,
572
+ }
573
+
574
+ type Droppable = 'effort' | 'ttft' | 'avgTtft' | 'last' | 'avg'
575
+ const hidden = new Set<Droppable>()
576
+ const assemble = (): string =>
577
+ [
578
+ [slots.model, hidden.has('effort') ? null : slots.effort],
579
+ [hidden.has('ttft') ? null : slots.ttft, hidden.has('avgTtft') ? null : slots.avgTtft],
580
+ [hidden.has('last') ? null : slots.last, hidden.has('avg') ? null : slots.avg],
581
+ ]
582
+ .map((group) => group.filter((slot): slot is string => slot !== null))
583
+ .filter((group) => group.length > 0)
584
+ .map((group) => group.join(separator))
585
+ .join(wall)
586
+
587
+ const ladder: Droppable[] = ['avg', 'avgTtft', 'effort', 'last']
588
+ let right = assemble()
589
+ for (const key of ladder) {
590
+ if (visibleWidth(right) <= width) break
591
+ hidden.add(key)
592
+ right = assemble()
593
+ }
594
+ return truncateToWidth(right, width, theme.fg('dim', '…'))
481
595
  }
482
596
 
483
597
  /** `label value` where the key is quiet and the value is not. */