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/index.ts
CHANGED
|
@@ -8,17 +8,23 @@ import {
|
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
10
10
|
|
|
11
|
-
import { buildAtoms, formatLocatedAtom,
|
|
11
|
+
import { buildAtoms, formatLocatedAtom, isProtectedAtom, locateAtomMatches } from "./atoms.js";
|
|
12
|
+
import { buildInventory, formatInventory, formatSpanInspection } from "./inventory.js";
|
|
12
13
|
import { messageText } from "./messages.js";
|
|
13
|
-
import { addDraftRange, emptyDraft, formatDraft, removeDraftRange, updateDraftRange } from "./plan.js";
|
|
14
|
+
import { addDraftRange, emptyDraft, formatDraft, formatPlanMutation, removeDraftRange, replaceDraftRanges, updateDraftRange } from "./plan.js";
|
|
15
|
+
import { expandSelection } from "./selection.js";
|
|
14
16
|
import { projectMessages } from "./projection.js";
|
|
15
17
|
import { registerStateRenderer, stateTreeLabel } from "./renderers.js";
|
|
16
18
|
import { showReviewUi } from "./review-ui.js";
|
|
19
|
+
import { showSelectionUi } from "./selection-ui.js";
|
|
20
|
+
import { showStartChoiceUi } from "./start-ui.js";
|
|
17
21
|
import { showReviewWebUi } from "./review-webui.js";
|
|
18
22
|
import {
|
|
19
23
|
DRAFT_ENTRY,
|
|
20
24
|
STATE_ENTRY,
|
|
21
25
|
TXN_ENTRY,
|
|
26
|
+
coerceDraftRange,
|
|
27
|
+
defaultStartMode,
|
|
22
28
|
emptyCompressionState,
|
|
23
29
|
restoreCompressionState,
|
|
24
30
|
restoreTransaction,
|
|
@@ -32,15 +38,27 @@ import type {
|
|
|
32
38
|
DraftPlan,
|
|
33
39
|
DraftTelemetry,
|
|
34
40
|
MessageLike,
|
|
41
|
+
SelectionSpan,
|
|
42
|
+
StartMode,
|
|
35
43
|
TransactionState,
|
|
36
44
|
} from "./types.js";
|
|
45
|
+
import {
|
|
46
|
+
acquireAgent,
|
|
47
|
+
emptyPlanningLock,
|
|
48
|
+
releaseAgent,
|
|
49
|
+
releaseUi,
|
|
50
|
+
tryAcquireUi,
|
|
51
|
+
type PlanningLockState,
|
|
52
|
+
} from "./planning-lock.js";
|
|
37
53
|
|
|
38
54
|
const TOOL_NAME = "midcompact";
|
|
39
|
-
const TOOL_DESCRIPTION =
|
|
55
|
+
const TOOL_DESCRIPTION =
|
|
56
|
+
"Inventory, locate, draft, or recall mid-context compression. Use the `midcompact` skill to route planning versus recall; during an active transaction, follow the runtime prompt for the state-specific first action.";
|
|
40
57
|
const STATUS_KEY = "midcompact";
|
|
58
|
+
const START_PROMPT_PREFIX = "A mid-compaction transaction is active on a frozen anchor snapshot.";
|
|
41
59
|
|
|
42
60
|
const Params = Type.Object({
|
|
43
|
-
action: StringEnum(["locate", "plan", "recall"] as const),
|
|
61
|
+
action: StringEnum(["inspect", "locate", "plan", "recall"] as const),
|
|
44
62
|
ref: Type.Optional(Type.String()),
|
|
45
63
|
pattern: Type.Optional(Type.String()),
|
|
46
64
|
source: Type.Optional(StringEnum(["any", "user", "assistant", "tool_call", "tool_result"] as const)),
|
|
@@ -54,6 +72,10 @@ const Params = Type.Object({
|
|
|
54
72
|
draft_id: Type.Optional(Type.String()),
|
|
55
73
|
topic: Type.Optional(Type.String()),
|
|
56
74
|
summary: Type.Optional(Type.String()),
|
|
75
|
+
// inspect inventory pagination or explicit candidate-span measurement
|
|
76
|
+
page_size: Type.Optional(Type.Number()),
|
|
77
|
+
cursor: Type.Optional(Type.String()),
|
|
78
|
+
spans: Type.Optional(Type.Array(Type.Object({ start: Type.String(), end: Type.String() }))),
|
|
57
79
|
});
|
|
58
80
|
|
|
59
81
|
type ParamsType = Static<typeof Params>;
|
|
@@ -64,6 +86,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
64
86
|
let activeState: CompressionState | undefined;
|
|
65
87
|
let transaction: TransactionState | undefined;
|
|
66
88
|
let draft: DraftPlan | undefined;
|
|
89
|
+
// Runtime mutex over DraftPlan edits. Not persisted: lost on reload by design.
|
|
90
|
+
const planningLock: PlanningLockState = emptyPlanningLock();
|
|
67
91
|
|
|
68
92
|
registerStateRenderer(pi);
|
|
69
93
|
|
|
@@ -71,9 +95,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
71
95
|
const branch = ctx.sessionManager.getBranch() as SessionEntry[];
|
|
72
96
|
activeState = restoreCompressionState(branch) ?? undefined;
|
|
73
97
|
const restored = restoreTransaction(branch);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
if (restored.transaction) {
|
|
99
|
+
const tx = withCompatDefaults(restored.transaction)!;
|
|
100
|
+
transaction = tx;
|
|
101
|
+
draft = restored.draft ? { ...restored.draft, ranges: restored.draft.ranges.map(coerceDraftRange) } : emptyDraft(tx.id);
|
|
102
|
+
} else {
|
|
103
|
+
transaction = undefined;
|
|
104
|
+
draft = undefined;
|
|
105
|
+
}
|
|
106
|
+
planningLock.owner = undefined;
|
|
107
|
+
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
77
108
|
}
|
|
78
109
|
|
|
79
110
|
pi.on("session_start", async (_event: unknown, ctx: ExtensionContext) => restoreRuntime(ctx));
|
|
@@ -83,15 +114,54 @@ export default function (pi: ExtensionAPI) {
|
|
|
83
114
|
activeState = undefined;
|
|
84
115
|
transaction = undefined;
|
|
85
116
|
draft = undefined;
|
|
117
|
+
planningLock.owner = undefined;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Agent turns hold the runtime edit lock for their entire lifetime, including
|
|
121
|
+
// inspect/locate before the first DraftPlan mutation.
|
|
122
|
+
pi.on("agent_start", async () => {
|
|
123
|
+
if (transaction) acquireAgent(planningLock);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Keep a user-created DraftPlan visible to the next Agent turn without
|
|
127
|
+
// starting a turn automatically after the user saves the UI.
|
|
128
|
+
pi.on("before_agent_start", async (event) => {
|
|
129
|
+
if (!transaction || event.prompt.startsWith(START_PROMPT_PREFIX)) return;
|
|
130
|
+
const currentDraft = draft ?? emptyDraft(transaction.id);
|
|
131
|
+
return {
|
|
132
|
+
message: {
|
|
133
|
+
customType: "midcompact-handoff",
|
|
134
|
+
content: [
|
|
135
|
+
"An active midcompact transaction exists with a persisted DraftPlan.",
|
|
136
|
+
`Draft revision ${currentDraft.revision}; ${currentDraft.ranges.length} existing range(s), which may have been created by the user.`,
|
|
137
|
+
"If the current user request asks to continue midcompact, read the `midcompact` skill first, then call midcompact(action=\"plan\", op=\"show\") before any other midcompact action. Treat the existing plan as the current shared draft. Infer from the user's request whether to preserve, refine, or extend it; ask only if materially ambiguous.",
|
|
138
|
+
].join("\n"),
|
|
139
|
+
display: false,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
86
142
|
});
|
|
87
143
|
|
|
144
|
+
// Agent turn end releases the Agent's runtime edit lock so the user can open a UI.
|
|
145
|
+
pi.on("agent_settled", async () => {
|
|
146
|
+
releaseAgent(planningLock);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Expose the UI-side lock operations so the future Selection/Review UI (and tests)
|
|
150
|
+
// can acquire/release the runtime mutex without a dedicated tool action. This is
|
|
151
|
+
// the UI's entry point for mutual exclusion, parallel to the Agent's plan mutation path.
|
|
152
|
+
(pi as unknown as { midcompactPlanningLock?: unknown }).midcompactPlanningLock = {
|
|
153
|
+
tryAcquireUi: () => tryAcquireUi(planningLock),
|
|
154
|
+
releaseUi: () => releaseUi(planningLock),
|
|
155
|
+
getOwner: () => planningLock.owner,
|
|
156
|
+
};
|
|
157
|
+
|
|
88
158
|
pi.on("context", async (event) => {
|
|
89
159
|
if (!activeState?.blocks.length) return;
|
|
90
160
|
return { messages: projectMessages(event.messages as MessageLike[], activeState) as typeof event.messages };
|
|
91
161
|
});
|
|
92
162
|
|
|
93
163
|
pi.registerCommand("midcompact", {
|
|
94
|
-
description: "Start, review, commit, inspect, or abort a branch-isolated mid-context compression transaction",
|
|
164
|
+
description: "Start, select, review, commit, inspect, or abort a branch-isolated mid-context compression transaction",
|
|
95
165
|
getArgumentCompletions(prefix: string): AutocompleteItem[] | null {
|
|
96
166
|
const query = prefix.trimStart().toLowerCase();
|
|
97
167
|
if (/\s/.test(query)) return null;
|
|
@@ -101,6 +171,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
101
171
|
{ value: "commit", label: "commit — Commit the current draft to the branch state" },
|
|
102
172
|
{ value: "review", label: "review — Open interactive TUI to inspect and edit the draft" },
|
|
103
173
|
{ value: "review-webui", label: "review-webui — Open a local web page to inspect and edit the draft (works without TUI)" },
|
|
174
|
+
{ value: "select", label: "select — Open the TUI Selection surface to edit range boundaries" },
|
|
175
|
+
{ value: "select-webui", label: "select-webui — Open Selection in a local browser" },
|
|
104
176
|
{ value: "status", label: "status — Show current transaction and draft status" },
|
|
105
177
|
];
|
|
106
178
|
const filtered = items.filter((item) => item.value.startsWith(query));
|
|
@@ -109,183 +181,272 @@ export default function (pi: ExtensionAPI) {
|
|
|
109
181
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
110
182
|
await ctx.waitForIdle();
|
|
111
183
|
const rawArgs = args.trim();
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
draft = undefined;
|
|
128
|
-
updateStatus(ctx, transaction, draft);
|
|
129
|
-
ctx.ui.notify("Midcompact transaction aborted; returned to anchor.", "info");
|
|
130
|
-
return;
|
|
184
|
+
const lower = rawArgs.toLowerCase();
|
|
185
|
+
|
|
186
|
+
if (lower === "abort") return abortTransaction(ctx);
|
|
187
|
+
if (lower === "commit") return commitTransaction(ctx);
|
|
188
|
+
if (lower === "review") return reviewTransaction(ctx, "tui");
|
|
189
|
+
if (lower === "review-webui") return reviewTransaction(ctx, "web");
|
|
190
|
+
if (lower === "select") return openSelectionUi(ctx, "auto");
|
|
191
|
+
if (lower === "select-webui") return openSelectionUi(ctx, "web");
|
|
192
|
+
if (lower === "status") return showStatus(ctx);
|
|
193
|
+
|
|
194
|
+
// start [instructions...]
|
|
195
|
+
const startMatch = rawArgs.match(/^start\b\s*(.*)$/i);
|
|
196
|
+
if (startMatch) {
|
|
197
|
+
const instructions = (startMatch[1] ?? "").trim();
|
|
198
|
+
return startTransaction(ctx, instructions || undefined);
|
|
131
199
|
}
|
|
132
200
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
if (!currentDraft?.ranges.length) {
|
|
142
|
-
ctx.ui.notify("Draft is empty; nothing to commit.", "warning");
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
146
|
-
const telemetry = draftTelemetry(currentTx, currentDraft);
|
|
147
|
-
const nextState = mergeDraftIntoState(snapshot.anchorState, currentDraft, currentTx, telemetry);
|
|
148
|
-
const result = await ctx.navigateTree(currentTx.anchorEntryId, { summarize: false });
|
|
149
|
-
if (result.cancelled) {
|
|
150
|
-
ctx.ui.notify("Midcompact commit cancelled by tree navigation.", "warning");
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
pi.appendEntry(STATE_ENTRY, nextState);
|
|
154
|
-
const stateEntryId = ctx.sessionManager.getLeafId();
|
|
155
|
-
if (stateEntryId) pi.setLabel(stateEntryId, stateTreeLabel(nextState));
|
|
156
|
-
activeState = nextState;
|
|
157
|
-
transaction = undefined;
|
|
158
|
-
draft = undefined;
|
|
159
|
-
updateStatus(ctx, transaction, draft);
|
|
160
|
-
ctx.ui.notify(commitNotice(nextState), "info");
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
201
|
+
ctx.ui.notify(
|
|
202
|
+
"Usage: /midcompact start [instructions] | /midcompact select[-webui] | /midcompact review[-webui] | /midcompact commit | /midcompact status | /midcompact abort",
|
|
203
|
+
"warning",
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
});
|
|
163
207
|
|
|
164
|
-
|
|
165
|
-
await reviewTransaction(ctx, "tui");
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
208
|
+
// ---- Transaction lifecycle ----
|
|
168
209
|
|
|
169
|
-
|
|
170
|
-
|
|
210
|
+
async function startTransaction(ctx: ExtensionCommandContext, customInstructions?: string): Promise<void> {
|
|
211
|
+
if (transaction) {
|
|
212
|
+
ctx.ui.notify("A midcompact transaction is already active on this branch.", "warning");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const sm = ctx.sessionManager;
|
|
216
|
+
const anchorEntryId = sm.getLeafId();
|
|
217
|
+
if (!anchorEntryId) {
|
|
218
|
+
ctx.ui.notify("Cannot start midcompact without a session leaf.", "error");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (ctx.hasUI && ctx.mode !== "tui") {
|
|
222
|
+
const ok = await ctx.ui.confirm(
|
|
223
|
+
"Start midcompact transaction?",
|
|
224
|
+
"The current context snapshot will be frozen as an anchor. You will need to review and explicitly commit or abort later.",
|
|
225
|
+
);
|
|
226
|
+
if (!ok) {
|
|
227
|
+
ctx.ui.notify("Midcompact start cancelled.", "info");
|
|
171
228
|
return;
|
|
172
229
|
}
|
|
230
|
+
}
|
|
231
|
+
// Mode choice: Agent-first or User-first. Both operate on the same DraftPlan;
|
|
232
|
+
// neither freezes boundaries. No mode flags on the command line.
|
|
233
|
+
const modeChoice = await chooseStartMode(ctx);
|
|
234
|
+
if (modeChoice === "cancelled") {
|
|
235
|
+
ctx.ui.notify("Midcompact start cancelled.", "info");
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const startMode: StartMode = modeChoice;
|
|
239
|
+
transaction = {
|
|
240
|
+
version: 1,
|
|
241
|
+
id: `tx-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
|
|
242
|
+
anchorEntryId,
|
|
243
|
+
startedAt: new Date().toISOString(),
|
|
244
|
+
startMode,
|
|
245
|
+
anchorUsage: snapshotContextUsage(ctx.getContextUsage()),
|
|
246
|
+
};
|
|
247
|
+
draft = emptyDraft(transaction.id);
|
|
248
|
+
pi.appendEntry(TXN_ENTRY, transaction);
|
|
249
|
+
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
250
|
+
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
251
|
+
ctx.ui.notify(`Midcompact started at anchor ${anchorEntryId} (${startMode}-first). ${compactUsage(transaction)}`, "info");
|
|
252
|
+
|
|
253
|
+
if (startMode === "agent") {
|
|
254
|
+
await sendAgentStartPrompt(transaction, customInstructions, "agent");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
// Use the same context setup for User manual, but make this turn an
|
|
258
|
+
// acknowledgement only before opening the user editing surface.
|
|
259
|
+
await sendAgentStartPrompt(transaction, customInstructions, "user");
|
|
260
|
+
await ctx.waitForIdle();
|
|
261
|
+
await openSelectionUi(ctx);
|
|
262
|
+
}
|
|
173
263
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (currentTx) {
|
|
179
|
-
ctx.ui.notify(formatDraft(currentDraft ?? emptyDraft(currentTx.id), draftTelemetry(currentTx, currentDraft)), "info");
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
if (!activeState?.blocks.length) {
|
|
183
|
-
ctx.ui.notify("No active transaction and no active midcompact blocks on this branch.", "info");
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
ctx.ui.notify(activeStateStatus(activeState), "info");
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
264
|
+
async function chooseStartMode(ctx: ExtensionCommandContext): Promise<StartMode | "cancelled"> {
|
|
265
|
+
if (!ctx.hasUI || ctx.mode !== "tui") return "agent";
|
|
266
|
+
return showStartChoiceUi(ctx);
|
|
267
|
+
}
|
|
189
268
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
269
|
+
async function openSelectionUi(ctx: ExtensionCommandContext, mode: "auto" | "tui" | "web" = "auto"): Promise<void> {
|
|
270
|
+
const currentTx = transaction;
|
|
271
|
+
if (!currentTx || !draft) {
|
|
272
|
+
ctx.ui.notify("No active midcompact transaction.", "warning");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (!tryAcquireUi(planningLock)) {
|
|
276
|
+
ctx.ui.notify("The Agent is currently processing the midcompact draft. Try Selection after the Agent turn ends.", "warning");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
193
279
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
280
|
+
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
281
|
+
const applySelection = (spans: SelectionSpan[], keepRefs: string[]): void => {
|
|
282
|
+
const normalized = expandSelection(snapshot.atoms, { spans, keepRefs });
|
|
283
|
+
draft = replaceDraftRanges(draft ?? emptyDraft(currentTx.id), snapshot.atoms, normalized.spans);
|
|
284
|
+
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
285
|
+
updateStatus(ctx, currentTx, draft, planningLock.owner);
|
|
286
|
+
};
|
|
198
287
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"Start midcompact transaction?",
|
|
212
|
-
"The current context snapshot will be frozen as an anchor. You will need to review and explicitly commit (/midcompact commit) or abort (/midcompact abort) later.",
|
|
213
|
-
);
|
|
214
|
-
if (!ok) {
|
|
215
|
-
ctx.ui.notify("Midcompact start cancelled.", "info");
|
|
216
|
-
return;
|
|
288
|
+
try {
|
|
289
|
+
if (mode === "tui" || (mode === "auto" && ctx.mode === "tui")) {
|
|
290
|
+
const action = await showSelectionUi(ctx, snapshot.atoms, draft, draftTelemetry(currentTx, draft));
|
|
291
|
+
if (action.action === "save") {
|
|
292
|
+
try {
|
|
293
|
+
applySelection(action.spans ?? [], action.keepRefs ?? []);
|
|
294
|
+
ctx.ui.notify("DraftPlan saved. Tell the Agent to continue processing it when ready.", "info");
|
|
295
|
+
} catch (error) {
|
|
296
|
+
ctx.ui.notify(`Selection could not be saved: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
ctx.ui.notify("Selection closed. The DraftPlan remains available; reopen select or tell the Agent to continue.", "info");
|
|
217
300
|
}
|
|
301
|
+
return;
|
|
218
302
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
303
|
+
await showReviewWebUi(ctx, snapshot.atoms, () => ({
|
|
304
|
+
draft: draft ?? emptyDraft(currentTx.id),
|
|
305
|
+
telemetry: draftTelemetry(currentTx, draft),
|
|
306
|
+
}), {
|
|
307
|
+
applySelection,
|
|
308
|
+
editSummary: () => { throw new Error("Summary editing belongs to Review."); },
|
|
309
|
+
editTopic: () => { throw new Error("Topic editing belongs to Review."); },
|
|
310
|
+
remove: (id) => {
|
|
311
|
+
draft = removeDraftRange(draft ?? emptyDraft(currentTx.id), id);
|
|
312
|
+
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
313
|
+
updateStatus(ctx, currentTx, draft, planningLock.owner);
|
|
314
|
+
},
|
|
315
|
+
}, "selection");
|
|
316
|
+
ctx.ui.notify("Selection closed. The DraftPlan is saved; tell the Agent to continue when ready.", "info");
|
|
317
|
+
} finally {
|
|
318
|
+
releaseUi(planningLock);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function sendAgentStartPrompt(tx: TransactionState, customInstructions: string | undefined, mode: StartMode): Promise<void> {
|
|
323
|
+
const awareness = formatTelemetry(draftTelemetry(tx, draft));
|
|
324
|
+
const promptLines = [
|
|
325
|
+
START_PROMPT_PREFIX,
|
|
326
|
+
awareness,
|
|
327
|
+
"The extension provides inspect for bounded inventory, locate for local details, plan show/add/update/remove for one shared DraftPlan, and recall for committed blocks.",
|
|
328
|
+
"The user owns the final compression decision. You may edit the DraftPlan, but you must not commit. Preserve facts that future work still needs; local character and image counts are not token estimates.",
|
|
329
|
+
];
|
|
330
|
+
if (customInstructions) promptLines.push(`User focus: ${customInstructions}`);
|
|
331
|
+
if (mode === "agent") {
|
|
240
332
|
promptLines.push(
|
|
241
|
-
"
|
|
242
|
-
"The Agent cannot commit. The user can inspect `/midcompact review`, then explicitly run `/midcompact commit` when satisfied.",
|
|
333
|
+
"FINAL STATE: AGENT DIRECT. The new DraftPlan is empty. Read the `midcompact` skill before doing any planning work, then call inspect first and use locate and plan to create ranges and summaries. Stop before commit.",
|
|
243
334
|
);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
description: TOOL_DESCRIPTION,
|
|
252
|
-
parameters: Params,
|
|
253
|
-
async execute(_id: string, params: ParamsType, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
254
|
-
try {
|
|
255
|
-
if (params.action === "recall") return toolResult(handleRecall(params, ctx));
|
|
256
|
-
if (!transaction) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact start` first.");
|
|
257
|
-
const snapshot = buildAnchorSnapshot(ctx.sessionManager, transaction);
|
|
258
|
-
if (params.action === "locate") return toolResult(handleLocate(params, snapshot.atoms));
|
|
259
|
-
if (params.action === "plan") {
|
|
260
|
-
draft ??= emptyDraft(transaction.id);
|
|
261
|
-
draft = handlePlan(params, draft, snapshot.atoms);
|
|
262
|
-
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
263
|
-
updateStatus(ctx, transaction, draft);
|
|
264
|
-
return toolResult(formatDraft(draft, draftTelemetry(transaction, draft)));
|
|
265
|
-
}
|
|
266
|
-
return toolResult("Unknown action.");
|
|
267
|
-
} catch (error) {
|
|
268
|
-
return toolResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
269
|
-
}
|
|
270
|
-
},
|
|
271
|
-
});
|
|
335
|
+
} else {
|
|
336
|
+
promptLines.push(
|
|
337
|
+
"FINAL STATE: USER MANUAL. The user is about to edit the initial DraftPlan. Acknowledge with OK only. Do not call any midcompact tool, inspect, locate, plan, or recall; do not change the draft or commit. Wait until the user finishes editing and sends a later request. On that later request, read the `midcompact` skill before doing any planning work, then call plan show first.",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
await pi.sendUserMessage(promptLines.join("\n"));
|
|
341
|
+
}
|
|
272
342
|
|
|
273
|
-
async function
|
|
343
|
+
async function abortTransaction(ctx: ExtensionCommandContext): Promise<void> {
|
|
344
|
+
if (planningLock.owner === "agent") {
|
|
345
|
+
ctx.ui.notify("The Agent is currently processing the midcompact draft. Abort after the Agent turn ends.", "warning");
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
274
348
|
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
275
349
|
const currentTx = restored.transaction ?? transaction;
|
|
276
|
-
|
|
350
|
+
if (!currentTx) {
|
|
351
|
+
ctx.ui.notify("No active midcompact transaction.", "info");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const result = await ctx.navigateTree(currentTx.anchorEntryId, { summarize: false });
|
|
355
|
+
if (result.cancelled) {
|
|
356
|
+
ctx.ui.notify("Midcompact abort cancelled by tree navigation.", "warning");
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
transaction = undefined;
|
|
360
|
+
draft = undefined;
|
|
361
|
+
releaseAgent(planningLock);
|
|
362
|
+
releaseUi(planningLock);
|
|
363
|
+
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
364
|
+
ctx.ui.notify("Midcompact transaction aborted; returned to anchor.", "info");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function commitTransaction(ctx: ExtensionCommandContext): Promise<void> {
|
|
368
|
+
if (planningLock.owner === "agent") {
|
|
369
|
+
ctx.ui.notify("The Agent is currently processing the midcompact draft. Commit after the Agent turn ends.", "warning");
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
373
|
+
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
374
|
+
const currentDraft = restored.draft?.ranges.map(coerceDraftRange) ? { ...restored.draft!, ranges: restored.draft.ranges.map(coerceDraftRange) } : draft;
|
|
277
375
|
if (!currentTx) {
|
|
278
376
|
ctx.ui.notify("No active midcompact transaction.", "warning");
|
|
279
377
|
return;
|
|
280
378
|
}
|
|
281
|
-
|
|
379
|
+
if (!currentDraft?.ranges.length) {
|
|
380
|
+
ctx.ui.notify("Draft is empty; nothing to commit.", "warning");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Commit validation: reject empty summary, invalid boundaries, overlaps, protected atoms.
|
|
282
384
|
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
385
|
+
try {
|
|
386
|
+
validateDraftForCommit(currentDraft, snapshot.atoms);
|
|
387
|
+
} catch (err) {
|
|
388
|
+
ctx.ui.notify(`Midcompact commit rejected: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const telemetry = draftTelemetry(currentTx, currentDraft);
|
|
392
|
+
const nextState = mergeDraftIntoState(snapshot.anchorState, currentDraft, currentTx, telemetry);
|
|
393
|
+
const result = await ctx.navigateTree(currentTx.anchorEntryId, { summarize: false });
|
|
394
|
+
if (result.cancelled) {
|
|
395
|
+
ctx.ui.notify("Midcompact commit cancelled by tree navigation.", "warning");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
pi.appendEntry(STATE_ENTRY, nextState);
|
|
399
|
+
const stateEntryId = ctx.sessionManager.getLeafId();
|
|
400
|
+
if (stateEntryId) pi.setLabel(stateEntryId, stateTreeLabel(nextState));
|
|
401
|
+
activeState = nextState;
|
|
402
|
+
transaction = undefined;
|
|
403
|
+
draft = undefined;
|
|
404
|
+
releaseAgent(planningLock);
|
|
405
|
+
releaseUi(planningLock);
|
|
406
|
+
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
407
|
+
ctx.ui.notify(commitNotice(nextState), "info");
|
|
408
|
+
}
|
|
283
409
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
410
|
+
async function showStatus(ctx: ExtensionCommandContext): Promise<void> {
|
|
411
|
+
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
412
|
+
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
413
|
+
const currentDraft = restored.draft ?? draft;
|
|
414
|
+
if (currentTx) {
|
|
415
|
+
const parts = [
|
|
416
|
+
formatDraft(currentDraft ?? emptyDraft(currentTx.id), draftTelemetry(currentTx, currentDraft)),
|
|
417
|
+
`Mode: ${currentTx.startMode ?? "agent"}-first · lock: ${planningLock.owner ?? "free"}`,
|
|
418
|
+
];
|
|
419
|
+
ctx.ui.notify(parts.join("\n"), "info");
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (!activeState?.blocks.length) {
|
|
423
|
+
ctx.ui.notify("No active transaction and no active midcompact blocks on this branch.", "info");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
ctx.ui.notify(activeStateStatus(activeState), "info");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function reviewTransaction(ctx: ExtensionCommandContext, mode: "tui" | "web" = "tui"): Promise<void> {
|
|
430
|
+
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
431
|
+
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
432
|
+
const currentDraft = restored.draft ?? draft;
|
|
433
|
+
if (!currentTx) {
|
|
434
|
+
ctx.ui.notify("No active midcompact transaction.", "warning");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!tryAcquireUi(planningLock)) {
|
|
438
|
+
ctx.ui.notify("The Agent is currently processing the midcompact draft. Try opening review after the Agent turn ends.", "warning");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const currentPlan = currentDraft ?? emptyDraft(currentTx.id);
|
|
443
|
+
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
444
|
+
|
|
445
|
+
const commitMutation = (next: DraftPlan): void => {
|
|
446
|
+
draft = next;
|
|
447
|
+
pi.appendEntry(DRAFT_ENTRY, next);
|
|
448
|
+
updateStatus(ctx, currentTx, next, planningLock.owner);
|
|
449
|
+
};
|
|
289
450
|
|
|
290
451
|
if (mode === "web") {
|
|
291
452
|
const getLatest = (): { draft: DraftPlan; telemetry: DraftTelemetry } => ({
|
|
@@ -335,6 +496,79 @@ export default function (pi: ExtensionAPI) {
|
|
|
335
496
|
commitMutation(removeDraftRange(plan, range.id));
|
|
336
497
|
}
|
|
337
498
|
}
|
|
499
|
+
} finally {
|
|
500
|
+
releaseUi(planningLock);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---- Planning lock (runtime mutex, not persisted) ----
|
|
505
|
+
|
|
506
|
+
/** Agent tool path: all active-transaction operations yield to an editing UI. */
|
|
507
|
+
function requireAgentAccess(ctx: ExtensionContext): boolean {
|
|
508
|
+
if (!acquireAgent(planningLock)) {
|
|
509
|
+
ctx.ui.notify("A Selection/Review UI is currently editing the midcompact draft. Close it before the Agent can continue.", "warning");
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// UI-side acquire/release are exposed via the module re-export above so the
|
|
516
|
+
// future Selection/Review UI (and tests) can drive them without going through
|
|
517
|
+
// a tool action. The Agent acquires the lock at agent_start and releases it
|
|
518
|
+
// at agent_settled.
|
|
519
|
+
|
|
520
|
+
// ---- Tool ----
|
|
521
|
+
|
|
522
|
+
pi.registerTool({
|
|
523
|
+
name: TOOL_NAME,
|
|
524
|
+
label: "Midcompact",
|
|
525
|
+
description: TOOL_DESCRIPTION,
|
|
526
|
+
parameters: Params,
|
|
527
|
+
async execute(_id: string, params: ParamsType, _signal: AbortSignal | undefined, _onUpdate: unknown, ctx: ExtensionContext) {
|
|
528
|
+
try {
|
|
529
|
+
if (params.action === "recall") return toolResult(handleRecall(params, ctx));
|
|
530
|
+
const restored = restoreTransaction(ctx.sessionManager.getBranch() as SessionEntry[]);
|
|
531
|
+
const currentTx = withCompatDefaults(restored.transaction ?? transaction);
|
|
532
|
+
if (!currentTx) return toolResult("No active midcompact transaction. Ask the user to run `/midcompact start` first.");
|
|
533
|
+
transaction = currentTx;
|
|
534
|
+
draft = restored.draft ? { ...restored.draft, ranges: restored.draft.ranges.map(coerceDraftRange) } : (draft ?? emptyDraft(currentTx.id));
|
|
535
|
+
if (!requireAgentAccess(ctx)) {
|
|
536
|
+
return toolResult("Agent operation blocked: a user editing UI holds the planning lock.");
|
|
537
|
+
}
|
|
538
|
+
const snapshot = buildAnchorSnapshot(ctx.sessionManager, currentTx);
|
|
539
|
+
|
|
540
|
+
if (params.action === "inspect") return toolResult(handleInspect(params, snapshot.atoms, currentTx));
|
|
541
|
+
if (params.action === "locate") return toolResult(handleLocate(params, snapshot.atoms));
|
|
542
|
+
if (params.action === "plan") {
|
|
543
|
+
const result = handlePlan(params, draft!, snapshot.atoms);
|
|
544
|
+
if (result.op === "show") {
|
|
545
|
+
return toolResult(formatDraft(draft!, draftTelemetry(transaction, draft), {
|
|
546
|
+
detail: params.detail,
|
|
547
|
+
draftId: params.draft_id,
|
|
548
|
+
atoms: snapshot.atoms,
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
draft = result.draft;
|
|
552
|
+
pi.appendEntry(DRAFT_ENTRY, draft);
|
|
553
|
+
updateStatus(ctx, transaction, draft, planningLock.owner);
|
|
554
|
+
return toolResult(formatPlanMutation(draft, result.op, result.changedId, snapshot.atoms));
|
|
555
|
+
}
|
|
556
|
+
return toolResult("Unknown action.");
|
|
557
|
+
} catch (error) {
|
|
558
|
+
return toolResult(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
function handleInspect(params: ParamsType, atoms: Atom[], tx: TransactionState): string {
|
|
564
|
+
if (params.spans) {
|
|
565
|
+
if (params.page_size !== undefined || params.cursor !== undefined) {
|
|
566
|
+
throw new Error("inspect spans cannot be combined with inventory pagination.");
|
|
567
|
+
}
|
|
568
|
+
return formatSpanInspection(atoms, params.spans);
|
|
569
|
+
}
|
|
570
|
+
const page = buildInventory(atoms, { pageSize: params.page_size, cursor: params.cursor }, { transaction: tx });
|
|
571
|
+
return formatInventory(page);
|
|
338
572
|
}
|
|
339
573
|
|
|
340
574
|
function handleRecall(params: ParamsType, ctx: ExtensionContext): string {
|
|
@@ -346,7 +580,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
346
580
|
const matches = branchState.blocks.filter((block) => !query || `${block.id}\n${block.topic ?? ""}\n${block.summary}`.toLocaleLowerCase().includes(query));
|
|
347
581
|
if (!matches.length) return "No compressed blocks matched.";
|
|
348
582
|
return matches.slice(0, Math.max(1, Math.min(params.limit ?? 8, 20))).map((block) =>
|
|
349
|
-
`${block.id}${block.topic ? ` | ${block.topic}` : ""} |
|
|
583
|
+
`${block.id}${block.topic ? ` | ${block.topic}` : ""} | ${block.originalContentChars ?? 0} original chars${block.originalImageCount ? ` · ${block.originalImageCount} images` : ""}\n${block.summary}`
|
|
350
584
|
).join("\n\n");
|
|
351
585
|
}
|
|
352
586
|
const block = branchState.blocks.find((candidate) => candidate.id === params.ref);
|
|
@@ -361,20 +595,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
361
595
|
const limit = params.detail === "full" ? 40_000 : 12_000;
|
|
362
596
|
return full.length > limit ? `${full.slice(0, limit)}\n\n[truncated; refine the recall request or inspect the source session for more]` : full;
|
|
363
597
|
}
|
|
364
|
-
}
|
|
365
598
|
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return { atoms: buildAtoms(visibleMessages, anchorBranch as any), anchorState };
|
|
599
|
+
function withCompatDefaults(tx: TransactionState | undefined): TransactionState | undefined {
|
|
600
|
+
if (!tx) return undefined;
|
|
601
|
+
return {
|
|
602
|
+
...tx,
|
|
603
|
+
startMode: defaultStartMode(tx.startMode),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
374
606
|
}
|
|
375
607
|
|
|
608
|
+
// ---- Pure handlers ----
|
|
609
|
+
|
|
376
610
|
function handleLocate(params: ParamsType, atoms: Atom[]): string {
|
|
377
|
-
const
|
|
611
|
+
const hasFilter = Boolean(params.pattern || params.tool_name || (params.source && params.source !== "any"));
|
|
612
|
+
if (params.ref && hasFilter) {
|
|
613
|
+
throw new Error("locate accepts either one direct ref or search filters, not both.");
|
|
614
|
+
}
|
|
615
|
+
if (params.detail === "full" && !params.ref) {
|
|
616
|
+
throw new Error("locate detail=full requires one direct atom ref.");
|
|
617
|
+
}
|
|
618
|
+
const result = locateAtomMatches(atoms, {
|
|
378
619
|
ref: params.ref,
|
|
379
620
|
pattern: params.pattern,
|
|
380
621
|
source: params.source,
|
|
@@ -383,24 +624,69 @@ function handleLocate(params: ParamsType, atoms: Atom[]): string {
|
|
|
383
624
|
limit: params.limit,
|
|
384
625
|
detail: params.detail,
|
|
385
626
|
});
|
|
386
|
-
if (!
|
|
387
|
-
|
|
627
|
+
if (!result.atoms.length) return "No matching atoms in the frozen anchor snapshot.";
|
|
628
|
+
const rendered = result.atoms
|
|
629
|
+
.map((atom) => formatLocatedAtom(atom, params.detail ?? "brief", params.pattern))
|
|
630
|
+
.join("\n\n---\n\n");
|
|
631
|
+
if (result.totalMatches <= result.atoms.length) return rendered;
|
|
632
|
+
return [
|
|
633
|
+
`Showing ${result.atoms.length} of ${result.totalMatches} matches (${params.direction ?? "oldest"} first). Refine pattern or add source, tool_name, or direction.`,
|
|
634
|
+
rendered,
|
|
635
|
+
].join("\n\n");
|
|
388
636
|
}
|
|
389
637
|
|
|
390
|
-
|
|
638
|
+
type PlanHandleResult =
|
|
639
|
+
| { op: "show"; draft: DraftPlan }
|
|
640
|
+
| { op: "add" | "update" | "remove"; draft: DraftPlan; changedId: string };
|
|
641
|
+
|
|
642
|
+
function handlePlan(params: ParamsType, current: DraftPlan, atoms: Atom[]): PlanHandleResult {
|
|
391
643
|
const op = params.op ?? "show";
|
|
392
|
-
if (op === "show") return current;
|
|
644
|
+
if (op === "show") return { op, draft: current };
|
|
393
645
|
if (op === "remove") {
|
|
394
646
|
if (!params.draft_id) throw new Error("plan remove requires draft_id.");
|
|
395
|
-
return removeDraftRange(current, params.draft_id);
|
|
647
|
+
return { op, draft: removeDraftRange(current, params.draft_id), changedId: params.draft_id };
|
|
396
648
|
}
|
|
397
649
|
if (op === "update") {
|
|
398
650
|
if (!params.draft_id) throw new Error("plan update requires draft_id.");
|
|
399
651
|
if (params.summary === undefined && params.topic === undefined) throw new Error("plan update requires summary or topic.");
|
|
400
|
-
return
|
|
652
|
+
return {
|
|
653
|
+
op,
|
|
654
|
+
draft: updateDraftRange(current, params.draft_id, { summary: params.summary, topic: params.topic }),
|
|
655
|
+
changedId: params.draft_id,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
if (!params.start || !params.end) throw new Error("plan add requires start and end.");
|
|
659
|
+
const next = addDraftRange(current, atoms, { start: params.start, end: params.end, summary: params.summary, topic: params.topic });
|
|
660
|
+
const previousIds = new Set(current.ranges.map((range) => range.id));
|
|
661
|
+
const changedId = next.ranges.find((range) => !previousIds.has(range.id))!.id;
|
|
662
|
+
return { op, draft: next, changedId };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function validateDraftForCommit(draft: DraftPlan, atoms: Atom[]): void {
|
|
666
|
+
for (const range of draft.ranges) {
|
|
667
|
+
if (range.summary.trim().length === 0) {
|
|
668
|
+
throw new Error(`Range ${range.id} has an empty (pending) summary; commit rejected.`);
|
|
669
|
+
}
|
|
670
|
+
if (range.startIndex > range.endIndex) throw new Error(`Range ${range.id} has reversed boundaries.`);
|
|
671
|
+
const slice = atoms.slice(range.startIndex, range.endIndex + 1);
|
|
672
|
+
const unsafe = slice.find((atom) => isProtectedAtom(atom));
|
|
673
|
+
if (unsafe) throw new Error(`Range ${range.id} crosses protected atom ${unsafe.ref}.`);
|
|
674
|
+
}
|
|
675
|
+
for (let i = 1; i < draft.ranges.length; i += 1) {
|
|
676
|
+
if (draft.ranges[i]!.startIndex <= draft.ranges[i - 1]!.endIndex) {
|
|
677
|
+
throw new Error(`Ranges ${draft.ranges[i - 1]!.id} and ${draft.ranges[i]!.id} overlap.`);
|
|
678
|
+
}
|
|
401
679
|
}
|
|
402
|
-
|
|
403
|
-
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function buildAnchorSnapshot(sm: ExtensionContext["sessionManager"], tx: TransactionState): RuntimeSnapshot {
|
|
683
|
+
const entries = sm.getEntries() as SessionEntry[];
|
|
684
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
685
|
+
const built = buildSessionContext(entries, tx.anchorEntryId, byId);
|
|
686
|
+
const anchorBranch = sm.getBranch(tx.anchorEntryId) as SessionEntry[];
|
|
687
|
+
const anchorState = restoreCompressionState(anchorBranch);
|
|
688
|
+
const visibleMessages = projectMessages(built.messages as MessageLike[], anchorState);
|
|
689
|
+
return { atoms: buildAtoms(visibleMessages, anchorBranch as any), anchorState };
|
|
404
690
|
}
|
|
405
691
|
|
|
406
692
|
function mergeDraftIntoState(
|
|
@@ -421,6 +707,10 @@ function mergeDraftIntoState(
|
|
|
421
707
|
entryIds: [...range.entryIds],
|
|
422
708
|
messageKeys: [...range.messageKeys],
|
|
423
709
|
createdAt: new Date().toISOString(),
|
|
710
|
+
originalContentChars: range.originalContentChars,
|
|
711
|
+
originalImageCount: range.originalImageCount,
|
|
712
|
+
originalImagePayloadBytes: range.originalImagePayloadBytes,
|
|
713
|
+
replacementContentChars: range.replacementContentChars,
|
|
424
714
|
originalApproxTokens: range.originalApproxTokens,
|
|
425
715
|
compressedApproxTokens: range.compressedApproxTokens,
|
|
426
716
|
};
|
|
@@ -434,10 +724,14 @@ function mergeDraftIntoState(
|
|
|
434
724
|
committedAt,
|
|
435
725
|
addedBlockIds,
|
|
436
726
|
addedRangeCount: draft.ranges.length,
|
|
727
|
+
selectedOriginalContentChars: telemetry.selectedOriginalContentChars,
|
|
728
|
+
selectedReplacementContentChars: telemetry.selectedReplacementContentChars,
|
|
729
|
+
selectedImageCount: telemetry.selectedImageCount,
|
|
730
|
+
selectedImagePayloadBytes: telemetry.selectedImagePayloadBytes,
|
|
731
|
+
anchorUsage: transaction.anchorUsage,
|
|
437
732
|
selectedOriginalApproxTokens: telemetry.selectedOriginalApproxTokens,
|
|
438
733
|
selectedCompressedApproxTokens: telemetry.selectedCompressedApproxTokens,
|
|
439
734
|
estimatedSavedTokens: telemetry.estimatedSavedTokens,
|
|
440
|
-
anchorUsage: transaction.anchorUsage,
|
|
441
735
|
projectedTokens: telemetry.projectedTokens,
|
|
442
736
|
projectedPercent: telemetry.projectedPercent,
|
|
443
737
|
};
|
|
@@ -457,14 +751,16 @@ function updateStatus(
|
|
|
457
751
|
ctx: ExtensionContext,
|
|
458
752
|
tx: TransactionState | undefined,
|
|
459
753
|
currentDraft: DraftPlan | undefined,
|
|
754
|
+
lockOwner: "agent" | "ui" | undefined = undefined,
|
|
460
755
|
): void {
|
|
461
756
|
const theme = ctx.ui.theme;
|
|
462
757
|
if (tx) {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
758
|
+
const pending = (currentDraft?.ranges ?? []).filter((range) => range.summary.trim().length === 0).length;
|
|
759
|
+
const chars = (currentDraft?.ranges ?? []).reduce((sum, range) => sum + range.originalContentChars, 0);
|
|
760
|
+
const lock = lockOwner === "ui" ? " · UI editing" : lockOwner === "agent" ? " · Agent editing" : "";
|
|
465
761
|
ctx.ui.setStatus(
|
|
466
762
|
STATUS_KEY,
|
|
467
|
-
`${theme.fg("accent", "MC planning")} · ${currentDraft?.ranges.length ?? 0} ranges${
|
|
763
|
+
`${theme.fg("accent", "MC planning")} · ${currentDraft?.ranges.length ?? 0} ranges${pending ? ` · ${pending} pending` : ""} · ${chars} chars${lock}`,
|
|
468
764
|
);
|
|
469
765
|
return;
|
|
470
766
|
}
|
|
@@ -473,25 +769,20 @@ function updateStatus(
|
|
|
473
769
|
|
|
474
770
|
function compactUsage(tx: TransactionState): string {
|
|
475
771
|
const usage = tx.anchorUsage;
|
|
476
|
-
if (!usage) return "Anchor context usage unavailable.";
|
|
477
|
-
return `Anchor context ${formatTokenCount(usage.tokens)}/${formatTokenCount(usage.contextWindow)} (${formatPercent(usage.percent)}).`;
|
|
772
|
+
if (!usage) return "Anchor context usage unavailable [Pi reported].";
|
|
773
|
+
return `Anchor context ${formatTokenCount(usage.tokens)}/${formatTokenCount(usage.contextWindow)} (${formatPercent(usage.percent)}) [Pi reported].`;
|
|
478
774
|
}
|
|
479
775
|
|
|
480
776
|
function activeStateStatus(state: CompressionState): string {
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
);
|
|
485
|
-
return `Midcompact active on this branch: ${state.blocks.length} block(s), ~${formatTokenCount(saved)} estimated tokens saved. Original history remains recallable.`;
|
|
777
|
+
const originalChars = state.blocks.reduce((sum, block) => sum + (block.originalContentChars ?? 0), 0);
|
|
778
|
+
const replacementChars = state.blocks.reduce((sum, block) => sum + (block.replacementContentChars ?? 0), 0);
|
|
779
|
+
return `Midcompact active on this branch: ${state.blocks.length} block(s), ${originalChars} → ${replacementChars} content chars. Original history remains recallable.`;
|
|
486
780
|
}
|
|
487
781
|
|
|
488
782
|
function commitNotice(state: CompressionState): string {
|
|
489
783
|
const commit = state.lastCommit;
|
|
490
784
|
if (!commit) return `Midcompact committed: ${state.blocks.length} active compressed block(s).`;
|
|
491
|
-
|
|
492
|
-
? ""
|
|
493
|
-
: ` Projected anchor context ${formatPercent(commit.anchorUsage?.percent ?? null)} → ${formatPercent(commit.projectedPercent, true)}.`;
|
|
494
|
-
return `Midcompact committed: ${commit.addedRangeCount} new range(s), ~${formatTokenCount(commit.estimatedSavedTokens)} estimated tokens saved.${projection}`;
|
|
785
|
+
return `Midcompact committed: ${commit.addedRangeCount} new range(s), ${commit.selectedOriginalContentChars} → ${commit.selectedReplacementContentChars} content chars${commit.selectedImageCount ? ` · ${commit.selectedImageCount} images` : ""}. Original history retained; use recall for exact details.`;
|
|
495
786
|
}
|
|
496
787
|
|
|
497
788
|
function toolResult(text: string) {
|