diff --git a/src/extraction/index.ts b/src/extraction/index.ts index c026bce..4b40931 100644 --- a/src/extraction/index.ts +++ b/src/extraction/index.ts @@ -51,7 +51,7 @@ const WORKER_RECYCLE_INTERVAL = 250; * Progress callback for indexing operations */ export interface IndexProgress { - phase: 'scanning' | 'parsing' | 'storing' | 'resolving'; + phase: 'scanning' | 'parsing' | 'storing' | 'finalizing' | 'resolving'; current: number; total: number; currentFile?: string; @@ -460,6 +460,16 @@ export class ExtractionOrchestrator { const total = files.length; let processed = 0; + // Emit parsing phase immediately so the progress bar appears during worker setup. + // The yield lets the shimmer worker flush the phase transition to stdout before + // the main thread starts synchronous grammar detection work. + onProgress?.({ + phase: 'parsing', + current: 0, + total, + }); + await new Promise(resolve => setImmediate(resolve)); + // Detect needed languages and load grammars in the parse worker const neededLanguages = [...new Set(files.map((f) => detectLanguage(f)))]; @@ -719,6 +729,18 @@ export class ExtractionOrchestrator { } } + // Report 100% so the progress bar doesn't hang at 99% + onProgress?.({ + phase: 'parsing', + current: total, + total, + }); + + // Yield so the shimmer worker's buffered stdout writes can flush. + // Worker thread stdout is proxied through the main thread's event loop, + // so synchronous work here blocks the animation from rendering. + await new Promise(resolve => setImmediate(resolve)); + // Retry pass: files that failed due to WASM memory corruption may succeed // on a fresh worker with a clean heap. Recycle before each attempt so // every file gets the absolute cleanest WASM state possible. diff --git a/src/index.ts b/src/index.ts index 9d0afb1..92af4e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -390,6 +390,16 @@ export class CodeGraph { // Resolve references to create call/import/extends edges if (result.success && result.filesIndexed > 0) { + // Signal transition so progress bar doesn't hang at "Parsing 100%" + options.onProgress?.({ + phase: 'finalizing', + current: 0, + total: 0, + }); + + // Yield so shimmer worker can flush the phase transition to stdout + await new Promise(resolve => setImmediate(resolve)); + // Get count without loading all refs into memory const unresolvedCount = this.queries.getUnresolvedReferencesCount(); @@ -399,7 +409,7 @@ export class CodeGraph { total: unresolvedCount, }); - this.resolveReferencesBatched((current, total) => { + await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, @@ -479,7 +489,7 @@ export class CodeGraph { total: unresolvedCount, }); - this.resolveReferencesBatched((current, total) => { + await this.resolveReferencesBatched((current, total) => { options.onProgress?.({ phase: 'resolving', current, @@ -540,7 +550,7 @@ export class CodeGraph { * Resolve references in batches to keep memory bounded on large codebases. * Processes chunks of unresolved refs, persisting results after each batch. */ - resolveReferencesBatched(onProgress?: (current: number, total: number) => void): ResolutionResult { + async resolveReferencesBatched(onProgress?: (current: number, total: number) => void): Promise { return this.resolver.resolveAndPersistBatched(onProgress); } diff --git a/src/resolution/index.ts b/src/resolution/index.ts index f4c10aa..b5a4e9e 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -326,10 +326,10 @@ export class ReferenceResolver { * Processes unresolved references in chunks, persisting edges and cleaning * up resolved refs after each batch to avoid accumulating large arrays. */ - resolveAndPersistBatched( + async resolveAndPersistBatched( onProgress?: (current: number, total: number) => void, batchSize: number = 5000 - ): ResolutionResult { + ): Promise { this.warmCaches(); const total = this.queries.getUnresolvedReferencesCount(); @@ -388,6 +388,9 @@ export class ReferenceResolver { processed += batch.length; onProgress?.(processed, total); + // Yield so progress UI can render between batches + await new Promise(resolve => setImmediate(resolve)); + // If nothing was resolved or removed in this batch, we'd loop forever // on the same rows. Break to avoid infinite loop. if (result.resolved.length === 0 && result.unresolved.length === batch.length) { diff --git a/src/ui/shimmer-progress.ts b/src/ui/shimmer-progress.ts index 2535902..9d2191c 100644 --- a/src/ui/shimmer-progress.ts +++ b/src/ui/shimmer-progress.ts @@ -5,6 +5,7 @@ const PHASE_NAMES: Record = { scanning: 'Scanning files', parsing: 'Parsing code', storing: 'Storing data', + finalizing: 'Finalizing', resolving: 'Resolving refs', }; diff --git a/src/ui/shimmer-worker.ts b/src/ui/shimmer-worker.ts index 2644c47..46b9119 100644 --- a/src/ui/shimmer-worker.ts +++ b/src/ui/shimmer-worker.ts @@ -1,6 +1,16 @@ import { parentPort, workerData } from 'worker_threads'; +import { writeSync } from 'fs'; import type { ShimmerWorkerMessage } from './types'; +// Write directly to fd 1 (stdout) instead of writeStdout(). +// In Node.js worker threads, process.stdout is proxied through the main +// thread's event loop — so if the main thread is blocked (e.g. SQLite), +// stdout writes from the worker queue up and the animation freezes. +// fs.writeSync(1, ...) is a direct kernel syscall that bypasses this. +function writeStdout(s: string): void { + writeSync(1, s); +} + const SPINNER_GLYPHS = ['·', '✢', '✳', '✶', '✻', '✽']; const ANIM_INTERVAL = 150; const FRAMES_PER_GLYPH = 3; @@ -74,16 +84,16 @@ function render(): void { line = `${DM}│${RST} ${color}${glyph}${RST} ${currentMessage}...`; } - process.stdout.write(`\r\x1b[K${line}`); + writeStdout(`\r\x1b[K${line}`); } function finishPhase(): void { if (!currentMessage) return; - process.stdout.write(`\r\x1b[K`); + writeStdout(`\r\x1b[K`); let detail = ''; if (currentPercent >= 0) detail = ' — done'; else if (currentCount > 0) detail = ` — ${formatNumber(currentCount)} found`; - process.stdout.write(`${DM}│${RST} ${GRN}◆${RST} ${currentMessage}${detail}\n`); + writeStdout(`${DM}│${RST} ${GRN}◆${RST} ${currentMessage}${detail}\n`); currentMessage = ''; currentPercent = -1; currentCount = 0;