bermudis-pi-goodies 0.26.0 → 0.27.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/README.md CHANGED
@@ -26,7 +26,7 @@ extensions. One entry point, thirteen independent features.
26
26
  After publishing the package to npm:
27
27
 
28
28
  ```bash
29
- pi install npm:bermudis-pi-goodies@0.26.0
29
+ pi install npm:bermudis-pi-goodies@0.27.0
30
30
  ```
31
31
 
32
32
  Remove any old `bermudis-pi-goodies.ts` symlink before reloading Pi. Each
@@ -373,11 +373,16 @@ session. `/side-exit` closes it and chooses what the main agent receives.
373
373
  How it works:
374
374
 
375
375
  - **Badge**: while a side session is open, the footer's model readout becomes
376
- `side: (zai) glm-5.3 • max` — the provider-balance footer prepends the
377
- badge to the stats line instead of adding a separate `side:` status line
378
- (which remains the fallback when that footer is not mounted). The readout
379
- tracks mid-side ctrl+l switches, and the badge clears on exit or when
380
- switching to a session that is not in a side consultation.
376
+ `side: (zai) glm-5.3 • max · ~46k lensed` — the provider-balance footer
377
+ prepends the badge to the stats line instead of adding a separate `side:`
378
+ status line (which remains the fallback when that footer is not mounted).
379
+ The `~46k lensed` suffix is the estimated size of the request the lens will
380
+ actually send (quote + side turns, pi's own chars/4 estimator): pi's footer
381
+ context-usage figure estimates the raw session branch, which the lens then
382
+ rewrites at the wire, so it over-reports during side sessions. The suffix
383
+ is dropped first when the line gets tight. The readout tracks mid-side
384
+ ctrl+l switches, and the badge clears on exit or when switching to a
385
+ session that is not in a side consultation.
381
386
  - **Entry**: `/side` opens pi's own model selector (falls back to a plain
382
387
  selector when pi's internal runtime is not reachable; in headless modes pass
383
388
  the model explicitly as `/side provider/model-id[:level]`). The main agent's
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bermudis-pi-goodies",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/bermudi/agent-extensions.git",
@@ -32,6 +32,7 @@ import {
32
32
  import { getKiloCatalogStatus, type KiloCatalogStatus } from "./kilo.ts";
33
33
  import { logGoodiesEvent, reportFailure } from "./goodies-log.ts";
34
34
  import {
35
+ formatSideLensSuffix,
35
36
  getSideSessionModel,
36
37
  onSideBadgeChange,
37
38
  setMergedSideBadgeInstalled,
@@ -1306,11 +1307,13 @@ const SGR_SEQUENCE = /\x1b\[[0-9;:]*m/g;
1306
1307
 
1307
1308
  /**
1308
1309
  * Prepend a badge to the right-aligned model readout of the stats line pi's
1309
- * FooterComponent produced: `side: (zai) glm-5.3 • max`. The line is built
1310
- * as `<dim statsLeft><dim padding + model>` — split at the opener of the
1311
- * final styled run (the last SGR sequence is its closer, so the opener is
1312
- * the second-to-last), keep the left half verbatim (it may carry a colored
1313
- * context percentage), and rebuild the right half around the badge.
1310
+ * FooterComponent produced: `side: (zai) glm-5.3 • max · ~46k lensed`. The
1311
+ * line is built as `<dim statsLeft><dim padding + model>` — split at the
1312
+ * opener of the final styled run (the last SGR sequence is its closer, so
1313
+ * the opener is the second-to-last), keep the left half verbatim (it may
1314
+ * carry a colored context percentage), and rebuild the right half around
1315
+ * the badge. A suffix that does not fit is dropped before the badge itself
1316
+ * is ever truncated — the model name outranks the token estimate.
1314
1317
  *
1315
1318
  * Returns undefined when the line lacks that two-run shape — the caller then
1316
1319
  * leaves the line untouched and the /side badge falls back to a status line.
@@ -1322,6 +1325,7 @@ export function mergeSideBadgeIntoStatsLine(
1322
1325
  width: number,
1323
1326
  theme: FooterTheme,
1324
1327
  prefix = "side: ",
1328
+ suffix = "",
1325
1329
  ): string | undefined {
1326
1330
  if (!line || width <= 0) return undefined;
1327
1331
  const runs = [...line.matchAll(SGR_SEQUENCE)];
@@ -1332,14 +1336,17 @@ export function mergeSideBadgeIntoStatsLine(
1332
1336
  const tail = stripTerminalSequences(line.slice(opener)).trim();
1333
1337
  if (!tail || !stripTerminalSequences(head).trim()) return undefined;
1334
1338
 
1335
- const labeled = `${prefix}${tail}`;
1339
+ const withSuffix = `${prefix}${tail}${suffix}`;
1340
+ const bare = `${prefix}${tail}`;
1336
1341
  const headWidth = visibleWidth(head);
1337
1342
  const room = width - headWidth - 2; // pi keeps ≥2 columns between the halves
1338
1343
  if (room <= 0) return undefined;
1339
1344
  const right =
1340
- visibleWidth(labeled) <= room
1341
- ? labeled
1342
- : truncateToWidth(labeled, room, "");
1345
+ visibleWidth(withSuffix) <= room
1346
+ ? withSuffix
1347
+ : visibleWidth(bare) <= room
1348
+ ? bare
1349
+ : truncateToWidth(bare, room, "");
1343
1350
  const padding = " ".repeat(
1344
1351
  Math.max(2, width - headWidth - visibleWidth(right)),
1345
1352
  );
@@ -1781,6 +1788,8 @@ export default function providerBalance(
1781
1788
  lines[1],
1782
1789
  width,
1783
1790
  theme,
1791
+ "side: ",
1792
+ formatSideLensSuffix(),
1784
1793
  );
1785
1794
  if (statsLine !== undefined) {
1786
1795
  lines[1] = statsLine;
package/side-state.ts CHANGED
@@ -54,6 +54,42 @@ export function getSideSessionModel(): SideModelRef | undefined {
54
54
  return activeModel;
55
55
  }
56
56
 
57
+ let lensTokens: number | undefined;
58
+
59
+ /**
60
+ * side.ts: estimated token size of the LENSED request (the quote + side
61
+ * turns actually sent to the side model), via pi's own chars/4 estimator
62
+ * so it stays comparable with the footer's raw-branch figure. Undefined on
63
+ * non-side branches — pi's footer estimate is correct there.
64
+ *
65
+ * Why: pi's footer context-usage and pre-send compaction threshold both
66
+ * estimate the RAW session projection; the side lens rewrites the request
67
+ * at the wire, after every estimator. The footer therefore over-reports
68
+ * while a side session is active. The badge carries this estimate as the
69
+ * honest counterpoint.
70
+ */
71
+ export function setSideLensTokens(tokens: number | undefined): void {
72
+ if (lensTokens === tokens) return;
73
+ lensTokens = tokens;
74
+ emit();
75
+ }
76
+
77
+ export function getSideLensTokens(): number | undefined {
78
+ return lensTokens;
79
+ }
80
+
81
+ /** Badge suffix for the lensed estimate: " · ~46k lensed", or "" when off. */
82
+ export function formatSideLensSuffix(): string {
83
+ if (lensTokens === undefined || lensTokens < 1_000) return "";
84
+ return ` · ~${formatSideLensTokens(lensTokens)} lensed`;
85
+ }
86
+
87
+ function formatSideLensTokens(tokens: number): string {
88
+ if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}M`;
89
+ if (tokens >= 10_000) return `${Math.round(tokens / 1_000)}k`;
90
+ return `${(tokens / 1_000).toFixed(1)}k`;
91
+ }
92
+
57
93
  /** provider-balance: its footer now owns (or handed back) the stats line. */
58
94
  export function setMergedSideBadgeInstalled(installed: boolean): void {
59
95
  if (mergedInstalled === installed) return;
@@ -88,6 +124,7 @@ export function onSideBadgeChange(listener: () => void): () => void {
88
124
  /** Test-only: restore the pristine module state (undefined/false/false). */
89
125
  export function resetSideBadgeState(): void {
90
126
  activeModel = undefined;
127
+ lensTokens = undefined;
91
128
  mergedInstalled = false;
92
129
  mergedRendered = false;
93
130
  listeners.clear();
package/side.ts CHANGED
@@ -28,9 +28,13 @@
28
28
  * limb that is ever resumed delivers only newer turns. v1 always starts a
29
29
  * fresh side limb, but the bookkeeping keeps that future-safe.
30
30
  *
31
- * Known cosmetic limitation: the footer's context-usage estimate reflects the
32
- * raw session branch, not the lens output, so it over-reports while a side
33
- * session is active.
31
+ * Known limitation: pi's footer context-usage estimate (and its pre-send
32
+ * compaction threshold) reflect the raw session branch, not the lens
33
+ * output — the footer over-reports while a side session is active, and
34
+ * premature auto-compaction is possible on large side consults (the lens
35
+ * is compaction-aware and survives it; the main limb is never touched).
36
+ * The side badge carries pi's own estimator applied to the lensed messages
37
+ * ("· ~46k lensed") as the honest counterpoint to the inflated figure.
34
38
  */
35
39
 
36
40
  import type {
@@ -43,15 +47,18 @@ import type {
43
47
  } from "@earendil-works/pi-coding-agent";
44
48
  import {
45
49
  ModelSelectorComponent,
50
+ estimateTokens,
46
51
  sessionEntryToContextMessages,
47
52
  } from "@earendil-works/pi-coding-agent";
48
53
  import { describeError, extractTextParts } from "./json-file.ts";
49
54
  import { buildTrajectory, renderTrajectory } from "./copy-trajectory.ts";
50
55
  import { logGoodiesEvent } from "./goodies-log.ts";
51
56
  import {
57
+ formatSideLensSuffix,
52
58
  getSideSessionModel,
53
59
  isMergedSideBadgeActive,
54
60
  onSideBadgeChange,
61
+ setSideLensTokens,
55
62
  setSideSessionModel,
56
63
  type SideModelRef,
57
64
  } from "./side-state.ts";
@@ -298,6 +305,22 @@ export function buildLensMessages(
298
305
  return messages;
299
306
  }
300
307
 
308
+ /**
309
+ * Estimate the token size of the lensed request pi will actually send
310
+ * (quote + side-native messages), using pi's own chars/4 estimator so the
311
+ * number is comparable with the footer's raw-branch estimate. Undefined
312
+ * when markerIdx does not mark a side session.
313
+ */
314
+ export function estimateLensedContextTokens(
315
+ awareEntries: readonly SessionEntry[],
316
+ rawBranch: readonly SessionEntry[],
317
+ markerIdx: number,
318
+ ): number | undefined {
319
+ const messages = buildLensMessages(awareEntries, rawBranch, markerIdx, []);
320
+ if (!messages) return undefined;
321
+ return messages.reduce((sum, message) => sum + estimateTokens(message), 0);
322
+ }
323
+
301
324
  // ---------------------------------------------------------------------------
302
325
  // Handoff: what the main agent is given at exit
303
326
  // ---------------------------------------------------------------------------
@@ -511,7 +534,7 @@ export function applySideStatus(ctx: ExtensionContext): void {
511
534
  const model: SideModelRef | undefined = getSideSessionModel();
512
535
  const text =
513
536
  model && !isMergedSideBadgeActive()
514
- ? `side: ${modelRef(model)}`
537
+ ? `side: ${modelRef(model)}${formatSideLensSuffix()}`
515
538
  : undefined;
516
539
  ctx.ui.setStatus("side", text);
517
540
  }
@@ -547,6 +570,26 @@ export function activeSideModel(
547
570
  return markerData.sideModel;
548
571
  }
549
572
 
573
+ /**
574
+ * Refresh the badge's lensed-context estimate from the live branch —
575
+ * undefined on non-side branches, so the suffix also clears on exit and
576
+ * navigation. O(branch text) char counting: cheap per turn, far too hot
577
+ * for per-frame footer renders (those read the cached side-state value).
578
+ */
579
+ function refreshSideLens(ctx: ExtensionContext): void {
580
+ const raw = ctx.sessionManager.getBranch();
581
+ const markerIdx = findSideBoundary(raw);
582
+ let tokens: number | undefined;
583
+ if (markerIdx !== -1) {
584
+ tokens = estimateLensedContextTokens(
585
+ ctx.sessionManager.buildContextEntries(),
586
+ raw,
587
+ markerIdx,
588
+ );
589
+ }
590
+ setSideLensTokens(tokens);
591
+ }
592
+
550
593
  async function summarizeDelta(
551
594
  ctx: ExtensionCommandContext,
552
595
  sideModel: ModelRef,
@@ -627,6 +670,7 @@ export default function side(pi: ExtensionAPI): void {
627
670
  setSideSessionModel(undefined);
628
671
  applySideStatus(ctx);
629
672
  }
673
+ refreshSideLens(ctx);
630
674
  });
631
675
 
632
676
  // Restore the badge when a session resumes already on a side limb. The
@@ -650,8 +694,17 @@ export default function side(pi: ExtensionAPI): void {
650
694
  setSideSessionModel(undefined);
651
695
  applySideStatus(ctx);
652
696
  }
697
+ refreshSideLens(ctx);
653
698
  });
654
699
 
700
+ // The lensed-context estimate the badge carries: the quote is frozen at
701
+ // entry, so it only moves when the side limb grows, compaction rewrites
702
+ // the view, or navigation changes the limb. Non-side events are a cheap
703
+ // marker scan that ends in a no-op set.
704
+ pi.on("turn_start", (_event, ctx) => refreshSideLens(ctx));
705
+ pi.on("message_end", (_event, ctx) => refreshSideLens(ctx));
706
+ pi.on("session_compact", (_event, ctx) => refreshSideLens(ctx));
707
+
655
708
  // The footer-rendered badge needs a re-render when side state changes;
656
709
  // the status-line fallback needs re-applying when the merged footer
657
710
  // appears or disappears (install order between the two modules is not