From 3081d070f8a0e4af3b17ee9aff1cabd93a618782 Mon Sep 17 00:00:00 2001 From: Gil Rodrigues Date: Sat, 29 Aug 2026 10:37:42 +0200 Subject: [PATCH] fix(pi-fff): resume fuzzy-fallback grep cursors in fuzzy mode The grep tool's fuzzy fallback paginates its result and prints the engine cursor. On resume that cursor was replayed against the literal query (mode plain/regex) that had matched nothing, and the fallback is skipped for cursor requests, so every continuation returned "No matches found". Tag stored cursors with the stream they came from: fuzzy cursors resume a fuzzy grep, literal cursors resume unchanged, multi_grep cursors stay literal. Verified with a runtime contract harness: exact pagination round-trips unchanged, fuzzy fallback page 1 emits a cursor, and resuming it now returns fuzzy page 2 (previously "No matches found"). --- packages/pi-fff/src/index.ts | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 5a7e64cca..3131bdf00 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -78,12 +78,20 @@ function resolveToolNames(mode: FffMode): ToolNames { // Cursor store — simple bounded Map for pagination cursors // --------------------------------------------------------------------------- -const cursorCache = new Map(); +// Fuzzy-fallback cursors must resume a fuzzy query: the engine cursor is bound +// to the match stream that produced it, so replaying it against the literal +// query (which matched nothing) would turn every resume into "No matches found". +interface StoredCursor { + cursor: GrepCursor; + fuzzy: boolean; +} + +const cursorCache = new Map(); let cursorCounter = 0; -function storeCursor(cursor: GrepCursor): string { +function storeCursor(cursor: GrepCursor, fuzzy: boolean): string { const id = `fff_c${++cursorCounter}`; - cursorCache.set(id, cursor); + cursorCache.set(id, { cursor, fuzzy }); if (cursorCache.size > 200) { const first = cursorCache.keys().next().value; if (first) cursorCache.delete(first); @@ -91,7 +99,7 @@ function storeCursor(cursor: GrepCursor): string { return id; } -function getCursor(id: string): GrepCursor | undefined { +function getCursor(id: string): StoredCursor | undefined { return cursorCache.get(id); } @@ -897,12 +905,15 @@ export default function fffExtension(pi: ExtensionAPI) { // (case-insensitive when pattern is all lowercase). const smartCase = params.caseSensitive !== true; + const storedCursor = params.cursor ? getCursor(params.cursor) : undefined; + const resumeFuzzy = storedCursor?.fuzzy === true; + const grepResult = picker.grep(query, { - mode, + mode: resumeFuzzy ? "fuzzy" : mode, smartCase, maxMatchesPerFile: pageSize, pageSize, - cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null, + cursor: storedCursor?.cursor ?? null, beforeContext: context, afterContext: context, classifyDefinitions: true, @@ -954,7 +965,9 @@ export default function fffExtension(pi: ExtensionAPI) { notices.push(`Invalid regex: ${result.regexFallbackError}, used literal match`); } if (result.nextCursor) { - notices.push(`Continue with cursor="${storeCursor(result.nextCursor)}"`); + notices.push( + `Continue with cursor="${storeCursor(result.nextCursor, fuzzyNotice !== null || resumeFuzzy)}"`, + ); } if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`; @@ -1186,7 +1199,7 @@ export default function fffExtension(pi: ExtensionAPI) { maxMatchesPerFile: pageSize, pageSize, smartCase: true, - cursor: (params.cursor ? getCursor(params.cursor) : null) ?? null, + cursor: (params.cursor ? getCursor(params.cursor)?.cursor : null) ?? null, beforeContext: context, afterContext: context, }); @@ -1201,7 +1214,7 @@ export default function fffExtension(pi: ExtensionAPI) { notices.push(`${effectiveLimit}+ matches (refine patterns)`); if (result.nextCursor) notices.push( - `More available. cursor="${storeCursor(result.nextCursor)}" to continue`, + `More available. cursor="${storeCursor(result.nextCursor, false)}" to continue`, ); if (notices.length > 0) output += `\n\n[${notices.join(". ")}]`;