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:
Generated
+555
-555
File diff suppressed because it is too large
Load Diff
+14
-2
@@ -82,7 +82,13 @@ export default function (pi: ExtensionAPI) {
|
||||
verbose: false,
|
||||
llmClient: client,
|
||||
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 {
|
||||
content: [
|
||||
@@ -208,7 +214,13 @@ export default function (pi: ExtensionAPI) {
|
||||
verbose: false,
|
||||
llmClient: client,
|
||||
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 {
|
||||
content: [
|
||||
|
||||
+32
-2
@@ -63,6 +63,14 @@ function formatCount(count: number, label: string): string {
|
||||
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[]): {
|
||||
path: string;
|
||||
fix: boolean;
|
||||
@@ -128,7 +136,18 @@ async function main() {
|
||||
const entries = discoverProject(targetPath);
|
||||
console.log(`Scanning ${formatCount(entries.length, "directory")}...`);
|
||||
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);
|
||||
console.log(
|
||||
`${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")}...`,
|
||||
);
|
||||
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);
|
||||
console.log(`${pc.green("✓")} Regenerated in ${elapsed}s`);
|
||||
break;
|
||||
|
||||
+48
-7
@@ -12,11 +12,19 @@ import { writeFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
import type { LLMClient } from "./llm/llm-client.js";
|
||||
|
||||
export interface ProgressInfo {
|
||||
message: string;
|
||||
completed: number;
|
||||
total: number;
|
||||
currentFile?: string;
|
||||
dir?: string;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
verbose?: boolean;
|
||||
llmClient?: LLMClient;
|
||||
cacheDir?: string;
|
||||
onProgress?: (message: string) => void;
|
||||
onProgress?: (info: ProgressInfo) => void;
|
||||
}
|
||||
|
||||
export async function initProject(
|
||||
@@ -24,15 +32,40 @@ export async function initProject(
|
||||
options: InitOptions = {},
|
||||
): Promise<void> {
|
||||
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++) {
|
||||
const entry = entries[i];
|
||||
options.onProgress?.(`[${i + 1}/${entries.length}] Analyzing ${entry.relativePath} (${entry.files.length} files)...`);
|
||||
await generateDirectoryMap(entry, options.llmClient, options.cacheDir, options.onProgress);
|
||||
await generateDirectoryMap(
|
||||
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) {
|
||||
console.log(`Generated ${entries.length} .pi-map.md files`);
|
||||
}
|
||||
@@ -42,19 +75,27 @@ export async function generateDirectoryMap(
|
||||
entry: DirectoryEntry,
|
||||
llmClient?: LLMClient,
|
||||
cacheDir?: string,
|
||||
onProgress?: (message: string) => void,
|
||||
onProgress?: (info: ProgressInfo) => void,
|
||||
): Promise<FileEntry[]> {
|
||||
// Process files in parallel (4 concurrent) with retry logic
|
||||
const fileData = await processFiles(
|
||||
entry.files,
|
||||
async (file) => {
|
||||
const filePath = join(entry.dirPath, file);
|
||||
onProgress?.(` → ${file}`);
|
||||
const llmData = await extractFileLLM(filePath, llmClient, cacheDir);
|
||||
const astData = await extractFileAST(filePath);
|
||||
return mergeFileData(file, llmData, astData);
|
||||
},
|
||||
{ concurrency: 4, batchDelayMs: 100, maxRetries: 2 },
|
||||
(completed, total, currentFile) => {
|
||||
onProgress?.({
|
||||
message: ` → ${currentFile}`,
|
||||
completed,
|
||||
total,
|
||||
currentFile,
|
||||
dir: entry.relativePath,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const packageData = await extractPackageLLM(
|
||||
|
||||
@@ -45,11 +45,12 @@ export async function processFiles<T, R>(
|
||||
files: T[],
|
||||
processor: (file: T) => Promise<R>,
|
||||
options: BatchOptions = {},
|
||||
onProgress?: (completed: number, total: number, currentFile: T) => void,
|
||||
): Promise<R[]> {
|
||||
const opts = { ...DEFAULT_OPTIONS, ...options };
|
||||
const limit = pLimit(opts.concurrency);
|
||||
|
||||
const results: R[] = [];
|
||||
let completed = 0;
|
||||
let batchCount = 0;
|
||||
|
||||
const tasks = files.map((file, index) =>
|
||||
@@ -59,7 +60,10 @@ export async function processFiles<T, R>(
|
||||
batchCount++;
|
||||
await sleep(opts.batchDelayMs);
|
||||
}
|
||||
return withRetry(() => processor(file), opts);
|
||||
const result = await withRetry(() => processor(file), opts);
|
||||
completed++;
|
||||
onProgress?.(completed, files.length, file);
|
||||
return result;
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user