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,
|
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
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user