feat: add progress bar to init/reinit with per-file tracking

- processFiles() now accepts onProgress callback (completed, total, currentFile)
- InitOptions.onProgress receives structured ProgressInfo object
- initProject aggregates progress across all directories
- CLI renders ASCII progress bar: [████░░░░░░] 3/20 | filename.ts
- Pi extension sends structured updates with percentage + file details
- All 52 tests passing
This commit is contained in:
2026-06-10 18:43:42 +02:00
parent 78c60f9ca1
commit 9bdb862cca
5 changed files with 655 additions and 568 deletions
+555 -555
View File
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -82,7 +82,13 @@ export default function (pi: ExtensionAPI) {
verbose: false, verbose: false,
llmClient: client, llmClient: client,
cacheDir: ctx.cwd, cacheDir: ctx.cwd,
onProgress: (msg) => _onUpdate?.({ content: [{ type: "text", text: msg }] }), onProgress: (info) => {
const pct = info.total > 0 ? Math.round((info.completed / info.total) * 100) : 0;
_onUpdate?.({
content: [{ type: "text", text: `${info.message} (${pct}%)` }],
details: { progress: pct, file: info.currentFile, dir: info.dir },
});
},
}); });
return { return {
content: [ content: [
@@ -208,7 +214,13 @@ export default function (pi: ExtensionAPI) {
verbose: false, verbose: false,
llmClient: client, llmClient: client,
cacheDir: ctx.cwd, cacheDir: ctx.cwd,
onProgress: (msg) => _onUpdate?.({ content: [{ type: "text", text: msg }] }), onProgress: (info) => {
const pct = info.total > 0 ? Math.round((info.completed / info.total) * 100) : 0;
_onUpdate?.({
content: [{ type: "text", text: `${info.message} (${pct}%)` }],
details: { progress: pct, file: info.currentFile, dir: info.dir },
});
},
}); });
return { return {
content: [ content: [
+32 -2
View File
@@ -63,6 +63,14 @@ function formatCount(count: number, label: string): string {
return `${pc.bold(String(count))} ${count === 1 ? label : plural}`; return `${pc.bold(String(count))} ${count === 1 ? label : plural}`;
} }
function renderProgressBar(completed: number, total: number, currentFile?: string, width = 30): string {
const pct = total > 0 ? completed / total : 0;
const filled = Math.round(width * pct);
const bar = "█".repeat(filled) + "░".repeat(width - filled);
const file = currentFile ? ` | ${pc.dim(currentFile)}` : "";
return `[${pc.cyan(bar)}] ${completed}/${total}${file}`;
}
function parseArgs(args: string[]): { function parseArgs(args: string[]): {
path: string; path: string;
fix: boolean; fix: boolean;
@@ -128,7 +136,18 @@ async function main() {
const entries = discoverProject(targetPath); const entries = discoverProject(targetPath);
console.log(`Scanning ${formatCount(entries.length, "directory")}...`); console.log(`Scanning ${formatCount(entries.length, "directory")}...`);
const client = createClientFromArgs(parsed); const client = createClientFromArgs(parsed);
await initProject(targetPath, { verbose: false, llmClient: client, cacheDir: targetPath }); let lastLine = "";
await initProject(targetPath, {
verbose: false,
llmClient: client,
cacheDir: targetPath,
onProgress: (info) => {
const line = renderProgressBar(info.completed, info.total, info.currentFile);
process.stdout.write("\r" + line.padEnd(lastLine.length));
lastLine = line;
},
});
process.stdout.write("\n");
const elapsed = ((Date.now() - start) / 1000).toFixed(1); const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log( console.log(
`${pc.green("✓")} Generated ${formatCount(entries.length, ".pi-map.md file")} in ${elapsed}s`, `${pc.green("✓")} Generated ${formatCount(entries.length, ".pi-map.md file")} in ${elapsed}s`,
@@ -179,7 +198,18 @@ async function main() {
`Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`, `Regenerating ${formatCount(entries.length, ".pi-map.md file")}...`,
); );
const client = createClientFromArgs(parsed); const client = createClientFromArgs(parsed);
await reinitPath(targetPath, { verbose: false, llmClient: client, cacheDir: targetPath }); let lastLine = "";
await reinitPath(targetPath, {
verbose: false,
llmClient: client,
cacheDir: targetPath,
onProgress: (info) => {
const line = renderProgressBar(info.completed, info.total, info.currentFile);
process.stdout.write("\r" + line.padEnd(lastLine.length));
lastLine = line;
},
});
process.stdout.write("\n");
const elapsed = ((Date.now() - start) / 1000).toFixed(1); const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`); console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`);
break; break;
+48 -7
View File
@@ -12,11 +12,19 @@ import { writeFileSync } from "fs";
import { join } from "path"; import { join } from "path";
import type { LLMClient } from "./llm/llm-client.js"; import type { LLMClient } from "./llm/llm-client.js";
export interface ProgressInfo {
message: string;
completed: number;
total: number;
currentFile?: string;
dir?: string;
}
export interface InitOptions { export interface InitOptions {
verbose?: boolean; verbose?: boolean;
llmClient?: LLMClient; llmClient?: LLMClient;
cacheDir?: string; cacheDir?: string;
onProgress?: (message: string) => void; onProgress?: (info: ProgressInfo) => void;
} }
export async function initProject( export async function initProject(
@@ -24,15 +32,40 @@ export async function initProject(
options: InitOptions = {}, options: InitOptions = {},
): Promise<void> { ): Promise<void> {
const entries = discoverProject(rootPath); const entries = discoverProject(rootPath);
options.onProgress?.(`Scanning ${entries.length} directories...`); const totalFiles = entries.reduce((sum, e) => sum + e.files.length, 0);
let globalCompleted = 0;
options.onProgress?.({
message: `Scanning ${entries.length} directories (${totalFiles} files)...`,
completed: 0,
total: totalFiles,
});
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
const entry = entries[i]; const entry = entries[i];
options.onProgress?.(`[${i + 1}/${entries.length}] Analyzing ${entry.relativePath} (${entry.files.length} files)...`); await generateDirectoryMap(
await generateDirectoryMap(entry, options.llmClient, options.cacheDir, options.onProgress); entry,
options.llmClient,
options.cacheDir,
(info) => {
globalCompleted = info.completed + entries
.slice(0, i)
.reduce((sum, e) => sum + e.files.length, 0);
options.onProgress?.({
...info,
completed: globalCompleted,
total: totalFiles,
dir: entry.relativePath,
});
},
);
} }
options.onProgress?.(`Generated ${entries.length} .pi-map.md files`); options.onProgress?.({
message: `Generated ${entries.length} .pi-map.md files`,
completed: totalFiles,
total: totalFiles,
});
if (options.verbose !== false) { if (options.verbose !== false) {
console.log(`Generated ${entries.length} .pi-map.md files`); console.log(`Generated ${entries.length} .pi-map.md files`);
} }
@@ -42,19 +75,27 @@ export async function generateDirectoryMap(
entry: DirectoryEntry, entry: DirectoryEntry,
llmClient?: LLMClient, llmClient?: LLMClient,
cacheDir?: string, cacheDir?: string,
onProgress?: (message: string) => void, onProgress?: (info: ProgressInfo) => void,
): Promise<FileEntry[]> { ): Promise<FileEntry[]> {
// Process files in parallel (4 concurrent) with retry logic // Process files in parallel (4 concurrent) with retry logic
const fileData = await processFiles( const fileData = await processFiles(
entry.files, entry.files,
async (file) => { async (file) => {
const filePath = join(entry.dirPath, file); const filePath = join(entry.dirPath, file);
onProgress?.(`${file}`);
const llmData = await extractFileLLM(filePath, llmClient, cacheDir); const llmData = await extractFileLLM(filePath, llmClient, cacheDir);
const astData = await extractFileAST(filePath); const astData = await extractFileAST(filePath);
return mergeFileData(file, llmData, astData); return mergeFileData(file, llmData, astData);
}, },
{ concurrency: 4, batchDelayMs: 100, maxRetries: 2 }, { concurrency: 4, batchDelayMs: 100, maxRetries: 2 },
(completed, total, currentFile) => {
onProgress?.({
message: `${currentFile}`,
completed,
total,
currentFile,
dir: entry.relativePath,
});
},
); );
const packageData = await extractPackageLLM( const packageData = await extractPackageLLM(
+6 -2
View File
@@ -45,11 +45,12 @@ export async function processFiles<T, R>(
files: T[], files: T[],
processor: (file: T) => Promise<R>, processor: (file: T) => Promise<R>,
options: BatchOptions = {}, options: BatchOptions = {},
onProgress?: (completed: number, total: number, currentFile: T) => void,
): Promise<R[]> { ): Promise<R[]> {
const opts = { ...DEFAULT_OPTIONS, ...options }; const opts = { ...DEFAULT_OPTIONS, ...options };
const limit = pLimit(opts.concurrency); const limit = pLimit(opts.concurrency);
const results: R[] = []; let completed = 0;
let batchCount = 0; let batchCount = 0;
const tasks = files.map((file, index) => const tasks = files.map((file, index) =>
@@ -59,7 +60,10 @@ export async function processFiles<T, R>(
batchCount++; batchCount++;
await sleep(opts.batchDelayMs); await sleep(opts.batchDelayMs);
} }
return withRetry(() => processor(file), opts); const result = await withRetry(() => processor(file), opts);
completed++;
onProgress?.(completed, files.length, file);
return result;
}), }),
); );