pi-midcompact 0.3.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 +117 -42
- package/README.zh-CN.md +236 -0
- package/figures/review-tui.png +0 -0
- package/figures/review-webui.png +0 -0
- package/package.json +3 -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 +526 -207
- 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 +199 -155
- package/src/review-webui.html +1063 -0
- package/src/review-webui.ts +317 -0
- 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,9 +1,14 @@
|
|
|
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
|
|
|
6
|
+
/**
|
|
7
|
+
* Interactive TUI review. Rendered as a centered overlay (not a full-height
|
|
8
|
+
* editor replacement) so it never flattens the chat transcript. Ranges fold
|
|
9
|
+
* into single header lines; the selected range expands inline with its detail
|
|
10
|
+
* and edit affordances, removing the duplicate top "Selected" block.
|
|
11
|
+
*/
|
|
7
12
|
export async function showReviewUi(
|
|
8
13
|
ctx: ExtensionCommandContext,
|
|
9
14
|
atoms: Atom[],
|
|
@@ -12,148 +17,180 @@ export async function showReviewUi(
|
|
|
12
17
|
): Promise<ReviewAction> {
|
|
13
18
|
if (ctx.mode !== "tui") return { action: "close" };
|
|
14
19
|
|
|
15
|
-
return ctx.ui.custom<ReviewAction>(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
20
|
+
return ctx.ui.custom<ReviewAction>(
|
|
21
|
+
(tui, theme, _keybindings, done) => {
|
|
22
|
+
let scrollOffset = 0;
|
|
23
|
+
let selectedRange = draft.ranges.length ? 0 : -1;
|
|
24
|
+
let expandSelected = true;
|
|
25
|
+
let jumpToSelected = true;
|
|
26
|
+
|
|
27
|
+
const selected = (): DraftRange | undefined =>
|
|
28
|
+
selectedRange >= 0 ? draft.ranges[selectedRange] : undefined;
|
|
29
|
+
|
|
30
|
+
const component = {
|
|
31
|
+
render(width: number): string[] {
|
|
32
|
+
const w = Math.max(40, width);
|
|
33
|
+
// Conservative height budget: stay under the overlay's maxHeight so
|
|
34
|
+
// the overlay never clips and the chat transcript is not crushed.
|
|
35
|
+
const totalBudget = Math.max(18, Math.floor(tui.terminal.rows * 0.8));
|
|
36
|
+
const border = (text: string) => theme.fg("border", text);
|
|
37
|
+
const dim = (text: string) => theme.fg("dim", text);
|
|
38
|
+
const accent = (text: string) => theme.fg("accent", text);
|
|
39
|
+
const success = (text: string) => theme.fg("success", text);
|
|
40
|
+
const warning = (text: string) => theme.fg("warning", text);
|
|
41
|
+
const inner = Math.max(20, w - 4);
|
|
42
|
+
const framed = (text: string) => `${border("│")} ${truncateToWidth(text, inner, "…", true)} ${border("│")}`;
|
|
43
|
+
const h = (l: string, m = "─", r = "─") => border(`${l}${m.repeat(Math.max(0, w - 2))}${r}`);
|
|
44
|
+
|
|
45
|
+
const range = selected();
|
|
46
|
+
|
|
47
|
+
const header: string[] = [
|
|
48
|
+
h("╭"),
|
|
49
|
+
framed(accent(theme.bold(`Midcompact Review · Draft v${draft.revision} · ${draft.ranges.length} range(s)`))),
|
|
50
|
+
framed(usageLine(telemetry, theme)),
|
|
51
|
+
h("├"),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Timeline: KEEP atoms always shown; ranges fold to a header line,
|
|
55
|
+
// except the selected range which expands with detail + its atoms.
|
|
56
|
+
const body: string[] = [];
|
|
57
|
+
const rangeStarts = new Map(draft.ranges.map((r) => [r.startIndex, r]));
|
|
58
|
+
const rangeLineStarts = new Map<string, number>();
|
|
59
|
+
|
|
60
|
+
for (const atom of atoms) {
|
|
61
|
+
const head = rangeStarts.get(atom.index);
|
|
62
|
+
if (head) {
|
|
63
|
+
rangeLineStarts.set(head.id, body.length);
|
|
64
|
+
const isSel = Boolean(range && range.id === head.id);
|
|
65
|
+
const savedChars = Math.max(0, head.originalContentChars - head.replacementContentChars);
|
|
66
|
+
const atomCount = head.endIndex - head.startIndex + 1;
|
|
67
|
+
const caret = isSel ? (expandSelected ? "▾" : "▸") : "▸";
|
|
68
|
+
const caretCol = isSel ? accent(caret) : dim(caret);
|
|
69
|
+
const idCol = isSel ? theme.bold(warning(head.id)) : warning(head.id);
|
|
70
|
+
const span = dim(`${head.startRef}→${head.endRef} · ${atomCount} atoms`);
|
|
71
|
+
const chars = dim(`${formatCount(head.originalContentChars)}→${formatCount(head.replacementContentChars)} chars`);
|
|
72
|
+
const saved = success(`−${formatCount(savedChars)}`);
|
|
73
|
+
const topicCol = head.topic ? `${accent(head.topic)} ` : "";
|
|
74
|
+
const sumCol = dim(firstLine(head.summary, 48));
|
|
75
|
+
const headText = `${caretCol} ${idCol} ${span} ${chars} ${saved} ${topicCol}${sumCol}`;
|
|
76
|
+
body.push(framed(isSel ? accent(headText) : headText));
|
|
77
|
+
|
|
78
|
+
if (isSel) {
|
|
79
|
+
body.push(framed(dim(` topic: ${head.topic ?? "—"}`)));
|
|
80
|
+
body.push(framed(dim(` chars: ${formatCount(head.originalContentChars)} → ${formatCount(head.replacementContentChars)} (−${formatCount(savedChars)}) · ${head.originalImageCount} images`)));
|
|
81
|
+
const wrapWidth = Math.max(10, inner - 6);
|
|
82
|
+
for (const line of wrapTextWithAnsi(`${accent("summary:")} ${head.summary}`, wrapWidth)) {
|
|
83
|
+
body.push(framed(` ${line}`));
|
|
84
|
+
}
|
|
85
|
+
body.push(framed(dim(` [e] edit summary · [t] edit topic · [d] remove · [x] ${expandSelected ? "collapse" : "expand"} atoms`)));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const owner = owningRange(atom.index, draft.ranges);
|
|
90
|
+
const showAtom = !owner || (Boolean(range) && owner.id === range!.id && expandSelected);
|
|
91
|
+
if (!showAtom) continue;
|
|
92
|
+
|
|
93
|
+
const mark = owner
|
|
94
|
+
? atom.index === owner.startIndex
|
|
95
|
+
? "┌"
|
|
96
|
+
: atom.index === owner.endIndex
|
|
97
|
+
? "└"
|
|
98
|
+
: "│"
|
|
99
|
+
: " ";
|
|
100
|
+
const policy = owner ? warning(owner.id) : success("KEEP");
|
|
101
|
+
const facts = dim(`${formatCount(atom.metrics.contentChars)} chars${atom.metrics.imageCount ? ` · ${atom.metrics.imageCount} img` : ""}`);
|
|
102
|
+
const oneLine = dim(firstLine(atom.preview, 80));
|
|
103
|
+
body.push(framed(`${mark} ${policy} ${atom.ref} [${atom.kind}] ${facts} ${oneLine}`));
|
|
104
|
+
}
|
|
105
|
+
if (!body.length) body.push(framed(dim("(No atoms in anchor snapshot.)")));
|
|
106
|
+
|
|
107
|
+
const footer: string[] = [
|
|
108
|
+
h("├"),
|
|
109
|
+
framed(dim(`ranges ${hint("n/p", theme)} · scroll ${hint("↑↓ PgUp PgDn", theme)} · ${hint("Esc", theme)} close`)),
|
|
110
|
+
framed(dim(`selected: ${hint("e", theme)} summary · ${hint("t", theme)} topic · ${hint("d", theme)} remove · ${hint("x", theme)} expand`)),
|
|
111
|
+
h("╰"),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const viewportHeight = Math.max(5, totalBudget - header.length - footer.length);
|
|
115
|
+
if (jumpToSelected && range) {
|
|
116
|
+
const target = rangeLineStarts.get(range.id) ?? 0;
|
|
117
|
+
scrollOffset = Math.max(0, target - 2);
|
|
118
|
+
jumpToSelected = false;
|
|
119
|
+
}
|
|
120
|
+
const maxOffset = Math.max(0, body.length - viewportHeight);
|
|
121
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset, maxOffset));
|
|
122
|
+
const visible = body.slice(scrollOffset, scrollOffset + viewportHeight);
|
|
123
|
+
while (visible.length < viewportHeight) visible.push(framed(""));
|
|
124
|
+
|
|
125
|
+
return [...header, ...visible, ...footer];
|
|
126
|
+
},
|
|
127
|
+
invalidate(): void {},
|
|
128
|
+
handleInput(data: string): void {
|
|
129
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter) || data === "q") {
|
|
130
|
+
done({ action: "close" });
|
|
131
|
+
return;
|
|
69
132
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
if (matchesKey(data, Key.home)) {
|
|
133
|
-
scrollOffset = 0;
|
|
134
|
-
tui.requestRender();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (matchesKey(data, Key.end)) {
|
|
138
|
-
scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
139
|
-
tui.requestRender();
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (data === "x") {
|
|
143
|
-
expandSelected = !expandSelected;
|
|
144
|
-
jumpToSelected = true;
|
|
145
|
-
tui.requestRender();
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
const range = selected();
|
|
149
|
-
if (!range) return;
|
|
150
|
-
if (data === "e") done({ action: "edit-summary", draftId: range.id });
|
|
151
|
-
else if (data === "t") done({ action: "edit-topic", draftId: range.id });
|
|
152
|
-
else if (data === "d") done({ action: "remove", draftId: range.id });
|
|
153
|
-
},
|
|
154
|
-
};
|
|
155
|
-
return component;
|
|
156
|
-
});
|
|
133
|
+
if ((data === "n" || matchesKey(data, Key.right)) && draft.ranges.length) {
|
|
134
|
+
selectedRange = (selectedRange + 1 + draft.ranges.length) % draft.ranges.length;
|
|
135
|
+
expandSelected = true;
|
|
136
|
+
jumpToSelected = true;
|
|
137
|
+
tui.requestRender();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if ((data === "p" || matchesKey(data, Key.left)) && draft.ranges.length) {
|
|
141
|
+
selectedRange = (selectedRange - 1 + draft.ranges.length) % draft.ranges.length;
|
|
142
|
+
expandSelected = true;
|
|
143
|
+
jumpToSelected = true;
|
|
144
|
+
tui.requestRender();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
148
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
149
|
+
tui.requestRender();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
153
|
+
scrollOffset += 1;
|
|
154
|
+
tui.requestRender();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
158
|
+
scrollOffset = Math.max(0, scrollOffset - 12);
|
|
159
|
+
tui.requestRender();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
163
|
+
scrollOffset += 12;
|
|
164
|
+
tui.requestRender();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (matchesKey(data, Key.home)) {
|
|
168
|
+
scrollOffset = 0;
|
|
169
|
+
tui.requestRender();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (matchesKey(data, Key.end)) {
|
|
173
|
+
scrollOffset = Number.MAX_SAFE_INTEGER;
|
|
174
|
+
tui.requestRender();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (data === "x") {
|
|
178
|
+
expandSelected = !expandSelected;
|
|
179
|
+
jumpToSelected = true;
|
|
180
|
+
tui.requestRender();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const range = selected();
|
|
184
|
+
if (!range) return;
|
|
185
|
+
if (data === "e") done({ action: "edit-summary", draftId: range.id });
|
|
186
|
+
else if (data === "t") done({ action: "edit-topic", draftId: range.id });
|
|
187
|
+
else if (data === "d") done({ action: "remove", draftId: range.id });
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
return component;
|
|
191
|
+
},
|
|
192
|
+
{ overlay: true, overlayOptions: { width: "92%", maxHeight: "86%", anchor: "center" } },
|
|
193
|
+
);
|
|
157
194
|
}
|
|
158
195
|
|
|
159
196
|
export function buildReviewText(atoms: Atom[], draft: DraftPlan, telemetry: DraftTelemetry): string {
|
|
@@ -165,7 +202,7 @@ export function buildReviewText(atoms: Atom[], draft: DraftPlan, telemetry: Draf
|
|
|
165
202
|
];
|
|
166
203
|
for (const atom of atoms) {
|
|
167
204
|
const owner = owningRange(atom.index, draft.ranges);
|
|
168
|
-
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)}`);
|
|
169
206
|
}
|
|
170
207
|
if (draft.ranges.length) {
|
|
171
208
|
lines.push("", "Proposed summaries:");
|
|
@@ -178,21 +215,28 @@ function owningRange(atomIndex: number, ranges: DraftRange[]): DraftRange | unde
|
|
|
178
215
|
return ranges.find((range) => atomIndex >= range.startIndex && atomIndex <= range.endIndex);
|
|
179
216
|
}
|
|
180
217
|
|
|
181
|
-
function firstLine(text: string): string {
|
|
182
|
-
|
|
218
|
+
function firstLine(text: string, limit = 120): string {
|
|
219
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
220
|
+
if (normalized.length <= limit) return normalized;
|
|
221
|
+
return `${normalized.slice(0, Math.max(0, limit - 1))}…`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function hint(text: string, theme: ExtensionCommandContext["ui"]["theme"]): string {
|
|
225
|
+
return theme.fg("accent", text);
|
|
183
226
|
}
|
|
184
227
|
|
|
185
228
|
function plainUsageLine(telemetry: DraftTelemetry): string {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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");
|
|
193
238
|
}
|
|
194
239
|
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
return `${label}: ${plainUsageLine(telemetry)}`;
|
|
240
|
+
function usageLine(telemetry: DraftTelemetry, theme: ExtensionCommandContext["ui"]["theme"]): string {
|
|
241
|
+
return `${theme.fg("accent", "Context")}: ${plainUsageLine(telemetry)}`;
|
|
198
242
|
}
|