pi-midcompact 0.4.0 → 0.5.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/src/projection.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { CompressionBlock, CompressionState, MessageLike } from "./types.js";
1
+ // Projection: replaces an exact persisted messageKeys subsequence with a
2
+ // midcompact summary message and fails open when a sequence cannot be resolved.
3
+ // Owns factual replacement-size calculation based on the actual summary message
4
+ // wrapper text, not a fixed token heuristic.
5
+
6
+ import type { CompressionBlock, CompressionState, ContentMetrics, MessageLike } from "./types.js";
7
+ import { aggregateMetrics, measureMessage } from "./content-metrics.js";
2
8
  import { approxTokens, messageKey } from "./messages.js";
3
9
 
4
10
  export function summaryMessage(block: CompressionBlock, timestamp = Date.now()): MessageLike {
@@ -56,6 +62,42 @@ function findSubsequence(haystack: string[], needle: string[]): { start: number;
56
62
  return undefined;
57
63
  }
58
64
 
65
+ /**
66
+ * Measure the actual replacement message produced for a block, so callers can
67
+ * report factual replacement content chars (midcompact wrapper + summary).
68
+ */
69
+ export function replacementMetrics(block: CompressionBlock): ContentMetrics {
70
+ return measureMessage(summaryMessage(block));
71
+ }
72
+
73
+ /**
74
+ * Factual replacement content chars for a range, given its topic and summary.
75
+ * Computed from the actual summary message wrapper text that projection emits.
76
+ */
77
+ export function replacementContentChars(summary: string, topic?: string): number {
78
+ const block: CompressionBlock = {
79
+ id: "draft",
80
+ summary,
81
+ topic,
82
+ entryIds: [],
83
+ messageKeys: [],
84
+ createdAt: new Date().toISOString(),
85
+ originalContentChars: 0,
86
+ originalImageCount: 0,
87
+ originalImagePayloadBytes: 0,
88
+ replacementContentChars: 0,
89
+ originalApproxTokens: 0,
90
+ compressedApproxTokens: 0,
91
+ };
92
+ return replacementMetrics(block).contentChars;
93
+ }
94
+
95
+ /** @deprecated legacy heuristic; kept only for old approximate-token field compat. */
59
96
  export function estimateCompressedTokens(summary: string, topic?: string): number {
60
97
  return approxTokens(`${topic ?? ""}\n${summary}`) + 40;
61
98
  }
99
+
100
+ /** Factual metrics for a set of atoms that a range/block would replace. */
101
+ export function rangeMetricsForAtoms(atoms: readonly { metrics: ContentMetrics }[]): ContentMetrics {
102
+ return aggregateMetrics(atoms.map((atom) => atom.metrics));
103
+ }
package/src/renderers.ts CHANGED
@@ -14,28 +14,27 @@ export function registerStateRenderer(pi: ExtensionAPI): void {
14
14
  return box;
15
15
  }
16
16
  const commit = state.lastCommit;
17
- const saved = state.blocks.reduce(
18
- (sum, block) => sum + Math.max(0, block.originalApproxTokens - block.compressedApproxTokens),
19
- 0,
20
- );
17
+ const originalChars = state.blocks.reduce((sum, block) => sum + (block.originalContentChars ?? 0), 0);
18
+ const replacementChars = state.blocks.reduce((sum, block) => sum + (block.replacementContentChars ?? 0), 0);
19
+ const imageCount = state.blocks.reduce((sum, block) => sum + (block.originalImageCount ?? 0), 0);
21
20
  const added = commit?.addedRangeCount ?? 0;
22
21
  const headline = [
23
22
  theme.fg("success", "✓ MIDCOMPACT"),
24
23
  added ? `+${added} range${added === 1 ? "" : "s"}` : `${state.blocks.length} active block${state.blocks.length === 1 ? "" : "s"}`,
25
24
  `${state.blocks.length} active`,
26
- `~${formatTokenCount(saved)} saved`,
27
- ].join(" · ");
25
+ `${originalChars} → ${replacementChars} content chars`,
26
+ imageCount ? `${imageCount} images` : "",
27
+ ].filter(Boolean).join(" · ");
28
28
  box.addChild(new Text(headline, 0, 0));
29
29
 
30
- if (commit?.anchorUsage?.contextWindow && commit.projectedTokens !== null) {
30
+ if (commit?.anchorUsage?.contextWindow) {
31
+ // Pi-reported awareness only; no local projected token percentage claim.
31
32
  box.addChild(new Text(
32
33
  theme.fg(
33
34
  "dim",
34
- `anchor ${formatPercent(commit.anchorUsage.percent)} projected ${formatPercent(commit.projectedPercent, true)} ` +
35
- `(~${formatTokenCount(commit.projectedTokens)}/${formatTokenCount(commit.anchorUsage.contextWindow)})`,
35
+ `anchor ${formatPercent(commit.anchorUsage.percent)} [Pi reported] · ${commit.selectedOriginalContentChars} → ${commit.selectedReplacementContentChars} chars · ${commit.selectedImageCount} images`,
36
36
  ),
37
- 0,
38
- 0,
37
+ 0, 0,
39
38
  ));
40
39
  }
41
40
 
@@ -43,7 +42,7 @@ export function registerStateRenderer(pi: ExtensionAPI): void {
43
42
  const addedSet = new Set(commit?.addedBlockIds ?? []);
44
43
  const blocks = addedSet.size ? state.blocks.filter((block) => addedSet.has(block.id)) : state.blocks;
45
44
  for (const block of blocks) {
46
- const title = `${block.id}${block.topic ? ` · ${block.topic}` : ""} · ~${formatTokenCount(block.originalApproxTokens)} → ~${formatTokenCount(block.compressedApproxTokens)}`;
45
+ const title = `${block.id}${block.topic ? ` · ${block.topic}` : ""} · ${block.originalContentChars ?? 0} → ${block.replacementContentChars ?? 0} chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}`;
47
46
  box.addChild(new Text(theme.fg("accent", title), 0, 0));
48
47
  box.addChild(new Text(theme.fg("dim", block.summary), 1, 0));
49
48
  }
@@ -56,12 +55,10 @@ export function registerStateRenderer(pi: ExtensionAPI): void {
56
55
  export function stateTreeLabel(state: CompressionState): string {
57
56
  const commit = state.lastCommit;
58
57
  const added = commit?.addedRangeCount ?? 0;
59
- const saved = commit?.estimatedSavedTokens ?? state.blocks.reduce(
60
- (sum, block) => sum + Math.max(0, block.originalApproxTokens - block.compressedApproxTokens),
61
- 0,
62
- );
63
- const projection = commit?.projectedPercent === null || commit?.projectedPercent === undefined
58
+ const replacementChars = state.blocks.reduce((sum, block) => sum + (block.replacementContentChars ?? 0), 0);
59
+ const originalChars = state.blocks.reduce((sum, block) => sum + (block.originalContentChars ?? 0), 0);
60
+ const anchor = commit?.anchorUsage?.percent === null || commit?.anchorUsage?.percent === undefined
64
61
  ? ""
65
- : ` · →~${formatPercent(commit.projectedPercent, false)}`;
66
- return `midcompact${added ? ` +${added}` : ""} · ~${formatTokenCount(saved)} saved${projection}`;
62
+ : ` · anchor ${formatPercent(commit.anchorUsage.percent)} [Pi]`;
63
+ return `midcompact${added ? ` +${added}` : ""} · ${originalChars} ${replacementChars} chars${anchor}`;
67
64
  }
package/src/review-ui.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
3
3
 
4
- import { formatPercent, formatTokenCount } from "./telemetry.js";
5
4
  import type { Atom, DraftPlan, DraftRange, DraftTelemetry, ReviewAction } from "./types.js";
6
5
 
7
6
  /**
@@ -63,22 +62,22 @@ export async function showReviewUi(
63
62
  if (head) {
64
63
  rangeLineStarts.set(head.id, body.length);
65
64
  const isSel = Boolean(range && range.id === head.id);
66
- const save = Math.max(0, head.originalApproxTokens - head.compressedApproxTokens);
65
+ const savedChars = Math.max(0, head.originalContentChars - head.replacementContentChars);
67
66
  const atomCount = head.endIndex - head.startIndex + 1;
68
67
  const caret = isSel ? (expandSelected ? "▾" : "▸") : "▸";
69
68
  const caretCol = isSel ? accent(caret) : dim(caret);
70
69
  const idCol = isSel ? theme.bold(warning(head.id)) : warning(head.id);
71
70
  const span = dim(`${head.startRef}→${head.endRef} · ${atomCount} atoms`);
72
- const tok = dim(`~${formatTokenCount(head.originalApproxTokens)}→~${formatTokenCount(head.compressedApproxTokens)}`);
73
- const saveCol = success(`save ~${formatTokenCount(save)}`);
71
+ const chars = dim(`${formatCount(head.originalContentChars)}→${formatCount(head.replacementContentChars)} chars`);
72
+ const saved = success(`−${formatCount(savedChars)}`);
74
73
  const topicCol = head.topic ? `${accent(head.topic)} ` : "";
75
74
  const sumCol = dim(firstLine(head.summary, 48));
76
- const headText = `${caretCol} ${idCol} ${span} ${tok} ${saveCol} ${topicCol}${sumCol}`;
75
+ const headText = `${caretCol} ${idCol} ${span} ${chars} ${saved} ${topicCol}${sumCol}`;
77
76
  body.push(framed(isSel ? accent(headText) : headText));
78
77
 
79
78
  if (isSel) {
80
79
  body.push(framed(dim(` topic: ${head.topic ?? "—"}`)));
81
- body.push(framed(dim(` tokens: ~${formatTokenCount(head.originalApproxTokens)} → ~${formatTokenCount(head.compressedApproxTokens)} (save ~${formatTokenCount(save)})`)));
80
+ body.push(framed(dim(` chars: ${formatCount(head.originalContentChars)} → ${formatCount(head.replacementContentChars)} (−${formatCount(savedChars)}) · ${head.originalImageCount} images`)));
82
81
  const wrapWidth = Math.max(10, inner - 6);
83
82
  for (const line of wrapTextWithAnsi(`${accent("summary:")} ${head.summary}`, wrapWidth)) {
84
83
  body.push(framed(` ${line}`));
@@ -99,9 +98,9 @@ export async function showReviewUi(
99
98
  : "│"
100
99
  : " ";
101
100
  const policy = owner ? warning(owner.id) : success("KEEP");
102
- const tok = dim(`~${formatTokenCount(atom.approxTokens)}`);
101
+ const facts = dim(`${formatCount(atom.metrics.contentChars)} chars${atom.metrics.imageCount ? ` · ${atom.metrics.imageCount} img` : ""}`);
103
102
  const oneLine = dim(firstLine(atom.preview, 80));
104
- body.push(framed(`${mark} ${policy} ${atom.ref} [${atom.kind}] ${tok} ${oneLine}`));
103
+ body.push(framed(`${mark} ${policy} ${atom.ref} [${atom.kind}] ${facts} ${oneLine}`));
105
104
  }
106
105
  if (!body.length) body.push(framed(dim("(No atoms in anchor snapshot.)")));
107
106
 
@@ -203,7 +202,7 @@ export function buildReviewText(atoms: Atom[], draft: DraftPlan, telemetry: Draf
203
202
  ];
204
203
  for (const atom of atoms) {
205
204
  const owner = owningRange(atom.index, draft.ranges);
206
- lines.push(`${owner ? owner.id : "KEEP"} ${atom.ref} [${atom.kind}] ~${formatTokenCount(atom.approxTokens)} ${firstLine(atom.preview, 120)}`);
205
+ lines.push(`${owner ? owner.id : "KEEP"} ${atom.ref} [${atom.kind}] ${formatCount(atom.metrics.contentChars)} chars${atom.metrics.imageCount ? ` · ${atom.metrics.imageCount} images` : ""} ${firstLine(atom.preview, 120)}`);
207
206
  }
208
207
  if (draft.ranges.length) {
209
208
  lines.push("", "Proposed summaries:");
@@ -227,13 +226,15 @@ function hint(text: string, theme: ExtensionCommandContext["ui"]["theme"]): stri
227
226
  }
228
227
 
229
228
  function plainUsageLine(telemetry: DraftTelemetry): string {
230
- const anchor = telemetry.contextWindow === null
231
- ? "anchor usage unavailable"
232
- : `${formatTokenCount(telemetry.anchorTokens)}/${formatTokenCount(telemetry.contextWindow)} (${formatPercent(telemetry.anchorPercent)})`;
233
- const projected = telemetry.projectedTokens === null || telemetry.contextWindow === null
234
- ? "projected unavailable"
235
- : `~${formatTokenCount(telemetry.projectedTokens)}/${formatTokenCount(telemetry.contextWindow)} (${formatPercent(telemetry.projectedPercent, true)})`;
236
- return `Anchor ${anchor} · Draft selected ~${formatTokenCount(telemetry.selectedOriginalApproxTokens)}→~${formatTokenCount(telemetry.selectedCompressedApproxTokens)} · Projected ${projected}`;
229
+ const usage = telemetry.anchorUsage;
230
+ const anchor = usage
231
+ ? `${usage.tokens === null ? "unavailable" : formatCount(usage.tokens)}/${formatCount(usage.contextWindow)} tokens (${usage.percent === null ? "unavailable" : `${usage.percent}%`}, Pi reported)`
232
+ : "Pi reported anchor usage unavailable";
233
+ return `Anchor ${anchor} · Draft ${telemetry.rangeCount} ranges · ${formatCount(telemetry.selectedOriginalContentChars)}→${formatCount(telemetry.selectedReplacementContentChars)} chars · ${telemetry.selectedImageCount} images · ${telemetry.pendingSummaryCount} pending`;
234
+ }
235
+
236
+ function formatCount(value: number): string {
237
+ return value.toLocaleString("en-US");
237
238
  }
238
239
 
239
240
  function usageLine(telemetry: DraftTelemetry, theme: ExtensionCommandContext["ui"]["theme"]): string {