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/README.md +16 -7
- package/README.zh-CN.md +16 -7
- package/package.json +2 -2
- package/skills/midcompact/SKILL.md +87 -72
- package/skills/midcompact/references/tool-interface.md +83 -0
- package/src/atoms.ts +31 -7
- package/src/content-metrics.ts +185 -0
- package/src/index.ts +487 -196
- package/src/inventory.ts +295 -0
- package/src/messages.ts +56 -1
- package/src/plan.ts +176 -20
- package/src/planning-lock.ts +42 -0
- package/src/projection.ts +43 -1
- package/src/renderers.ts +16 -19
- package/src/review-ui.ts +17 -16
- package/src/review-webui.html +290 -73
- package/src/review-webui.ts +176 -71
- package/src/selection-ui.ts +220 -0
- package/src/selection.ts +95 -0
- package/src/start-ui.ts +57 -0
- package/src/state.ts +39 -2
- package/src/telemetry.ts +48 -18
- package/src/types.ts +141 -2
package/src/projection.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
27
|
-
|
|
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
|
|
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)}
|
|
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}` : ""} ·
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
: ` ·
|
|
66
|
-
return `midcompact${added ? ` +${added}` : ""} ·
|
|
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
|
|
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
|
|
73
|
-
const
|
|
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} ${
|
|
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(`
|
|
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
|
|
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}] ${
|
|
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}]
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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 {
|