diff --git a/packages/pi-fff/src/index.ts b/packages/pi-fff/src/index.ts index 7c8091f4..761d9001 100644 --- a/packages/pi-fff/src/index.ts +++ b/packages/pi-fff/src/index.ts @@ -285,6 +285,10 @@ export default function fffExtension(pi: ExtensionAPI) { // deadlock at the native layer (issue #403). let finderPromise: Promise | null = null; let activeCwd = process.cwd(); + // Bumped on every session_start / session_shutdown so an async warmup + // started for a previous session can detect that it's stale and bail out + // before touching `finder` / notifying a destroyed UI. + let lifecycleId = 0; // Mode resolution: flag > env > default let currentMode: FffMode = @@ -355,10 +359,13 @@ export default function fffExtension(pi: ExtensionAPI) { if (!result.ok) throw new Error(`Failed to create FFF file finder: ${result.error}`); - finder = result.value; + const created = result.value; + finder = created; finderCwd = cwd; - await finder.waitForScan(15000); - return finder; + await created.waitForScan(15000); + // Return the local handle: a shutdown during warmup may null/replace + // `finder`, but the caller still needs the instance they were promised. + return created; })().finally(() => { finderPromise = null; }); @@ -467,6 +474,7 @@ export default function fffExtension(pi: ExtensionAPI) { pi.on("session_start", async (_event, ctx) => { try { activeCwd = ctx.cwd; + const sessionLifecycleId = ++lifecycleId; // Restore persisted mode from session entries. This handles session // resume after process restart where env vars are lost, and ensures @@ -492,7 +500,20 @@ export default function fffExtension(pi: ExtensionAPI) { } registerAutocompleteProvider(ctx); - await ensureFinder(activeCwd); + + // Warm the finder in the background — Pi /new and /resume must not + // wait on the initial scan. Subsequent tool calls / mention lookups + // share the same in-flight promise via ensureFinder(). + setTimeout(() => { + if (sessionLifecycleId !== lifecycleId) return; + ensureFinder(activeCwd).catch((e: unknown) => { + if (sessionLifecycleId !== lifecycleId) return; + ctx.ui.notify( + `FFF init failed: ${e instanceof Error ? e.message : String(e)}`, + "error", + ); + }); + }, 0); } catch (e: unknown) { ctx.ui.notify( `FFF init failed: ${e instanceof Error ? e.message : String(e)}`, @@ -502,6 +523,7 @@ export default function fffExtension(pi: ExtensionAPI) { }); pi.on("session_shutdown", async () => { + lifecycleId++; destroyFinder(); }); diff --git a/packages/pi-fff/test/extension.test.ts b/packages/pi-fff/test/extension.test.ts index 1fa80ef4..a6820115 100644 --- a/packages/pi-fff/test/extension.test.ts +++ b/packages/pi-fff/test/extension.test.ts @@ -118,6 +118,10 @@ async function start(mode?: string) { const sessionStart = setup.events.get("session_start"); expect(sessionStart).toBeDefined(); await sessionStart?.({ reason: "startup" }, ctx); + // session_start now schedules the finder warmup via setTimeout(0); flush + // the queue so tests can observe FileFinder.create / waitForScan calls. + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); return { ...setup, ctx }; }