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.
@@ -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 parsed = parseRecallScope(raw);
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: parsed.scope, page };
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 with entry indices."),
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 (normalizeRecallMode(p.mode) === "touched") {
133
- const { rendered, rawMessages } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 } = loadAllMessages(sessionFile, true, lineageEntryIds);
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 output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(expanded);
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 output = (scope === "all" ? "Scope: all\n\n" : "") + formatRecallOutput(msgs.slice(-DEFAULT_RECENT), q);
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
- pi.registerCommand("omp-vcc", {
184
- description: "Compact conversation with omp-vcc structured summary (keep:N + optional focus)",
185
- handler: async (args: string, ctx: unknown) => {
186
- const c = ctx as {
187
- compact: (instructions?: string) => Promise<void>;
188
- ui: { notify: (msg: string, level?: string) => void };
189
- sessionManager?: { getSessionFile?: () => string | undefined };
190
- };
191
- const parsed = parseKeepAndPrompt(args);
192
- const keep = parsed.keepUserTurns;
193
- const followUpPrompt = parsed.followUpPrompt;
194
- const customInstructions = buildOmpCustomInstructions(keep);
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
- await c.compact(customInstructions);
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
- const msg = err instanceof Error ? err.message : String(err);
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: (instructions?: string) => Promise<void>;
318
+ compact: (options?: unknown) => Promise<void> | void;
229
319
  ui: { notify: (msg: string, level?: string) => void };
320
+ getSystemPrompt?: () => unknown;
230
321
  };
231
- const parsed = parseKeepAndPrompt(args);
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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 } = loadAllMessages(sessionFile, false, lineageEntryIds);
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" : "";