Skip to content

Commit 9da8107

Browse files
committed
feat(cli): comprehensive indexing UX improvements
- Add update plan display to 'dev update' (shows what will change before starting) - Add detailed progress with rates to all indexing phases (files/sec, docs/sec, commits/sec) - Extract progress formatting to reusable updateSectionWithRate() method - Fix NaN display when totalFiles is 0 (now shows 'Discovering...') - Improve 'dev map' hot paths display with tree branches and file icons - Add getUpdatePlan() public method to RepositoryIndexer All indexing commands now show consistent detailed progress: 1,234/4,567 files (27%, 45 files/sec) Instead of just: 27% complete
1 parent 5dcac3e commit 9da8107

7 files changed

Lines changed: 181 additions & 42 deletions

File tree

packages/cli/src/commands/git.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,11 @@ export const gitCommand = new Command('git')
9898
}
9999

100100
// Update embedding progress
101-
const pct = Math.round((progress.commitsProcessed / progress.totalCommits) * 100);
102-
progressRenderer.updateSection(
103-
`${progress.commitsProcessed}/${progress.totalCommits} commits (${pct}%)`
101+
progressRenderer.updateSectionWithRate(
102+
progress.commitsProcessed,
103+
progress.totalCommits,
104+
'commits',
105+
embeddingStartTime
104106
);
105107
}
106108
},

packages/cli/src/commands/github.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ Related:
133133
}
134134

135135
// Update embedding progress
136-
const pct = Math.round(
137-
(progress.documentsProcessed / progress.totalDocuments) * 100
138-
);
139-
progressRenderer.updateSection(
140-
`${progress.documentsProcessed}/${progress.totalDocuments} documents (${pct}%)`
136+
progressRenderer.updateSectionWithRate(
137+
progress.documentsProcessed,
138+
progress.totalDocuments,
139+
'documents',
140+
embeddingStartTime
141141
);
142142
}
143143
},

packages/cli/src/commands/index.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,17 +199,20 @@ export const indexCommand = new Command('index')
199199
}
200200

201201
// Update embedding progress
202-
const pct = Math.round((progress.documentsIndexed / progress.totalDocuments) * 100);
203-
const embeddingElapsed = (Date.now() - embeddingStartTime) / 1000;
204-
const docsPerSec =
205-
embeddingElapsed > 0 ? progress.documentsIndexed / embeddingElapsed : 0;
206-
progressRenderer.updateSection(
207-
`${progress.documentsIndexed.toLocaleString()}/${progress.totalDocuments.toLocaleString()} documents (${pct}%, ${docsPerSec.toFixed(0)} docs/sec)`
202+
progressRenderer.updateSectionWithRate(
203+
progress.documentsIndexed,
204+
progress.totalDocuments,
205+
'documents',
206+
embeddingStartTime
208207
);
209208
} else {
210209
// Scanning phase
211-
const percent = progress.percentComplete || 0;
212-
progressRenderer.updateSection(`${percent.toFixed(0)}% complete`);
210+
progressRenderer.updateSectionWithRate(
211+
progress.filesProcessed,
212+
progress.totalFiles,
213+
'files',
214+
scanStartTime
215+
);
213216
}
214217
},
215218
});
@@ -260,9 +263,11 @@ export const indexCommand = new Command('index')
260263
logger: indexLogger,
261264
onProgress: (progress) => {
262265
if (progress.phase === 'storing' && progress.totalCommits > 0) {
263-
const pct = Math.round((progress.commitsProcessed / progress.totalCommits) * 100);
264-
progressRenderer.updateSection(
265-
`${progress.commitsProcessed}/${progress.totalCommits} commits (${pct}%)`
266+
progressRenderer.updateSectionWithRate(
267+
progress.commitsProcessed,
268+
progress.totalCommits,
269+
'commits',
270+
gitStartTime
266271
);
267272
}
268273
},
@@ -280,6 +285,7 @@ export const indexCommand = new Command('index')
280285
let ghStats = { totalDocuments: 0, indexDuration: 0 };
281286
if (canIndexGitHub) {
282287
const ghStartTime = Date.now();
288+
let ghEmbeddingStartTime = 0;
283289
const ghVectorPath = `${filePaths.vectors}-github`;
284290
const ghIndexer = new GitHubIndexer({
285291
vectorStorePath: ghVectorPath,
@@ -295,9 +301,14 @@ export const indexCommand = new Command('index')
295301
if (progress.phase === 'fetching') {
296302
progressRenderer.updateSection('Fetching issues/PRs...');
297303
} else if (progress.phase === 'embedding') {
298-
const pct = Math.round((progress.documentsProcessed / progress.totalDocuments) * 100);
299-
progressRenderer.updateSection(
300-
`${progress.documentsProcessed}/${progress.totalDocuments} documents (${pct}%)`
304+
if (ghEmbeddingStartTime === 0) {
305+
ghEmbeddingStartTime = Date.now();
306+
}
307+
progressRenderer.updateSectionWithRate(
308+
progress.documentsProcessed,
309+
progress.totalDocuments,
310+
'documents',
311+
ghEmbeddingStartTime
301312
);
302313
}
303314
},

packages/cli/src/commands/update.ts

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,67 @@ export const updateCommand = new Command('update')
8383

8484
await indexer.initialize();
8585

86-
// Create logger for updating (verbose mode shows debug logs)
87-
const indexLogger = createIndexLogger(options.verbose);
86+
// Get update plan to show user what will be updated
87+
const updatePlan = await indexer.getUpdatePlan();
8888

89-
// Stop spinner and switch to section-based progress
89+
// Stop spinner
9090
spinner.stop();
9191

92+
if (!updatePlan || updatePlan.total === 0) {
93+
output.success('No changes detected');
94+
await indexer.close();
95+
metricsStore.close();
96+
return;
97+
}
98+
99+
// Show update plan
100+
console.log('');
101+
console.log(chalk.bold('Update plan:'));
102+
console.log('');
103+
104+
if (updatePlan.added.length > 0) {
105+
console.log(chalk.green(` ✓ ${updatePlan.added.length} new file(s)`));
106+
if (options.verbose) {
107+
for (const file of updatePlan.added.slice(0, 5)) {
108+
console.log(chalk.dim(` + ${file}`));
109+
}
110+
if (updatePlan.added.length > 5) {
111+
console.log(chalk.dim(` ... and ${updatePlan.added.length - 5} more`));
112+
}
113+
}
114+
}
115+
116+
if (updatePlan.changed.length > 0) {
117+
console.log(chalk.yellow(` ↻ ${updatePlan.changed.length} modified file(s)`));
118+
if (options.verbose) {
119+
for (const file of updatePlan.changed.slice(0, 5)) {
120+
console.log(chalk.dim(` ~ ${file}`));
121+
}
122+
if (updatePlan.changed.length > 5) {
123+
console.log(chalk.dim(` ... and ${updatePlan.changed.length - 5} more`));
124+
}
125+
}
126+
}
127+
128+
if (updatePlan.deleted.length > 0) {
129+
console.log(chalk.red(` ✗ ${updatePlan.deleted.length} deleted file(s)`));
130+
if (options.verbose) {
131+
for (const file of updatePlan.deleted.slice(0, 5)) {
132+
console.log(chalk.dim(` - ${file}`));
133+
}
134+
if (updatePlan.deleted.length > 5) {
135+
console.log(chalk.dim(` ... and ${updatePlan.deleted.length - 5} more`));
136+
}
137+
}
138+
}
139+
140+
console.log('');
141+
console.log(chalk.dim(`Total: ${updatePlan.total} file(s) to process`));
142+
console.log('');
143+
144+
// Create logger for updating (verbose mode shows debug logs)
145+
const indexLogger = createIndexLogger(options.verbose);
146+
92147
// Initialize progress renderer
93148
const progressRenderer = new ProgressRenderer({ verbose: options.verbose });
94149
progressRenderer.setSections(['Scanning Changed Files', 'Embedding Vectors']);
@@ -114,17 +169,20 @@ export const updateCommand = new Command('update')
114169
}
115170

116171
// Update embedding progress
117-
const pct = Math.round((progress.documentsIndexed / progress.totalDocuments) * 100);
118-
const embeddingElapsed = (Date.now() - embeddingStartTime) / 1000;
119-
const docsPerSec =
120-
embeddingElapsed > 0 ? progress.documentsIndexed / embeddingElapsed : 0;
121-
progressRenderer.updateSection(
122-
`${progress.documentsIndexed.toLocaleString()}/${progress.totalDocuments.toLocaleString()} documents (${pct}%, ${docsPerSec.toFixed(0)} docs/sec)`
172+
progressRenderer.updateSectionWithRate(
173+
progress.documentsIndexed,
174+
progress.totalDocuments,
175+
'documents',
176+
embeddingStartTime
123177
);
124178
} else {
125179
// Scanning phase
126-
const percent = progress.percentComplete || 0;
127-
progressRenderer.updateSection(`${percent.toFixed(0)}% complete`);
180+
progressRenderer.updateSectionWithRate(
181+
progress.filesProcessed,
182+
progress.totalFiles,
183+
'files',
184+
scanStartTime
185+
);
128186
}
129187
},
130188
});
@@ -155,13 +213,9 @@ export const updateCommand = new Command('update')
155213

156214
// Show completion message
157215
output.log('');
158-
if (stats.filesScanned === 0) {
159-
output.success('No changes detected');
160-
} else {
161-
output.success(
162-
`Updated ${stats.filesScanned.toLocaleString()} files in ${duration.toFixed(1)}s`
163-
);
164-
}
216+
output.success(
217+
`Updated ${stats.filesScanned.toLocaleString()} files in ${duration.toFixed(1)}s`
218+
);
165219
output.log('');
166220

167221
// Show errors if any

packages/cli/src/utils/progress.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,23 @@ export class ProgressRenderer {
5656
}
5757
}
5858

59+
/**
60+
* Update section with formatted progress including rate
61+
*/
62+
updateSectionWithRate(processed: number, total: number, unit: string, startTime: number): void {
63+
if (total === 0) {
64+
this.updateSection('Discovering...');
65+
return;
66+
}
67+
68+
const pct = Math.round((processed / total) * 100);
69+
const elapsed = (Date.now() - startTime) / 1000;
70+
const rate = elapsed > 0 ? processed / elapsed : 0;
71+
this.updateSection(
72+
`${processed.toLocaleString()}/${total.toLocaleString()} ${unit} (${pct}%, ${rate.toFixed(0)} ${unit}/sec)`
73+
);
74+
}
75+
5976
/**
6077
* Mark current section as complete and move to next
6178
*/

packages/core/src/indexer/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,29 @@ export class RepositoryIndexer {
591591
return validation.data;
592592
}
593593

594+
/**
595+
* Get update plan showing which files will be processed
596+
* Useful for displaying a plan before running update
597+
*/
598+
async getUpdatePlan(options: { since?: Date } = {}): Promise<{
599+
changed: string[];
600+
added: string[];
601+
deleted: string[];
602+
total: number;
603+
} | null> {
604+
if (!this.state) {
605+
return null;
606+
}
607+
608+
const { changed, added, deleted } = await this.detectChangedFiles(options.since);
609+
return {
610+
changed,
611+
added,
612+
deleted,
613+
total: changed.length + added.length + deleted.length,
614+
};
615+
}
616+
594617
/**
595618
* Enrich language stats with change frequency data
596619
* Non-blocking: returns original stats if git analysis fails

packages/core/src/map/index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,26 @@ function computeHotPaths(docs: SearchResult[], maxPaths: number): HotPath[] {
486486
return sorted;
487487
}
488488

489+
/**
490+
* Get file icon based on extension
491+
*/
492+
function getFileIcon(ext: string): string {
493+
const iconMap: Record<string, string> = {
494+
ts: '📘',
495+
tsx: '⚛️',
496+
js: '📜',
497+
jsx: '⚛️',
498+
go: '🐹',
499+
py: '🐍',
500+
rs: '🦀',
501+
md: '📝',
502+
json: '📋',
503+
yaml: '⚙️',
504+
yml: '⚙️',
505+
};
506+
return iconMap[ext] || '📄';
507+
}
508+
489509
/**
490510
* Format codebase map as readable text
491511
*/
@@ -501,8 +521,20 @@ export function formatCodebaseMap(map: CodebaseMap, options: MapOptions = {}): s
501521
lines.push('## Hot Paths (most referenced)');
502522
for (let i = 0; i < map.hotPaths.length; i++) {
503523
const hp = map.hotPaths[i];
504-
const component = hp.primaryComponent ? ` (${hp.primaryComponent})` : '';
505-
lines.push(`${i + 1}. \`${hp.file}\`${component} - ${hp.incomingRefs} refs`);
524+
const isLast = i === map.hotPaths.length - 1;
525+
const prefix = isLast ? '└─' : '├─';
526+
527+
// Get file extension for icon
528+
const ext = hp.file.split('.').pop() || '';
529+
const icon = getFileIcon(ext);
530+
531+
// Extract just the filename for cleaner display
532+
const fileName = hp.file.split('/').pop() || hp.file;
533+
const dirPath = hp.file.substring(0, hp.file.lastIndexOf('/'));
534+
535+
const component = hp.primaryComponent ? ` • ${hp.primaryComponent}` : '';
536+
lines.push(` ${prefix} ${icon} **${fileName}**${component}${hp.incomingRefs} refs`);
537+
lines.push(` ${dirPath}`);
506538
}
507539
lines.push('');
508540
}

0 commit comments

Comments
 (0)