pi-supernova 0.9.0 → 0.9.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-supernova",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "CodeMode for Pi and OMP: read, edit, write and bash, with transactional files, source views and shared-input program batches.",
5
5
  "type": "module",
6
6
  "author": "AdityaVG13",
@@ -40,10 +40,13 @@ export class Frecency {
40
40
  score(filePath, mtimeSec, now = Date.now() / 1000) {
41
41
  let total = 0;
42
42
  const cutoff = now - AI_MAX_HISTORY_DAYS * 86400;
43
+ const stamps = this.access.get(filePath);
43
44
 
44
- for (const t of this.access.get(filePath) || []) {
45
- if (t < cutoff) continue;
46
- total += Math.exp(-AI_DECAY * ((now - t) / 86400));
45
+ if (stamps) {
46
+ for (const t of stamps) {
47
+ if (t < cutoff) continue;
48
+ total += Math.exp(-AI_DECAY * ((now - t) / 86400));
49
+ }
47
50
  }
48
51
 
49
52
  if (mtimeSec) {
@@ -74,33 +77,42 @@ function isBoundary(hay, i) {
74
77
  }
75
78
 
76
79
  /**
77
- * Greedy forward match with backward tightening (fzf v1). Returns null or
78
- * { score, start, end }. Score: +16 boundary, +8 consecutive, +4 case match, −1 per gap char.
80
+ * Greedy forward scan. Returns the match end or the needle index that failed
81
+ * (failAt), which the typo retry uses to prune deletions provably unable to
82
+ * match (see matchWithTypos).
79
83
  */
80
- function matchOnce(needle, hay, caseSensitive) {
81
- const hayCmp = caseSensitive ? hay : hay.toLowerCase();
82
- const nCmp = caseSensitive ? needle : needle.toLowerCase();
84
+ function scanForward(nCmp, hayCmp) {
83
85
  let hi = 0;
84
- let firstAt = -1;
85
86
 
86
87
  for (let ni = 0; ni < nCmp.length; ni++) {
87
88
  hi = hayCmp.indexOf(nCmp[ni], hi);
88
89
 
89
- if (hi < 0) return null;
90
-
91
- if (firstAt < 0) firstAt = hi;
90
+ if (hi < 0) return { failAt: ni };
92
91
  hi++;
93
92
  }
94
93
 
95
- const end = hi;
94
+ return { end: hi, failAt: -1 };
95
+ }
96
+
97
+ /**
98
+ * Greedy forward match with backward tightening (fzf v1). Returns null or
99
+ * { score, start, end }. Score: +16 boundary, +8 consecutive, +4 case match, −1 per gap char.
100
+ * Lowered strings arrive precomputed: the needle once per query, the haystack
101
+ * once per path — never re-lowered per part or per typo variant.
102
+ */
103
+ function matchOnce(part, pCmp, hay, hayCmp) {
104
+ const scan = scanForward(pCmp, hayCmp);
105
+
106
+ if (scan.failAt >= 0) return null;
107
+ const end = scan.end;
96
108
  // Tighten: walk backwards from end to find the latest possible start.
97
109
  let start = end;
98
110
 
99
- for (let ni = nCmp.length - 1; ni >= 0; ni--) {
100
- start = hayCmp.lastIndexOf(nCmp[ni], start - 1);
111
+ for (let ni = pCmp.length - 1; ni >= 0; ni--) {
112
+ start = hayCmp.lastIndexOf(pCmp[ni], start - 1);
101
113
  }
102
114
 
103
- return { score: scoreAlignment(needle, nCmp, hay, hayCmp, start), start, end };
115
+ return { score: scoreAlignment(part, pCmp, hay, hayCmp, start), start, end };
104
116
  }
105
117
 
106
118
  /** +16 boundary, +8 consecutive, +4 exact-case, −1 per skipped haystack char. */
@@ -122,9 +134,14 @@ function scoreAlignment(needle, nCmp, hay, hayCmp, start) {
122
134
  return score;
123
135
  }
124
136
 
125
- function considerShorter(part, typosLeft, visit, best) {
126
- for (let i = 0; i < part.length; i++) {
127
- const m = visit(part.slice(0, i) + part.slice(i + 1), typosLeft - 1);
137
+ function considerShorter(sub, subCmp, subLower, typosLeft, visit, best, maxDel) {
138
+ for (let i = 0; i <= maxDel; i++) {
139
+ const m = visit(
140
+ sub.slice(0, i) + sub.slice(i + 1),
141
+ subCmp.slice(0, i) + subCmp.slice(i + 1),
142
+ subLower.slice(0, i) + subLower.slice(i + 1),
143
+ typosLeft - 1,
144
+ );
128
145
 
129
146
  if (!m) continue;
130
147
  const scored = { ...m, score: m.score - 12, typos: m.typos + 1, exact: false };
@@ -135,46 +152,95 @@ function considerShorter(part, typosLeft, visit, best) {
135
152
  return best;
136
153
  }
137
154
 
138
- function matchWithTypos(needle, hay, maxTypos, caseSensitive) {
155
+ // Failure pruning (exact, not heuristic): a deletion strictly after the
156
+ // fail index preserves the failing prefix, so that child fails too — and
157
+ // every deeper success deletes an early char first, which the unpruned
158
+ // order reaches with the same typo count via memo. On success all
159
+ // deletions are still explored (a shorter variant can outscore the -12).
160
+ // This turns full-miss retries from O(len^typos) attempts into O(len×typos).
161
+ function matchWithTypos(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos) {
139
162
  const memo = new Map();
163
+ let hayLower = hayLowerOrNull;
140
164
 
141
- const visit = (part, typosLeft) => {
142
- const key = part + "\0" + typosLeft;
165
+ const visit = (sub, subCmp, subLower, typosLeft) => {
166
+ const key = sub + "\0" + typosLeft;
143
167
 
144
168
  if (memo.has(key)) return memo.get(key);
145
- let best = matchOnce(part, hay, caseSensitive);
169
+ const scan = scanForward(subCmp, hayCmp);
170
+ let best = null;
171
+ let maxDel = sub.length - 1;
172
+
173
+ if (scan.failAt < 0) {
174
+ let start = scan.end;
175
+
176
+ for (let ni = subCmp.length - 1; ni >= 0; ni--) {
177
+ start = hayCmp.lastIndexOf(subCmp[ni], start - 1);
178
+ }
146
179
 
147
- if (best) best = { ...best, typos: 0, exact: hay.toLowerCase() === part.toLowerCase() };
180
+ if (hayLower === null) hayLower = hay.toLowerCase();
181
+ best = {
182
+ score: scoreAlignment(sub, subCmp, hay, hayCmp, start),
183
+ start,
184
+ end: scan.end,
185
+ typos: 0,
186
+ exact: hayLower === subLower,
187
+ };
188
+ } else {
189
+ maxDel = scan.failAt;
190
+ }
148
191
 
149
- if (typosLeft > 0) best = considerShorter(part, typosLeft, visit, best);
192
+ if (typosLeft > 0) best = considerShorter(sub, subCmp, subLower, typosLeft, visit, best, maxDel);
150
193
  memo.set(key, best);
151
194
 
152
195
  return best;
153
196
  };
154
197
 
155
- return visit(needle, maxTypos);
198
+ return visit(part, pCmp, partLower, maxTypos);
199
+ }
200
+
201
+ function matchPart(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos) {
202
+ const direct = matchOnce(part, pCmp, hay, hayCmp);
203
+
204
+ if (direct) {
205
+ const hayLower = hayLowerOrNull === null ? hay.toLowerCase() : hayLowerOrNull;
206
+
207
+ return { ...direct, typos: 0, exact: hayLower === partLower };
208
+ }
209
+
210
+ if (maxTypos <= 0 || part.length < 3 || part.length > 128) return null;
211
+
212
+ return matchWithTypos(part, pCmp, partLower, hay, hayCmp, hayLowerOrNull, maxTypos);
156
213
  }
157
214
 
158
215
  /** Best match allowing up to maxTypos skipped needle characters. */
159
216
  export function fuzzyMatch(needle, hay, { maxTypos = 0, caseSensitive = false } = {}) {
160
- const direct = matchOnce(needle, hay, caseSensitive);
161
-
162
- if (direct) return { ...direct, typos: 0, exact: hay.toLowerCase() === needle.toLowerCase() };
217
+ if (caseSensitive) return matchPart(needle, needle, needle.toLowerCase(), hay, hay, null, maxTypos);
163
218
 
164
- if (maxTypos <= 0 || needle.length < 3 || needle.length > 128) return null;
219
+ const needleLower = needle.toLowerCase();
220
+ const hayLower = hay.toLowerCase();
165
221
 
166
- return matchWithTypos(needle, hay, maxTypos, caseSensitive);
222
+ return matchPart(needle, needleLower, needleLower, hay, hayLower, hayLower, maxTypos);
167
223
  }
168
224
 
169
225
  export function smartCase(query) {
170
226
  return /[A-Z]/.test(query);
171
227
  }
172
228
 
229
+ function splitDirSegs(dir) {
230
+ return dir.split("/").filter(Boolean);
231
+ }
232
+
173
233
  /** fff distance penalty: directory hops from the current file's directory, floor −20. */
174
- function distancePenalty(currentDir, candidateDir) {
175
- if (!currentDir) return 0;
176
- const a = currentDir.split("/").filter(Boolean);
177
- const b = candidateDir.split("/").filter(Boolean);
234
+ function distancePenalty(currentSegs, candidateDir, dirCache) {
235
+ if (!currentSegs) return 0;
236
+ let b = dirCache.get(candidateDir);
237
+
238
+ if (!b) {
239
+ b = splitDirSegs(candidateDir);
240
+ dirCache.set(candidateDir, b);
241
+ }
242
+
243
+ const a = currentSegs;
178
244
  let common = 0;
179
245
 
180
246
  while (common < a.length && common < b.length && a[common] === b[common]) common++;
@@ -191,13 +257,14 @@ function partTypos(parts, ctx) {
191
257
  return ctx.maxTypos ?? (parts[0].length >= 6 ? 2 : parts[0].length >= 4 ? 1 : 0);
192
258
  }
193
259
 
194
- function scoredPath(rel, parts, maxTypos, caseSensitive, ctx, currentDir) {
195
- const matched = matchParts(parts, rel, maxTypos, caseSensitive);
260
+ function scoredPath(rel, parts, partLower, maxTypos, caseSensitive, ctx, currentSegs, dirCache) {
261
+ const hayCmp = caseSensitive ? rel : rel.toLowerCase();
262
+ const matched = matchParts(parts, partLower, rel, hayCmp, caseSensitive ? null : hayCmp, maxTypos, caseSensitive);
196
263
 
197
264
  if (!matched) return null;
198
265
  const { base, first, exact } = matched;
199
266
  const filenameStart = rel.lastIndexOf("/") + 1;
200
- const boosts = filenameBonus(base, rel, filenameStart, first, parts[0]) + contextBoost(base, rel, ctx) + distancePenalty(currentDir, rel.slice(0, filenameStart));
267
+ const boosts = filenameBonus(base, rel, filenameStart, first, partLower[0]) + contextBoost(base, rel, ctx) + distancePenalty(currentSegs, rel.slice(0, filenameStart), dirCache);
201
268
 
202
269
  return { path: rel, score: base + boosts, exact, typos: first.typos };
203
270
  }
@@ -208,11 +275,17 @@ export function rankPaths(query, paths, ctx = {}) {
208
275
  if (parts.length === 0 || parts.length > 16) return [];
209
276
  const caseSensitive = smartCase(query);
210
277
  const maxTypos = partTypos(parts, ctx);
278
+ // Per-query hoists: lowered parts once (not once per path per part), the
279
+ // current directory split once (not once per candidate), plus a
280
+ // per-call cache for candidate directory segments (paths share dirs).
281
+ const partLower = parts.map((p) => p.toLowerCase());
211
282
  const currentDir = ctx.currentFile ? ctx.currentFile.slice(0, ctx.currentFile.lastIndexOf("/") + 1) : "";
283
+ const currentSegs = currentDir ? splitDirSegs(currentDir) : null;
284
+ const dirCache = new Map();
212
285
  const out = [];
213
286
 
214
287
  for (const rel of paths) {
215
- const scored = scoredPath(rel, parts, maxTypos, caseSensitive, ctx, currentDir);
288
+ const scored = scoredPath(rel, parts, partLower, maxTypos, caseSensitive, ctx, currentSegs, dirCache);
216
289
 
217
290
  if (scored) out.push(scored);
218
291
  }
@@ -223,13 +296,13 @@ export function rankPaths(query, paths, ctx = {}) {
223
296
  }
224
297
 
225
298
  /** Every query part must match; later parts get at most one typo (fff narrows per part). Score is the average. */
226
- function matchParts(parts, rel, maxTypos, caseSensitive) {
299
+ function matchParts(parts, partLower, rel, hayCmp, hayLowerOrNull, maxTypos, caseSensitive) {
227
300
  let sum = 0;
228
301
  let first = null;
229
302
  let exact = true;
230
303
 
231
304
  for (let pi = 0; pi < parts.length; pi++) {
232
- const m = fuzzyMatch(parts[pi], rel, { maxTypos: pi === 0 ? maxTypos : Math.min(maxTypos, 1), caseSensitive });
305
+ const m = matchPart(parts[pi], caseSensitive ? parts[pi] : partLower[pi], partLower[pi], rel, hayCmp, hayLowerOrNull, pi === 0 ? maxTypos : Math.min(maxTypos, 1));
233
306
 
234
307
  if (!m) return null;
235
308
  first ??= m;
@@ -241,10 +314,10 @@ function matchParts(parts, rel, maxTypos, caseSensitive) {
241
314
  }
242
315
 
243
316
  /** fff: exact filename +40% of base, any filename match +20%. */
244
- function filenameBonus(base, rel, filenameStart, first, needle) {
317
+ function filenameBonus(base, rel, filenameStart, first, needleLower) {
245
318
  if (first.start < filenameStart) return 0;
246
319
 
247
- return rel.slice(filenameStart).toLowerCase() === needle.toLowerCase() ? Math.floor((base * 2) / 5) : Math.floor(base / 5);
320
+ return rel.slice(filenameStart).toLowerCase() === needleLower ? Math.floor((base * 2) / 5) : Math.floor(base / 5);
248
321
  }
249
322
 
250
323
  /** fff: frecency boost base·f/100 and +15% for git-modified files. */
@@ -12,6 +12,14 @@ const SOURCE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs",
12
12
 
13
13
  const TYPED_EXT = new Set([".ts", ".tsx", ".rs", ".go"]);
14
14
 
15
+ const BUILD_DIRS = new Set(["node_modules", "dist", "target"]);
16
+
17
+ const TEST_WORDS = new Set(["test", "tests", "testing", "spec", "specs"]);
18
+
19
+ const TYPE_WORDS = new Set(["type", "types", "interface", "interfaces", "schema", "schemas"]);
20
+
21
+ const DOC_WORDS = new Set(["doc", "docs", "documentation", "readme"]);
22
+
15
23
  const MAX_NEEDLE_CHARS = 128;
16
24
 
17
25
  /** Light suffix stripping so "terminated" ⊇ "terminat" matches "terminate"; deterministic, no dictionary. */
@@ -27,9 +35,9 @@ export function tokenizeQuery(query) {
27
35
 
28
36
  return {
29
37
  tokens: [...new Set(words.filter(word => word.length > 1 && !STOP_WORDS.has(word)))],
30
- wantsTest: words.some(word => ["test", "tests", "testing", "spec", "specs"].includes(word)),
31
- wantsType: words.some(word => ["type", "types", "interface", "interfaces", "schema", "schemas"].includes(word)),
32
- wantsDoc: words.some(word => ["doc", "docs", "documentation", "readme"].includes(word)),
38
+ wantsTest: words.some(word => TEST_WORDS.has(word)),
39
+ wantsType: words.some(word => TYPE_WORDS.has(word)),
40
+ wantsDoc: words.some(word => DOC_WORDS.has(word)),
33
41
  };
34
42
  }
35
43
 
@@ -37,7 +45,8 @@ function tokenPathScore(base, words, normalized, tokens) {
37
45
  let score = 0;
38
46
 
39
47
  for (const token of tokens) {
40
- if (base === token || base.startsWith(token + ".")) score += 60;
48
+ // base === token + "." without the concat alloc: same verdict, no garbage.
49
+ if (base === token || (base.length > token.length && base[token.length] === "." && base.startsWith(token))) score += 60;
41
50
  else if (base.includes(token)) score += 30;
42
51
  else if (words.includes(token)) score += 15;
43
52
  else if (normalized.includes(token)) score += 5;
@@ -49,7 +58,7 @@ function tokenPathScore(base, words, normalized, tokens) {
49
58
  function topologyPenalty(normalized, flags) {
50
59
  const parts = normalized.split("/");
51
60
 
52
- if (parts.some(part => ["node_modules", "dist", "target"].includes(part))) return -100;
61
+ if (parts.some(part => BUILD_DIRS.has(part))) return -100;
53
62
  const test = isTestPath(normalized);
54
63
 
55
64
  if (test && !flags.wantsTest) return -50;
@@ -113,6 +113,19 @@ function grepEntryRows(e, filePath, root, regex, nameRegex, out) {
113
113
  }
114
114
  }
115
115
 
116
+ // One alternation scan per file instead of one full scan per needle — same
117
+ // verdict as needles.some(includes). Needles are escaped: they arrive as
118
+ // literals that may carry regex syntax.
119
+ function anyOfProbe(needles) {
120
+ return new RegExp(needles.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"));
121
+ }
122
+
123
+ function fileMatchesNeedles(entry, needles, anyOf, probe) {
124
+ if (probe) return probe.test(entry.lower);
125
+
126
+ return anyOf ? needles.some((n) => entry.lower.includes(n)) : needles.every((n) => entry.lower.includes(n));
127
+ }
128
+
116
129
  export class WorkspaceIndex {
117
130
  constructor(runCommand) {
118
131
  this.runCommand = runCommand;
@@ -358,14 +371,12 @@ export class WorkspaceIndex {
358
371
  /** Files whose lowercase text contains any (or every) needle; needles are lowercase. */
359
372
  filesContaining(files, needles, anyOf) {
360
373
  const hits = [];
374
+ const probe = anyOf && needles.length > 1 ? anyOfProbe(needles) : null;
361
375
 
362
376
  for (const filePath of files) {
363
377
  const e = this.entry(filePath);
364
378
 
365
- if (!e) continue;
366
- const found = anyOf ? needles.some((n) => e.lower.includes(n)) : needles.every((n) => e.lower.includes(n));
367
-
368
- if (found) hits.push(filePath);
379
+ if (e && fileMatchesNeedles(e, needles, anyOf, probe)) hits.push(filePath);
369
380
  }
370
381
 
371
382
  return hits;
@@ -15,17 +15,15 @@ function inScope(filePath, dir, includeHidden) {
15
15
  return !parts.includes(".git") && (includeHidden || !parts.some(part => part.startsWith(".") && part.length > 1));
16
16
  }
17
17
 
18
- function makeCandidate(filePath, dir, query, tokens, flags) {
18
+ function makeCandidate(filePath, dir, query, tokens, flags, needles = tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS))) {
19
19
  const relative = path.relative(dir, filePath);
20
20
  const lower = relative.toLowerCase();
21
21
  const base = path.basename(lower);
22
22
 
23
23
  const extension = path.extname(base);
24
24
  const stemBase = extension ? base.slice(0, -extension.length) : base;
25
- const exactPath = lower === query.toLowerCase() || base === query.toLowerCase()
26
- || stemBase === query.toLowerCase();
27
-
28
- const needles = tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
25
+ const queryLower = query.toLowerCase();
26
+ const exactPath = lower === queryLower || base === queryLower || stemBase === queryLower;
29
27
 
30
28
  return { path: filePath, pathScore: scorePathTopology(relative, tokens, flags), exactPath,
31
29
  pathCoverage: tokens.filter((token, index) => lower.includes(needles[index] ?? token)).length,
@@ -120,7 +118,7 @@ function parseRgRecord(line, truncated, isLast) {
120
118
  }
121
119
  }
122
120
 
123
- function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles) {
121
+ function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles) {
124
122
  if (record.type !== "match" && record.type !== "context") return;
125
123
  const data = record.data;
126
124
 
@@ -131,21 +129,21 @@ function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candid
131
129
  let candidate = candidates.get(filePath);
132
130
 
133
131
  if (!candidate) {
134
- candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
132
+ candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags, candidateNeedles);
135
133
  candidates.set(filePath, candidate);
136
134
  }
137
135
 
138
136
  inspectLine(candidate, data.line_number, data.lines.text, query, tokens, needles, record.type === "match");
139
137
  }
140
138
 
141
- function overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
139
+ function overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal) {
142
140
  let overlayTruncated = false;
143
141
 
144
142
  for (const filePath of pendingPaths) {
145
143
  const pending = overlayText(filePath);
146
144
 
147
145
  if (pending === undefined) continue;
148
- const candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
146
+ const candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags, candidateNeedles);
149
147
  overlayTruncated = inspectOverlay(candidate, pending, needles, query, tokens, signal) || overlayTruncated;
150
148
 
151
149
  if (candidate.matched.size) candidates.set(filePath, candidate);
@@ -176,7 +174,7 @@ async function runContentSearch({ dir, includeHidden, searchNeedles, run, overla
176
174
  return response;
177
175
  }
178
176
 
179
- function absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
177
+ function absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal) {
180
178
  const records = response.stdout.split("\n");
181
179
 
182
180
  for (let i = 0; i < records.length; i++) {
@@ -185,17 +183,20 @@ function absorbRgRecords(candidates, response, dir, includeHidden, overlayText,
185
183
 
186
184
  if (record === undefined) break;
187
185
  if (!record) continue;
188
- absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles);
186
+ absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles);
189
187
  }
190
188
  }
191
189
 
192
190
  async function contentCandidates({ dir, includeHidden, query, tokens, flags, pendingPaths, run, overlayText, signal, exact, diskFiles, focusFile }) {
193
191
  const needles = exact ? [query.toLowerCase().slice(0, MAX_NEEDLE_CHARS)] : tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
192
+ // Coverage needles are always token-derived (even in exact mode, where the
193
+ // search needles collapse to the query): computed once, not once per file.
194
+ const candidateNeedles = exact ? tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS)) : needles;
194
195
  const candidateRoot = focusFile ? path.dirname(focusFile) : dir;
195
196
  const candidates = new Map();
196
197
  const response = await runContentSearch({ dir, includeHidden, searchNeedles: [...new Set(needles)], run, overlayText, signal, diskFiles, focusFile });
197
- absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal);
198
- const overlayTruncated = overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal);
198
+ absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal);
199
+ const overlayTruncated = overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, candidateNeedles, signal);
199
200
 
200
201
  return { candidates, truncated: response.outputTruncated === true || overlayTruncated };
201
202
  }