omp-vcc 0.1.12 → 0.1.14
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/extensions/main.ts +174 -81
- package/extensions/vcc-core/core/compaction-chain.ts +301 -0
- package/extensions/vcc-core/core/drill-down.ts +11 -4
- package/extensions/vcc-core/core/format-recall.ts +18 -2
- package/extensions/vcc-core/core/global-indices.ts +46 -0
- package/extensions/vcc-core/core/load-messages.ts +116 -21
- package/extensions/vcc-core/core/normalize.ts +13 -13
- package/extensions/vcc-core/core/recall-budget.ts +107 -0
- package/extensions/vcc-core/core/recall-scope.ts +16 -9
- package/extensions/vcc-core/core/search-entries.ts +201 -36
- package/extensions/vcc-core/core/session-lines.ts +81 -0
- package/extensions/vcc-core/core/settings.ts +239 -99
- package/extensions/vcc-core/core/summarize.ts +8 -2
- package/extensions/vcc-core/core/token-estimate.ts +55 -0
- package/extensions/vcc-core/core/tool-output-budget.ts +217 -0
- package/extensions/vcc-core/details.ts +35 -0
- package/extensions/vcc-core/hook.ts +782 -140
- package/package.json +77 -1
- package/scripts/smoke.ts +8 -0
- package/types.d.ts +10 -2
package/extensions/main.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// pi-vcc port: sting8k/pi-vcc @0.7.0 — algorithmic, zero-LLM, brief transcript + 5 sections, token-budgeted
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
|
|
8
|
-
import { scaffoldSettings } from "./vcc-core/core/settings";
|
|
8
|
+
import { scaffoldSettings, loadSettings, loadSettingsWithPluginOverlay } from "./vcc-core/core/settings";
|
|
9
9
|
import { migrateStalePluginEntries } from "./vcc-core/core/migrate-stale";
|
|
10
10
|
import { loadAllMessages } from "./vcc-core/core/load-messages";
|
|
11
11
|
import {
|
|
@@ -21,12 +21,14 @@ import {
|
|
|
21
21
|
registerVccStatsCommand as registerVccStatsCommandHook,
|
|
22
22
|
registerVccConfigCommand as registerVccConfigCommandHook,
|
|
23
23
|
invalidExpandIndices,
|
|
24
|
+
getCompactForm,
|
|
24
25
|
} from "./vcc-core/hook";
|
|
25
26
|
import { searchEntriesDetailed, getTouchedFiles } from "./vcc-core/core/search-entries";
|
|
26
27
|
import { formatRecallOutput, formatTouchedOutput } from "./vcc-core/core/format-recall";
|
|
27
28
|
import { getActiveLineageEntryIds } from "./vcc-core/core/lineage";
|
|
28
|
-
import { normalizeRecallScope, normalizeRecallMode, parseRecallScope } from "./vcc-core/core/recall-scope";
|
|
29
|
+
import { normalizeRecallScope, normalizeRecallMode, parseRecallScope, parseRecallMode } from "./vcc-core/core/recall-scope";
|
|
29
30
|
import { parseDrillDown, expandEntryFile, parseEntryRef, expandEntry } from "./vcc-core/core/drill-down";
|
|
31
|
+
import { capRecallBlocks, type RecallBudgetBlock } from "./vcc-core/core/recall-budget";
|
|
30
32
|
import { buildPiVccCustomInstructions, parseKeepAndPrompt } from "./vcc-core/core/compact-args";
|
|
31
33
|
|
|
32
34
|
// Build omp sentinel instructions; keep pi sentinel for backward compat in hook
|
|
@@ -38,14 +40,25 @@ const buildOmpCustomInstructions = (keepUserTurns: number | null): string => {
|
|
|
38
40
|
// Helper to parse recall command text: supports `query ... scope:all page:N`
|
|
39
41
|
const parseRecallCommandArgs = (
|
|
40
42
|
raw: string,
|
|
41
|
-
): { query: string; scope: "lineage" | "all"; page: number } => {
|
|
42
|
-
const
|
|
43
|
+
): { query: string; scope: "lineage" | "all"; mode: "hybrid" | "touched" | "file"; page: number } => {
|
|
44
|
+
const scoped = parseRecallScope(raw);
|
|
45
|
+
const parsed = parseRecallMode(scoped.text);
|
|
43
46
|
const pageMatch = parsed.text.match(/\bpage:(\d+)\b/i);
|
|
44
47
|
const page = pageMatch ? Math.max(1, Number.parseInt(pageMatch[1] ?? "1", 10)) : 1;
|
|
45
48
|
const query = parsed.text.replace(/\bpage:\d+\b/i, "").trim();
|
|
46
|
-
return { query, scope:
|
|
49
|
+
return { query, scope: scoped.scope, mode: normalizeRecallMode(parsed.mode), page };
|
|
47
50
|
};
|
|
48
51
|
|
|
52
|
+
const capModelRecall = (settings: { recallResponseMaxChars: number }, blocks: Array<RecallBudgetBlock | string>, blockKind = "entry"): string =>
|
|
53
|
+
capRecallBlocks(blocks, settings.recallResponseMaxChars, { blockKind });
|
|
54
|
+
const loadRecallMessages = (ctx: unknown, sessionFile: string, full: boolean, lineageEntryIds?: Set<string>) =>
|
|
55
|
+
loadAllMessages(sessionFile, full, lineageEntryIds, (event) => {
|
|
56
|
+
if (!loadSettings(ctx).debug) return;
|
|
57
|
+
try {
|
|
58
|
+
(ctx as any)?.ui?.notify?.(`omp-vcc: ${event.kind} (${event.parseErrors} malformed lines)`, "warning");
|
|
59
|
+
} catch {}
|
|
60
|
+
});
|
|
61
|
+
|
|
49
62
|
const DEFAULT_RECENT = 25;
|
|
50
63
|
const PAGE_SIZE = 5;
|
|
51
64
|
export default function (pi: ExtensionAPI): void {
|
|
@@ -62,14 +75,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
62
75
|
name: "vcc_recall",
|
|
63
76
|
label: "VCC Recall",
|
|
64
77
|
description:
|
|
65
|
-
"Recall earlier parts of the current session — decisions made, files touched, commands run, including anything dropped by compaction. Reach for this before telling the user you no longer have the context. Plain keywords work best; a regex pattern is also accepted. Results are paged (page); pass expand with entry indices to read full untruncated content. Use mode:'touched' to list files worked on in this session with their entry indices, and #N:path to drill into a file's content from an entry (#N:path:full for all lines). Note: apply_patch paths (inside the diff payload) and bash redirects do not appear in the touched index. Only the current session is searchable — earlier sessions are not.",
|
|
78
|
+
"Recall earlier parts of the current session — decisions made, files touched, commands run, including anything dropped by compaction. Reach for this before telling the user you no longer have the context. Plain keywords work best; a regex pattern is also accepted. Results are paged (page); pass expand with entry indices to read full untruncated content. Use mode:'touched' to list files worked on in this session with their entry indices, mode:'file' to search only file tool arguments, and #N:path to drill into a file's content from an entry (#N:path:full for all lines). Note: apply_patch paths (inside the diff payload) and bash redirects do not appear in the touched index. Only the current session is searchable — earlier sessions are not.",
|
|
66
79
|
approval: "read",
|
|
67
80
|
parameters: pi.zod.object({
|
|
68
81
|
query: pi.zod.string().optional().describe("What to recall, in plain keywords (e.g. 'redis cache decision'). Multi-word queries are ranked by relevance. A regex pattern also works."),
|
|
69
82
|
expand: pi.zod.array(pi.zod.number()).optional().describe("Entry indices to return full untruncated content for"),
|
|
70
83
|
page: pi.zod.number().optional().describe("Page number (1-based) for paginated search results. Default: 1."),
|
|
71
84
|
scope: pi.zod.enum(["lineage", "all", "active"]).optional().describe("Default 'lineage' covers the active conversation path. Use 'all' to also reach messages from other branches, such as turns that were edited or retried."),
|
|
72
|
-
mode: pi.zod.enum(["hybrid", "touched"]).optional().describe("What to show. hybrid (default) = normal search; touched = aggregated files-by-path
|
|
85
|
+
mode: pi.zod.enum(["hybrid", "touched", "file"]).optional().describe("What to show. hybrid (default) = normal search; touched = aggregated files-by-path; file = only file tool arguments."),
|
|
73
86
|
}),
|
|
74
87
|
async execute(_toolCallId: string, params: unknown, _signal: unknown, _onUpdate: unknown, ctx: unknown) {
|
|
75
88
|
const p = params as {
|
|
@@ -82,6 +95,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
82
95
|
const c = ctx as {
|
|
83
96
|
sessionManager?: { getSessionFile?: () => string | undefined; getBranch?: () => unknown[]; getEntries?: () => unknown[] };
|
|
84
97
|
};
|
|
98
|
+
const settings = await loadSettingsWithPluginOverlay(ctx);
|
|
85
99
|
const sessionFile = c.sessionManager?.getSessionFile?.();
|
|
86
100
|
if (!sessionFile) {
|
|
87
101
|
return {
|
|
@@ -94,12 +108,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
94
108
|
const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
|
|
95
109
|
|
|
96
110
|
const q = p.query?.trim();
|
|
111
|
+
const mode = normalizeRecallMode(p.mode);
|
|
112
|
+
const bounded = (text: string, id: string | number, kind = "entry"): string => capModelRecall(settings, [{ id, text }], kind);
|
|
97
113
|
|
|
98
114
|
const entryRef = q ? parseEntryRef(q) : null;
|
|
99
115
|
if (entryRef) {
|
|
100
116
|
const ref = entryRef;
|
|
101
117
|
if (lineageEntryIds) {
|
|
102
|
-
const { rendered } =
|
|
118
|
+
const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
103
119
|
const exists = rendered.some((m) => m.index === ref.index);
|
|
104
120
|
if (!exists) {
|
|
105
121
|
return {
|
|
@@ -109,14 +125,31 @@ export default function (pi: ExtensionAPI): void {
|
|
|
109
125
|
}
|
|
110
126
|
}
|
|
111
127
|
const text = expandEntry(sessionFile, ref.index, ref.full, ref.offset, ref.limit);
|
|
112
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
128
|
+
return { content: [{ type: "text", text: bounded(text, `#${ref.index}`) }], details: undefined };
|
|
129
|
+
}
|
|
130
|
+
const textMatch = q?.match(/^#(\d+):text(?::(full|\d+(?::\d+)?))?$/);
|
|
131
|
+
if (textMatch) {
|
|
132
|
+
const index = Number.parseInt(textMatch[1] ?? "0", 10);
|
|
133
|
+
const suffix = textMatch[2];
|
|
134
|
+
const full = suffix === "full";
|
|
135
|
+
const parts = suffix && !full ? suffix.split(":") : [];
|
|
136
|
+
const offset = parts[0] !== undefined ? Number.parseInt(parts[0], 10) : undefined;
|
|
137
|
+
const limit = parts[1] !== undefined ? Number.parseInt(parts[1], 10) : undefined;
|
|
138
|
+
if (lineageEntryIds) {
|
|
139
|
+
const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
140
|
+
if (!rendered.some((m) => m.index === index)) {
|
|
141
|
+
return { content: [{ type: "text", text: `Cannot expand indices outside active lineage: ${index}. Use scope:'all' to reach other branches.` }], details: undefined };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const text = expandEntry(sessionFile, index, full, offset, limit);
|
|
145
|
+
return { content: [{ type: "text", text: bounded(text, `#${index}:text`) }], details: undefined };
|
|
113
146
|
}
|
|
114
147
|
|
|
115
148
|
const drill = q ? parseDrillDown(q) : null;
|
|
116
149
|
if (drill) {
|
|
117
150
|
const parsed = drill;
|
|
118
151
|
if (lineageEntryIds) {
|
|
119
|
-
const { rendered } =
|
|
152
|
+
const { rendered } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
120
153
|
const exists = rendered.some((m) => m.index === parsed.index);
|
|
121
154
|
if (!exists) {
|
|
122
155
|
return {
|
|
@@ -126,20 +159,26 @@ export default function (pi: ExtensionAPI): void {
|
|
|
126
159
|
}
|
|
127
160
|
}
|
|
128
161
|
const text = expandEntryFile(sessionFile, parsed.index, parsed.pathPattern, parsed.full, parsed.offset, parsed.limit);
|
|
129
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
162
|
+
return { content: [{ type: "text", text: bounded(text, `#${parsed.index}:path`) }], details: undefined };
|
|
130
163
|
}
|
|
131
164
|
|
|
132
|
-
if (
|
|
133
|
-
const { rendered, rawMessages } =
|
|
165
|
+
if (mode === "touched") {
|
|
166
|
+
const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
134
167
|
const touched = getTouchedFiles(rawMessages as unknown[], rendered);
|
|
135
168
|
const text = formatTouchedOutput(touched, p.page);
|
|
136
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
169
|
+
return { content: [{ type: "text", text: bounded(text, `page:${p.page ?? 1}`, "page") }], details: undefined };
|
|
170
|
+
}
|
|
171
|
+
if (mode === "file" && !q) {
|
|
172
|
+
const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
173
|
+
const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
|
|
174
|
+
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
|
|
175
|
+
return { content: [{ type: "text", text: capModelRecall(settings, [{ id: "file", text: output }], "file") }], details: undefined };
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
const expandSet = new Set(p.expand ?? []);
|
|
140
179
|
const hasExpand = expandSet.size > 0;
|
|
141
180
|
if (hasExpand) {
|
|
142
|
-
const { rendered: fullMsgs } =
|
|
181
|
+
const { rendered: fullMsgs } = loadRecallMessages(ctx, sessionFile, true, lineageEntryIds);
|
|
143
182
|
const requested = [...expandSet];
|
|
144
183
|
const byIndex = new Map(fullMsgs.map((m) => [m.index, m]));
|
|
145
184
|
const invalid = invalidExpandIndices(requested, new Set(byIndex.keys()));
|
|
@@ -150,13 +189,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
150
189
|
};
|
|
151
190
|
}
|
|
152
191
|
const expanded = requested.map((i) => byIndex.get(i)).filter((m): m is NonNullable<typeof m> => Boolean(m));
|
|
153
|
-
const
|
|
192
|
+
const blocks = expanded.map((entry) => ({ id: `#${entry.index}`, text: formatRecallOutput([entry]) }));
|
|
193
|
+
const output = (scope === "all" ? "Scope: all\n\n" : "") + capModelRecall(settings, blocks, "entry");
|
|
154
194
|
return { content: [{ type: "text", text: output }], details: undefined };
|
|
155
195
|
}
|
|
156
196
|
|
|
157
|
-
const { rendered: msgs, rawMessages } =
|
|
197
|
+
const { rendered: msgs, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
158
198
|
if (q) {
|
|
159
|
-
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(msgs, rawMessages as unknown[], q);
|
|
199
|
+
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(msgs, rawMessages as unknown[], q, { mode });
|
|
160
200
|
const page = Math.max(1, p.page ?? 1);
|
|
161
201
|
const totalPages = Math.ceil(hits.length / PAGE_SIZE);
|
|
162
202
|
const scopeSuffix = scope === "all" ? " (scope: all)" : "";
|
|
@@ -164,59 +204,109 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
204
|
if (hits.length > 0 && page > totalPages) {
|
|
165
205
|
const guidance = truncated ? `Use a page between 1 and ${totalPages}.` : `Use a page between 1 and ${totalPages}, or refine your query.`;
|
|
166
206
|
const text = `Page ${page} is outside the available range 1-${totalPages} (${hits.length} matches${scopeSuffix}${truncationNote}). ${guidance}`;
|
|
167
|
-
return { content: [{ type: "text", text }], details: undefined };
|
|
207
|
+
return { content: [{ type: "text", text: bounded(text, `page:${page}`, "page") }], details: undefined };
|
|
168
208
|
}
|
|
169
209
|
const start = (page - 1) * PAGE_SIZE;
|
|
170
210
|
const pageResults = hits.slice(start, start + PAGE_SIZE);
|
|
171
211
|
const header = totalPages > 1 ? `Page ${page}/${totalPages} (${hits.length} total matches${scopeSuffix}${truncationNote})` : `${hits.length} matches${scopeSuffix}${truncationNote}`;
|
|
172
212
|
const footer = page < totalPages ? `\n--- Use page:${page + 1}${scope === "all" ? " with scope:'all'" : ""} for more results ---` : "";
|
|
173
213
|
const output = formatRecallOutput(pageResults, q, header, { truncated, totalBeforeCap }) + footer;
|
|
174
|
-
return { content: [{ type: "text", text: output }], details: undefined };
|
|
214
|
+
return { content: [{ type: "text", text: bounded(output, `page:${page}`, "page") }], details: undefined };
|
|
175
215
|
}
|
|
176
|
-
const
|
|
216
|
+
const recent = msgs.slice(-DEFAULT_RECENT);
|
|
217
|
+
const blocks = recent.map((entry) => ({ id: `#${entry.index}`, text: formatRecallOutput([entry]) }));
|
|
218
|
+
const output = (scope === "all" ? "Scope: all\n\n" : "") + capModelRecall(settings, blocks, "entry");
|
|
177
219
|
return { content: [{ type: "text", text: output }], details: undefined };
|
|
178
220
|
},
|
|
179
221
|
} as unknown as Parameters<ExtensionAPI["registerTool"]>[0]);
|
|
180
222
|
// ── vcc_stats tool — stats surface for savings (paper § verification) ──
|
|
181
223
|
registerVccStatsToolHook(pi);
|
|
182
224
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
225
|
+
// Shared /omp-vcc + /pi-vcc runner. The two hosts expose incompatible
|
|
226
|
+
// ctx.compact shapes, so the call form branches per live ctx:
|
|
227
|
+
// - omp: compact(string | CompactOptions) => Promise<void>; instructions
|
|
228
|
+
// ride the string (the host splits string|object and drops instructions
|
|
229
|
+
// from the object form), completion = awaited resolution, errors throw
|
|
230
|
+
// ("Compaction cancelled" / "Already compacted" / "Nothing to compact…").
|
|
231
|
+
// - pi: compact(CompactOptions) => void; instructions ONLY via
|
|
232
|
+
// options.customInstructions (a bare string reads as undefined), outcome
|
|
233
|
+
// arrives via onComplete/onError. `settled` keeps one outcome.
|
|
234
|
+
// Form detection is layered (explicit test override → getSystemPrompt
|
|
235
|
+
// shape → module scope → legacy omp default) so bundled runtimes without
|
|
236
|
+
// module scope still decide correctly off the live ctx.
|
|
237
|
+
const runCompactCommand = async (
|
|
238
|
+
args: string,
|
|
239
|
+
c: {
|
|
240
|
+
compact: (arg?: unknown) => Promise<void> | void;
|
|
241
|
+
ui: { notify: (msg: string, level?: string) => void };
|
|
242
|
+
},
|
|
243
|
+
buildInstructions: (keep: number | null) => string,
|
|
244
|
+
fallbackToast: string,
|
|
245
|
+
preNotify: boolean,
|
|
246
|
+
compactForm: "object" | "string",
|
|
247
|
+
): Promise<void> => {
|
|
248
|
+
const parsed = parseKeepAndPrompt(args);
|
|
249
|
+
const keep = parsed.keepUserTurns;
|
|
250
|
+
const followUpPrompt = parsed.followUpPrompt;
|
|
251
|
+
const customInstructions = buildInstructions(keep);
|
|
252
|
+
if (preNotify) {
|
|
195
253
|
try {
|
|
196
254
|
c.ui.notify(`omp-vcc: compacting with keep:${keep ?? 1}${followUpPrompt ? ` + focus` : ""}...`, "info");
|
|
197
255
|
} catch {}
|
|
256
|
+
}
|
|
257
|
+
let settled = false;
|
|
258
|
+
const finishOk = (): void => {
|
|
259
|
+
if (settled) return;
|
|
260
|
+
settled = true;
|
|
261
|
+
const stats = getLastCompactionStats(pi);
|
|
262
|
+
if (stats) {
|
|
263
|
+
scheduleCompactionStatsNotify(pi, c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
|
|
264
|
+
} else {
|
|
265
|
+
try { c.ui.notify(fallbackToast, "info"); } catch {}
|
|
266
|
+
}
|
|
267
|
+
if (followUpPrompt) {
|
|
268
|
+
try {
|
|
269
|
+
const piAny = pi as unknown as { sendUserMessage?: (content: string) => unknown };
|
|
270
|
+
const sent = piAny.sendUserMessage?.(followUpPrompt) as Promise<void> | undefined;
|
|
271
|
+
if (sent && typeof sent.catch === "function") sent.catch(() => {});
|
|
272
|
+
} catch {}
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const finishErr = (err: unknown): void => {
|
|
276
|
+
if (settled) return;
|
|
277
|
+
settled = true;
|
|
278
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
279
|
+
if (msg === "Compaction cancelled" || msg === "Already compacted" || msg.startsWith("Nothing to compact")) {
|
|
280
|
+
try { c.ui.notify("Nothing to compact", "warning"); } catch {}
|
|
281
|
+
} else {
|
|
282
|
+
try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if (compactForm === "object") {
|
|
198
286
|
try {
|
|
199
|
-
|
|
200
|
-
const stats = getLastCompactionStats(pi);
|
|
201
|
-
if (stats) {
|
|
202
|
-
scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
|
|
203
|
-
} else {
|
|
204
|
-
try { c.ui.notify("Compacted with omp-vcc", "info"); } catch {}
|
|
205
|
-
}
|
|
206
|
-
if (followUpPrompt) {
|
|
207
|
-
try {
|
|
208
|
-
const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
|
|
209
|
-
if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
|
|
210
|
-
} catch {}
|
|
211
|
-
}
|
|
287
|
+
c.compact({ customInstructions, onComplete: finishOk, onError: finishErr });
|
|
212
288
|
} catch (err: unknown) {
|
|
213
|
-
|
|
214
|
-
if (msg === "Compaction cancelled" || msg === "Already compacted") {
|
|
215
|
-
try { c.ui.notify("Nothing to compact", "warning"); } catch {}
|
|
216
|
-
} else {
|
|
217
|
-
try { c.ui.notify(`Compaction failed: ${msg}`, "error"); } catch {}
|
|
218
|
-
}
|
|
289
|
+
finishErr(err);
|
|
219
290
|
}
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await c.compact(customInstructions);
|
|
295
|
+
finishOk();
|
|
296
|
+
} catch (err: unknown) {
|
|
297
|
+
finishErr(err);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
pi.registerCommand("omp-vcc", {
|
|
302
|
+
description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
|
|
303
|
+
handler: async (args: string, ctx: unknown) => {
|
|
304
|
+
const c = ctx as {
|
|
305
|
+
compact: (options?: unknown) => Promise<void> | void;
|
|
306
|
+
ui: { notify: (msg: string, level?: string) => void };
|
|
307
|
+
getSystemPrompt?: () => unknown;
|
|
308
|
+
};
|
|
309
|
+
await runCompactCommand(args, c, buildOmpCustomInstructions, "Compacted with omp-vcc", true, getCompactForm(() => c.getSystemPrompt?.()));
|
|
220
310
|
},
|
|
221
311
|
});
|
|
222
312
|
|
|
@@ -225,30 +315,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
225
315
|
description: "Alias for /omp-vcc (pi-vcc compat)",
|
|
226
316
|
handler: async (args: string, ctx: unknown) => {
|
|
227
317
|
const c = ctx as {
|
|
228
|
-
compact: (
|
|
318
|
+
compact: (options?: unknown) => Promise<void> | void;
|
|
229
319
|
ui: { notify: (msg: string, level?: string) => void };
|
|
320
|
+
getSystemPrompt?: () => unknown;
|
|
230
321
|
};
|
|
231
|
-
|
|
232
|
-
const keep = parsed.keepUserTurns;
|
|
233
|
-
const followUpPrompt = parsed.followUpPrompt;
|
|
234
|
-
const customInstructions = buildPiVccCustomInstructions(keep);
|
|
235
|
-
try {
|
|
236
|
-
await c.compact(customInstructions);
|
|
237
|
-
const stats = getLastCompactionStats(pi);
|
|
238
|
-
if (stats) scheduleCompactionStatsNotify(c as unknown as { ui: { notify: (msg: string, level?: string) => void } }, stats);
|
|
239
|
-
else try { c.ui.notify("Compacted with pi-vcc (via omp-vcc)", "info"); } catch {}
|
|
240
|
-
if (followUpPrompt) {
|
|
241
|
-
try {
|
|
242
|
-
const piAny = pi as unknown as { sendUserMessage?: (content: string) => Promise<void> | void };
|
|
243
|
-
if (piAny.sendUserMessage) await piAny.sendUserMessage(followUpPrompt);
|
|
244
|
-
} catch {}
|
|
245
|
-
}
|
|
246
|
-
} catch (err: unknown) {
|
|
247
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
248
|
-
const cancelled = msg === "Compaction cancelled" || msg === "Already compacted";
|
|
249
|
-
const note = cancelled ? "Nothing to compact" : `Compaction failed: ${msg}`;
|
|
250
|
-
try { c.ui.notify(note, cancelled ? "warning" : "error"); } catch {}
|
|
251
|
-
}
|
|
322
|
+
await runCompactCommand(args, c, buildPiVccCustomInstructions, "Compacted with pi-vcc (via omp-vcc)", false, getCompactForm(() => c.getSystemPrompt?.()));
|
|
252
323
|
},
|
|
253
324
|
});
|
|
254
325
|
|
|
@@ -265,19 +336,30 @@ export default function (pi: ExtensionAPI): void {
|
|
|
265
336
|
try { c.ui.notify("No session file available.", "error"); } catch {}
|
|
266
337
|
return;
|
|
267
338
|
}
|
|
268
|
-
const { query, scope, page } = parseRecallCommandArgs(args);
|
|
339
|
+
const { query, scope, mode, page } = parseRecallCommandArgs(args);
|
|
269
340
|
const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
|
|
270
341
|
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
271
342
|
if (!query) {
|
|
272
|
-
const { rendered } =
|
|
343
|
+
const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
344
|
+
if (mode === "file") {
|
|
345
|
+
const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
|
|
346
|
+
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
|
|
347
|
+
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (mode === "touched") {
|
|
351
|
+
const output = formatTouchedOutput(getTouchedFiles(rawMessages as unknown[], rendered), page);
|
|
352
|
+
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
273
355
|
const recent = rendered.slice(-DEFAULT_RECENT);
|
|
274
356
|
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
|
|
275
357
|
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
276
358
|
try { c.ui.notify(`vcc_recall: ${recent.length} recent`, "info"); } catch {}
|
|
277
359
|
return;
|
|
278
360
|
}
|
|
279
|
-
const { rendered, rawMessages } =
|
|
280
|
-
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
|
|
361
|
+
const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
362
|
+
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query, { mode });
|
|
281
363
|
const totalPages = Math.ceil(hits.length / PAGE_SIZE);
|
|
282
364
|
const scopeSuffix = scope === "all" ? " (scope: all)" : "";
|
|
283
365
|
const scopeArg = scope === "all" ? " scope:all" : "";
|
|
@@ -311,17 +393,28 @@ export default function (pi: ExtensionAPI): void {
|
|
|
311
393
|
try { c.ui.notify("No session file available.", "error"); } catch {}
|
|
312
394
|
return;
|
|
313
395
|
}
|
|
314
|
-
const { query, scope, page } = parseRecallCommandArgs(args);
|
|
396
|
+
const { query, scope, mode, page } = parseRecallCommandArgs(args);
|
|
315
397
|
const lineageEntryIds = scope === "lineage" ? getActiveLineageEntryIds(c.sessionManager as unknown as { getBranch: () => { id?: string }[] }) : undefined;
|
|
316
398
|
const piAny = pi as unknown as { sendMessage?: (msg: unknown, opts?: unknown) => void };
|
|
317
|
-
const { rendered, rawMessages } =
|
|
399
|
+
const { rendered, rawMessages } = loadRecallMessages(ctx, sessionFile, false, lineageEntryIds);
|
|
318
400
|
if (!query) {
|
|
401
|
+
if (mode === "file") {
|
|
402
|
+
const { hits } = searchEntriesDetailed(rendered, rawMessages as unknown[], undefined, { mode });
|
|
403
|
+
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(hits);
|
|
404
|
+
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (mode === "touched") {
|
|
408
|
+
const output = formatTouchedOutput(getTouchedFiles(rawMessages as unknown[], rendered), page);
|
|
409
|
+
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
319
412
|
const recent = rendered.slice(-DEFAULT_RECENT);
|
|
320
413
|
const output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(recent);
|
|
321
414
|
try { piAny.sendMessage?.({ customType: "vcc-recall", content: output, display: true }, { triggerTurn: false }); } catch {}
|
|
322
415
|
return;
|
|
323
416
|
}
|
|
324
|
-
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query);
|
|
417
|
+
const { hits, totalBeforeCap, truncated } = searchEntriesDetailed(rendered, rawMessages as unknown[], query, { mode });
|
|
325
418
|
const totalPages = Math.ceil(hits.length / PAGE_SIZE);
|
|
326
419
|
const scopeSuffix = scope === "all" ? " (scope: all)" : "";
|
|
327
420
|
const scopeArg = scope === "all" ? " scope:all" : "";
|