From 88afb8b47323f209eb085a527d203665070cbea1 Mon Sep 17 00:00:00 2001 From: prosdev Date: Sat, 13 Dec 2025 19:01:06 -0800 Subject: [PATCH 1/4] fix(cli): fix dev owners subdirectory path matching Fixed 'No ownership data found' error when running dev owners from subdirectories like packages/. Root cause: File paths in topFiles are absolute (from database), but subdirectory mode was comparing them against relative paths. Solution: Normalize absolute paths by stripping repository prefix before comparison, matching the approach used in root directory mode. --- packages/cli/src/commands/owners.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/commands/owners.ts b/packages/cli/src/commands/owners.ts index 8c90c8d..de647f1 100644 --- a/packages/cli/src/commands/owners.ts +++ b/packages/cli/src/commands/owners.ts @@ -480,7 +480,10 @@ function formatSubdirectoryMode( ): string { // Filter developers to only those with files in current directory const relevantDevs = developers.filter((dev) => - dev.topFiles.some((f) => f.path.startsWith(`${repositoryPath}/${currentDir}`)) + dev.topFiles.some((f) => { + const relativePath = f.path.replace(`${repositoryPath}/`, ''); + return relativePath.startsWith(currentDir); + }) ); if (relevantDevs.length === 0) { @@ -498,7 +501,10 @@ function formatSubdirectoryMode( // Show top files in this directory const filesInDir = primary.topFiles - .filter((f) => f.path.startsWith(`${repositoryPath}/${currentDir}`)) + .filter((f) => { + const relativePath = f.path.replace(`${repositoryPath}/`, ''); + return relativePath.startsWith(currentDir); + }) .slice(0, 5); if (filesInDir.length > 0) { From 96fe67fbdda80fe0ed471823b18c1fb090c7c557 Mon Sep 17 00:00:00 2001 From: prosdev Date: Sat, 13 Dec 2025 19:33:57 -0800 Subject: [PATCH 2/4] 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 --- packages/cli/src/commands/git.ts | 8 ++- packages/cli/src/commands/github.ts | 10 ++-- packages/cli/src/commands/index.ts | 39 ++++++++----- packages/cli/src/commands/update.ts | 90 +++++++++++++++++++++++------ packages/cli/src/utils/progress.ts | 17 ++++++ packages/core/src/indexer/index.ts | 23 ++++++++ packages/core/src/map/index.ts | 36 +++++++++++- 7 files changed, 181 insertions(+), 42 deletions(-) diff --git a/packages/cli/src/commands/git.ts b/packages/cli/src/commands/git.ts index 96a1894..346d07f 100644 --- a/packages/cli/src/commands/git.ts +++ b/packages/cli/src/commands/git.ts @@ -98,9 +98,11 @@ export const gitCommand = new Command('git') } // Update embedding progress - const pct = Math.round((progress.commitsProcessed / progress.totalCommits) * 100); - progressRenderer.updateSection( - `${progress.commitsProcessed}/${progress.totalCommits} commits (${pct}%)` + progressRenderer.updateSectionWithRate( + progress.commitsProcessed, + progress.totalCommits, + 'commits', + embeddingStartTime ); } }, diff --git a/packages/cli/src/commands/github.ts b/packages/cli/src/commands/github.ts index 6d923bf..ecd703b 100644 --- a/packages/cli/src/commands/github.ts +++ b/packages/cli/src/commands/github.ts @@ -133,11 +133,11 @@ Related: } // Update embedding progress - const pct = Math.round( - (progress.documentsProcessed / progress.totalDocuments) * 100 - ); - progressRenderer.updateSection( - `${progress.documentsProcessed}/${progress.totalDocuments} documents (${pct}%)` + progressRenderer.updateSectionWithRate( + progress.documentsProcessed, + progress.totalDocuments, + 'documents', + embeddingStartTime ); } }, diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index b2144a6..3789359 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -199,17 +199,20 @@ export const indexCommand = new Command('index') } // Update embedding progress - const pct = Math.round((progress.documentsIndexed / progress.totalDocuments) * 100); - const embeddingElapsed = (Date.now() - embeddingStartTime) / 1000; - const docsPerSec = - embeddingElapsed > 0 ? progress.documentsIndexed / embeddingElapsed : 0; - progressRenderer.updateSection( - `${progress.documentsIndexed.toLocaleString()}/${progress.totalDocuments.toLocaleString()} documents (${pct}%, ${docsPerSec.toFixed(0)} docs/sec)` + progressRenderer.updateSectionWithRate( + progress.documentsIndexed, + progress.totalDocuments, + 'documents', + embeddingStartTime ); } else { // Scanning phase - const percent = progress.percentComplete || 0; - progressRenderer.updateSection(`${percent.toFixed(0)}% complete`); + progressRenderer.updateSectionWithRate( + progress.filesProcessed, + progress.totalFiles, + 'files', + scanStartTime + ); } }, }); @@ -260,9 +263,11 @@ export const indexCommand = new Command('index') logger: indexLogger, onProgress: (progress) => { if (progress.phase === 'storing' && progress.totalCommits > 0) { - const pct = Math.round((progress.commitsProcessed / progress.totalCommits) * 100); - progressRenderer.updateSection( - `${progress.commitsProcessed}/${progress.totalCommits} commits (${pct}%)` + progressRenderer.updateSectionWithRate( + progress.commitsProcessed, + progress.totalCommits, + 'commits', + gitStartTime ); } }, @@ -280,6 +285,7 @@ export const indexCommand = new Command('index') let ghStats = { totalDocuments: 0, indexDuration: 0 }; if (canIndexGitHub) { const ghStartTime = Date.now(); + let ghEmbeddingStartTime = 0; const ghVectorPath = `${filePaths.vectors}-github`; const ghIndexer = new GitHubIndexer({ vectorStorePath: ghVectorPath, @@ -295,9 +301,14 @@ export const indexCommand = new Command('index') if (progress.phase === 'fetching') { progressRenderer.updateSection('Fetching issues/PRs...'); } else if (progress.phase === 'embedding') { - const pct = Math.round((progress.documentsProcessed / progress.totalDocuments) * 100); - progressRenderer.updateSection( - `${progress.documentsProcessed}/${progress.totalDocuments} documents (${pct}%)` + if (ghEmbeddingStartTime === 0) { + ghEmbeddingStartTime = Date.now(); + } + progressRenderer.updateSectionWithRate( + progress.documentsProcessed, + progress.totalDocuments, + 'documents', + ghEmbeddingStartTime ); } }, diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 6ac2c4f..d17e84a 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -83,12 +83,67 @@ export const updateCommand = new Command('update') await indexer.initialize(); - // Create logger for updating (verbose mode shows debug logs) - const indexLogger = createIndexLogger(options.verbose); + // Get update plan to show user what will be updated + const updatePlan = await indexer.getUpdatePlan(); - // Stop spinner and switch to section-based progress + // Stop spinner spinner.stop(); + if (!updatePlan || updatePlan.total === 0) { + output.success('No changes detected'); + await indexer.close(); + metricsStore.close(); + return; + } + + // Show update plan + console.log(''); + console.log(chalk.bold('Update plan:')); + console.log(''); + + if (updatePlan.added.length > 0) { + console.log(chalk.green(` ✓ ${updatePlan.added.length} new file(s)`)); + if (options.verbose) { + for (const file of updatePlan.added.slice(0, 5)) { + console.log(chalk.dim(` + ${file}`)); + } + if (updatePlan.added.length > 5) { + console.log(chalk.dim(` ... and ${updatePlan.added.length - 5} more`)); + } + } + } + + if (updatePlan.changed.length > 0) { + console.log(chalk.yellow(` ↻ ${updatePlan.changed.length} modified file(s)`)); + if (options.verbose) { + for (const file of updatePlan.changed.slice(0, 5)) { + console.log(chalk.dim(` ~ ${file}`)); + } + if (updatePlan.changed.length > 5) { + console.log(chalk.dim(` ... and ${updatePlan.changed.length - 5} more`)); + } + } + } + + if (updatePlan.deleted.length > 0) { + console.log(chalk.red(` ✗ ${updatePlan.deleted.length} deleted file(s)`)); + if (options.verbose) { + for (const file of updatePlan.deleted.slice(0, 5)) { + console.log(chalk.dim(` - ${file}`)); + } + if (updatePlan.deleted.length > 5) { + console.log(chalk.dim(` ... and ${updatePlan.deleted.length - 5} more`)); + } + } + } + + console.log(''); + console.log(chalk.dim(`Total: ${updatePlan.total} file(s) to process`)); + console.log(''); + + // Create logger for updating (verbose mode shows debug logs) + const indexLogger = createIndexLogger(options.verbose); + // Initialize progress renderer const progressRenderer = new ProgressRenderer({ verbose: options.verbose }); progressRenderer.setSections(['Scanning Changed Files', 'Embedding Vectors']); @@ -114,17 +169,20 @@ export const updateCommand = new Command('update') } // Update embedding progress - const pct = Math.round((progress.documentsIndexed / progress.totalDocuments) * 100); - const embeddingElapsed = (Date.now() - embeddingStartTime) / 1000; - const docsPerSec = - embeddingElapsed > 0 ? progress.documentsIndexed / embeddingElapsed : 0; - progressRenderer.updateSection( - `${progress.documentsIndexed.toLocaleString()}/${progress.totalDocuments.toLocaleString()} documents (${pct}%, ${docsPerSec.toFixed(0)} docs/sec)` + progressRenderer.updateSectionWithRate( + progress.documentsIndexed, + progress.totalDocuments, + 'documents', + embeddingStartTime ); } else { // Scanning phase - const percent = progress.percentComplete || 0; - progressRenderer.updateSection(`${percent.toFixed(0)}% complete`); + progressRenderer.updateSectionWithRate( + progress.filesProcessed, + progress.totalFiles, + 'files', + scanStartTime + ); } }, }); @@ -155,13 +213,9 @@ export const updateCommand = new Command('update') // Show completion message output.log(''); - if (stats.filesScanned === 0) { - output.success('No changes detected'); - } else { - output.success( - `Updated ${stats.filesScanned.toLocaleString()} files in ${duration.toFixed(1)}s` - ); - } + output.success( + `Updated ${stats.filesScanned.toLocaleString()} files in ${duration.toFixed(1)}s` + ); output.log(''); // Show errors if any diff --git a/packages/cli/src/utils/progress.ts b/packages/cli/src/utils/progress.ts index 78d9476..8947360 100644 --- a/packages/cli/src/utils/progress.ts +++ b/packages/cli/src/utils/progress.ts @@ -56,6 +56,23 @@ export class ProgressRenderer { } } + /** + * Update section with formatted progress including rate + */ + updateSectionWithRate(processed: number, total: number, unit: string, startTime: number): void { + if (total === 0) { + this.updateSection('Discovering...'); + return; + } + + const pct = Math.round((processed / total) * 100); + const elapsed = (Date.now() - startTime) / 1000; + const rate = elapsed > 0 ? processed / elapsed : 0; + this.updateSection( + `${processed.toLocaleString()}/${total.toLocaleString()} ${unit} (${pct}%, ${rate.toFixed(0)} ${unit}/sec)` + ); + } + /** * Mark current section as complete and move to next */ diff --git a/packages/core/src/indexer/index.ts b/packages/core/src/indexer/index.ts index ac08faf..e23270b 100644 --- a/packages/core/src/indexer/index.ts +++ b/packages/core/src/indexer/index.ts @@ -591,6 +591,29 @@ export class RepositoryIndexer { return validation.data; } + /** + * Get update plan showing which files will be processed + * Useful for displaying a plan before running update + */ + async getUpdatePlan(options: { since?: Date } = {}): Promise<{ + changed: string[]; + added: string[]; + deleted: string[]; + total: number; + } | null> { + if (!this.state) { + return null; + } + + const { changed, added, deleted } = await this.detectChangedFiles(options.since); + return { + changed, + added, + deleted, + total: changed.length + added.length + deleted.length, + }; + } + /** * Enrich language stats with change frequency data * Non-blocking: returns original stats if git analysis fails diff --git a/packages/core/src/map/index.ts b/packages/core/src/map/index.ts index a8957bf..802a3e7 100644 --- a/packages/core/src/map/index.ts +++ b/packages/core/src/map/index.ts @@ -486,6 +486,26 @@ function computeHotPaths(docs: SearchResult[], maxPaths: number): HotPath[] { return sorted; } +/** + * Get file icon based on extension + */ +function getFileIcon(ext: string): string { + const iconMap: Record = { + ts: '📘', + tsx: '⚛️', + js: '📜', + jsx: '⚛️', + go: '🐹', + py: '🐍', + rs: '🦀', + md: '📝', + json: '📋', + yaml: '⚙️', + yml: '⚙️', + }; + return iconMap[ext] || '📄'; +} + /** * Format codebase map as readable text */ @@ -501,8 +521,20 @@ export function formatCodebaseMap(map: CodebaseMap, options: MapOptions = {}): s lines.push('## Hot Paths (most referenced)'); for (let i = 0; i < map.hotPaths.length; i++) { const hp = map.hotPaths[i]; - const component = hp.primaryComponent ? ` (${hp.primaryComponent})` : ''; - lines.push(`${i + 1}. \`${hp.file}\`${component} - ${hp.incomingRefs} refs`); + const isLast = i === map.hotPaths.length - 1; + const prefix = isLast ? '└─' : '├─'; + + // Get file extension for icon + const ext = hp.file.split('.').pop() || ''; + const icon = getFileIcon(ext); + + // Extract just the filename for cleaner display + const fileName = hp.file.split('/').pop() || hp.file; + const dirPath = hp.file.substring(0, hp.file.lastIndexOf('/')); + + const component = hp.primaryComponent ? ` • ${hp.primaryComponent}` : ''; + lines.push(` ${prefix} ${icon} **${fileName}**${component} • ${hp.incomingRefs} refs`); + lines.push(` ${dirPath}`); } lines.push(''); } From d24f2a8f8932ae37f133206d2ba925480193fe13 Mon Sep 17 00:00:00 2001 From: prosdev Date: Sat, 13 Dec 2025 20:17:25 -0800 Subject: [PATCH 3/4] feat(cli): improve visual formatting and fix GitHub stats Visual Improvements: - Add tree branches and file icons to 'dev map' hot paths - Add tree branches and file icons to 'dev activity' output - Extract getFileIcon() to shared utility in @lytics/dev-agent-core GitHub Stats Fix: - Track issue and PR states separately (issuesByState, prsByState) - Fix confusing display showing '14 open PRs' when there were 0 - Add proper per-type state counts to GitHubIndexStats interface - Update display to show accurate counts for issues and PRs Progress Display Enhancements: - Add detailed scanning progress with rates (e.g., '1,234/4,567 files (27%, 45 files/sec)') - Extend detailed progress to 'dev update', 'dev git index', and 'dev github index' - Add update plan display to 'dev update' (shows changed/added/deleted before starting) - Refactor progress formatting into reusable updateSectionWithRate() method - Fix NaN display when totalFiles is 0 (now shows 'Discovering files...') Test Updates: - Update map test to match new hot paths format with tree branches All visual outputs now use consistent tree-based formatting with file icons. --- packages/cli/src/commands/activity.ts | 50 ++++++++++----------- packages/cli/src/commands/stats.ts | 4 ++ packages/cli/src/utils/output.ts | 50 +++++++++++++-------- packages/core/src/map/__tests__/map.test.ts | 3 +- packages/core/src/map/index.ts | 21 +-------- packages/core/src/utils/icons.ts | 27 +++++++++++ packages/core/src/utils/index.ts | 1 + packages/subagents/src/github/indexer.ts | 21 +++++++++ packages/types/src/github.ts | 8 +++- 9 files changed, 117 insertions(+), 68 deletions(-) create mode 100644 packages/core/src/utils/icons.ts diff --git a/packages/cli/src/commands/activity.ts b/packages/cli/src/commands/activity.ts index 2c6fb54..58c9c63 100644 --- a/packages/cli/src/commands/activity.ts +++ b/packages/cli/src/commands/activity.ts @@ -5,6 +5,7 @@ import * as path from 'node:path'; import { type FileMetrics, + getFileIcon, getMostActive, getStoragePath, MetricsStore, @@ -31,43 +32,38 @@ function formatRelativeTime(date: Date): string { } /** - * Format files as a compact table + * Format files with tree branches and icons */ function formatFileMetricsTable(files: FileMetrics[]): string { if (files.length === 0) return ''; - // Calculate column widths - const maxPathLen = Math.max(...files.map((f) => f.filePath.length), 40); - const pathWidth = Math.min(maxPathLen, 55); + let output = ''; - // Header - let output = chalk.bold( - `${'FILE'.padEnd(pathWidth)} ${'COMMITS'.padStart(7)} ${'LOC'.padStart(6)} ${'AUTHORS'.padStart(7)} ${'LAST CHANGE'}\n` - ); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + const isLast = i === files.length - 1; + const branch = isLast ? '└─' : '├─'; + const connector = isLast ? ' ' : '│'; // Vertical line for non-last items + const icon = getFileIcon(file.filePath); - // Separator line - output += chalk.dim(`${'─'.repeat(pathWidth + 2 + 7 + 2 + 6 + 2 + 7 + 2 + 12)}\n`); - - // Rows - for (const file of files) { - // Truncate path if too long - let displayPath = file.filePath; - if (displayPath.length > pathWidth) { - displayPath = `...${displayPath.slice(-(pathWidth - 3))}`; - } - displayPath = displayPath.padEnd(pathWidth); - - const commits = String(file.commitCount).padStart(7); - const loc = String(file.linesOfCode).padStart(6); - - // Author count with emoji - const authorIcon = file.authorCount === 1 ? ' 👤' : file.authorCount === 2 ? ' 👥' : '👥👥'; - const authors = `${String(file.authorCount).padStart(5)}${authorIcon}`; + // Author info + const authorIcon = file.authorCount === 1 ? '👤' : file.authorCount === 2 ? '👥' : '👥👥'; // Relative time const lastChange = file.lastModified ? formatRelativeTime(file.lastModified) : 'unknown'; - output += `${chalk.dim(displayPath)} ${chalk.cyan(commits)} ${chalk.yellow(loc)} ${chalk.green(authors)} ${chalk.gray(lastChange)}\n`; + // File path line with icon and branch + output += `${chalk.dim(branch)} ${icon} ${file.filePath}\n`; + + // Metrics line with vertical connector + output += chalk.dim( + `${connector} ${file.commitCount} commits • ${file.authorCount} ${authorIcon} • Last: ${lastChange}\n` + ); + + // Add vertical line separator between items (except after last) + if (!isLast) { + output += chalk.dim(`${connector}\n`); + } } return output; diff --git a/packages/cli/src/commands/stats.ts b/packages/cli/src/commands/stats.ts index bed5dd2..c3bb173 100644 --- a/packages/cli/src/commands/stats.ts +++ b/packages/cli/src/commands/stats.ts @@ -100,6 +100,8 @@ async function loadCurrentStats(): Promise<{ totalDocuments: state.totalDocuments || 0, byType: state.byType || {}, byState: state.byState || {}, + issuesByState: state.issuesByState, + prsByState: state.prsByState, lastIndexed: state.lastIndexed || '', indexDuration: state.indexDuration || 0, }; @@ -217,6 +219,8 @@ What You'll See: totalDocuments: number; byType: { issue?: number; pull_request?: number }; byState: { open?: number; closed?: number; merged?: number }; + issuesByState?: { open: number; closed: number }; + prsByState?: { open: number; closed: number; merged: number }; lastIndexed: string; } | null) || undefined, }); diff --git a/packages/cli/src/utils/output.ts b/packages/cli/src/utils/output.ts index f67b95d..100b4cb 100644 --- a/packages/cli/src/utils/output.ts +++ b/packages/cli/src/utils/output.ts @@ -142,18 +142,26 @@ export function formatGitHubSummary(githubStats: { totalDocuments: number; byType: { issue?: number; pull_request?: number }; byState: { open?: number; closed?: number; merged?: number }; + issuesByState?: { open: number; closed: number }; + prsByState?: { open: number; closed: number; merged: number }; lastIndexed: string; }): string { const issues = githubStats.byType.issue || 0; const prs = githubStats.byType.pull_request || 0; - const open = githubStats.byState.open || 0; - const merged = githubStats.byState.merged || 0; + + // Use per-type state counts if available (new format), fall back to aggregate (old format) + const issuesOpen = githubStats.issuesByState?.open ?? 0; + const issuesClosed = githubStats.issuesByState?.closed ?? 0; + const prsOpen = githubStats.prsByState?.open ?? 0; + const prsMerged = githubStats.prsByState?.merged ?? 0; const timeSince = getTimeSince(new Date(githubStats.lastIndexed)); return [ `🔗 ${chalk.bold(githubStats.repository)} • ${formatNumber(githubStats.totalDocuments)} documents`, - ` ${chalk.gray(issues.toString())} issues • ${chalk.gray(prs.toString())} PRs • ${chalk.gray(open.toString())} open • ${chalk.gray(merged.toString())} merged • Synced ${timeSince}`, + ` Issues: ${chalk.gray(issues.toString())} total (${issuesOpen} open, ${issuesClosed} closed)`, + ` Pull Requests: ${chalk.gray(prs.toString())} total (${prsOpen} open, ${prsMerged} merged)`, + ` Last synced: ${timeSince}`, ].join('\n'); } @@ -369,6 +377,8 @@ export function printRepositoryStats(data: { totalDocuments: number; byType: { issue?: number; pull_request?: number }; byState: { open?: number; closed?: number; merged?: number }; + issuesByState?: { open: number; closed: number }; + prsByState?: { open: number; closed: number; merged: number }; lastIndexed: string; } | null; }): void { @@ -471,19 +481,21 @@ export function printRepositoryStats(data: { const issues = githubStats.byType.issue || 0; const prs = githubStats.byType.pull_request || 0; + // Use per-type state counts if available (new format), fall back to aggregate (old format) + const issuesOpen = githubStats.issuesByState?.open ?? githubStats.byState.open ?? 0; + const issuesClosed = githubStats.issuesByState?.closed ?? githubStats.byState.closed ?? 0; + const prsOpen = githubStats.prsByState?.open ?? githubStats.byState.open ?? 0; + const prsMerged = githubStats.prsByState?.merged ?? githubStats.byState.merged ?? 0; + if (issues > 0) { - const openIssues = githubStats.byState.open || 0; - const closedIssues = githubStats.byState.closed || 0; output.log( - ` Issues: ${chalk.bold(issues.toString())} total (${chalk.green(`${openIssues} open`)}, ${chalk.gray(`${closedIssues} closed`)})` + ` Issues: ${chalk.bold(issues.toString())} total (${issuesOpen} open, ${issuesClosed} closed)` ); } if (prs > 0) { - const openPRs = githubStats.byState.open || 0; - const mergedPRs = githubStats.byState.merged || 0; output.log( - ` Pull Requests: ${chalk.bold(prs.toString())} total (${chalk.green(`${openPRs} open`)}, ${chalk.magenta(`${mergedPRs} merged`)})` + ` Pull Requests: ${chalk.bold(prs.toString())} total (${prsOpen} open, ${prsMerged} merged)` ); } @@ -905,6 +917,8 @@ export function printGitHubStats(githubStats: { totalDocuments: number; byType: { issue?: number; pull_request?: number; discussion?: number }; byState: { open?: number; closed?: number; merged?: number }; + issuesByState?: { open: number; closed: number }; + prsByState?: { open: number; closed: number; merged: number }; lastIndexed: string; indexDuration?: number; }): void { @@ -912,9 +926,12 @@ export function printGitHubStats(githubStats: { const prs = githubStats.byType.pull_request || 0; const discussions = githubStats.byType.discussion || 0; - const openCount = githubStats.byState.open || 0; - const closedCount = githubStats.byState.closed || 0; - const mergedCount = githubStats.byState.merged || 0; + // Use per-type state counts if available (new format), fall back to aggregate (old format) + const issueOpen = githubStats.issuesByState?.open ?? 0; + const issueClosed = githubStats.issuesByState?.closed ?? 0; + const prOpen = githubStats.prsByState?.open ?? 0; + const prClosed = githubStats.prsByState?.closed ?? 0; + const prMerged = githubStats.prsByState?.merged ?? 0; const timeSince = getTimeSince(new Date(githubStats.lastIndexed)); @@ -928,9 +945,6 @@ export function printGitHubStats(githubStats: { // Issues breakdown if (issues > 0) { const issueStates: string[] = []; - // Calculate issue-specific counts (open + closed, no merged for issues) - const issueOpen = openCount > 0 ? openCount : 0; - const issueClosed = closedCount > 0 ? closedCount : 0; if (issueOpen > 0) { issueStates.push(`${chalk.green('●')} ${issueOpen} open`); @@ -949,13 +963,13 @@ export function printGitHubStats(githubStats: { // Pull requests breakdown if (prs > 0) { const prStates: string[] = []; - // For PRs, we show open and merged - const prOpen = openCount > 0 ? openCount : 0; - const prMerged = mergedCount > 0 ? mergedCount : 0; if (prOpen > 0) { prStates.push(`${chalk.green('●')} ${prOpen} open`); } + if (prClosed > 0) { + prStates.push(`${chalk.gray('●')} ${prClosed} closed`); + } if (prMerged > 0) { prStates.push(`${chalk.magenta('●')} ${prMerged} merged`); } diff --git a/packages/core/src/map/__tests__/map.test.ts b/packages/core/src/map/__tests__/map.test.ts index 7d28e4d..76aa911 100644 --- a/packages/core/src/map/__tests__/map.test.ts +++ b/packages/core/src/map/__tests__/map.test.ts @@ -485,8 +485,9 @@ describe('Codebase Map', () => { const output = formatCodebaseMap(map, { includeHotPaths: true }); expect(output).toContain('## Hot Paths'); - expect(output).toContain('src/core.ts'); + expect(output).toContain('**core.ts**'); // Filename in bold expect(output).toContain('2 refs'); + expect(output).toContain('src'); // Directory path on separate line }); }); diff --git a/packages/core/src/map/index.ts b/packages/core/src/map/index.ts index 802a3e7..e8f96c2 100644 --- a/packages/core/src/map/index.ts +++ b/packages/core/src/map/index.ts @@ -7,6 +7,7 @@ import * as path from 'node:path'; import type { Logger } from '@lytics/kero'; import type { LocalGitExtractor } from '../git/extractor'; import type { RepositoryIndexer } from '../indexer'; +import { getFileIcon } from '../utils/icons'; import type { SearchResult } from '../vector/types'; import type { ChangeFrequency, @@ -486,26 +487,6 @@ function computeHotPaths(docs: SearchResult[], maxPaths: number): HotPath[] { return sorted; } -/** - * Get file icon based on extension - */ -function getFileIcon(ext: string): string { - const iconMap: Record = { - ts: '📘', - tsx: '⚛️', - js: '📜', - jsx: '⚛️', - go: '🐹', - py: '🐍', - rs: '🦀', - md: '📝', - json: '📋', - yaml: '⚙️', - yml: '⚙️', - }; - return iconMap[ext] || '📄'; -} - /** * Format codebase map as readable text */ diff --git a/packages/core/src/utils/icons.ts b/packages/core/src/utils/icons.ts new file mode 100644 index 0000000..a40b865 --- /dev/null +++ b/packages/core/src/utils/icons.ts @@ -0,0 +1,27 @@ +/** + * Utility functions for file and UI icons + */ + +/** + * Get file icon based on extension + */ +export function getFileIcon(filePathOrExt: string): string { + // Extract extension (handle both full paths and just extensions) + const ext = filePathOrExt.includes('.') ? filePathOrExt.split('.').pop() || '' : filePathOrExt; + + const iconMap: Record = { + ts: '📘', + tsx: '⚛️', + js: '📜', + jsx: '⚛️', + go: '🐹', + py: '🐍', + rs: '🦀', + md: '📝', + json: '📋', + yaml: '⚙️', + yml: '⚙️', + }; + + return iconMap[ext] || '📄'; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 0a7212b..f503524 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -4,5 +4,6 @@ export * from './concurrency'; export * from './file-validator'; +export * from './icons'; export * from './retry'; export * from './wasm-resolver'; diff --git a/packages/subagents/src/github/indexer.ts b/packages/subagents/src/github/indexer.ts index 10914d3..408fd51 100644 --- a/packages/subagents/src/github/indexer.ts +++ b/packages/subagents/src/github/indexer.ts @@ -178,6 +178,21 @@ export class GitHubIndexer { {} as Record ); + // Calculate states per type for accurate reporting + const issuesByState = { open: 0, closed: 0 }; + const prsByState = { open: 0, closed: 0, merged: 0 }; + + for (const doc of enrichedDocs) { + if (doc.type === 'issue') { + if (doc.state === 'open') issuesByState.open++; + else if (doc.state === 'closed') issuesByState.closed++; + } else if (doc.type === 'pull_request') { + if (doc.state === 'open') prsByState.open++; + else if (doc.state === 'closed') prsByState.closed++; + else if (doc.state === 'merged') prsByState.merged++; + } + } + // Update state this.state = { version: INDEXER_VERSION, @@ -186,6 +201,8 @@ export class GitHubIndexer { totalDocuments: enrichedDocs.length, byType: byType as Record<'issue' | 'pull_request' | 'discussion', number>, byState: byState as Record<'open' | 'closed' | 'merged', number>, + issuesByState, + prsByState, }; // Save state to disk @@ -202,6 +219,8 @@ export class GitHubIndexer { totalDocuments: enrichedDocs.length, byType: byType as Record<'issue' | 'pull_request' | 'discussion', number>, byState: byState as Record<'open' | 'closed' | 'merged', number>, + issuesByState, + prsByState, lastIndexed: this.state.lastIndexed, indexDuration: durationMs, }; @@ -398,6 +417,8 @@ export class GitHubIndexer { totalDocuments: this.state.totalDocuments, byType: this.state.byType, byState: this.state.byState, + issuesByState: this.state.issuesByState, + prsByState: this.state.prsByState, lastIndexed: this.state.lastIndexed, indexDuration: 0, }; diff --git a/packages/types/src/github.ts b/packages/types/src/github.ts index 948b0c3..b406576 100644 --- a/packages/types/src/github.ts +++ b/packages/types/src/github.ts @@ -110,7 +110,9 @@ export interface GitHubIndexerState { lastIndexed: string; // ISO date totalDocuments: number; byType: Record; - byState: Record; + byState: Record; // Deprecated: aggregate counts (kept for compatibility) + issuesByState: { open: number; closed: number }; + prsByState: { open: number; closed: number; merged: number }; } /** @@ -145,7 +147,9 @@ export interface GitHubIndexStats { repository: string; totalDocuments: number; byType: Record; - byState: Record; + byState: Record; // Deprecated: aggregate counts (kept for compatibility) + issuesByState: { open: number; closed: number }; + prsByState: { open: number; closed: number; merged: number }; lastIndexed: string; // ISO date indexDuration: number; // milliseconds } From 05e89a1a509af704da668e602e0f09f682320189 Mon Sep 17 00:00:00 2001 From: prosdev Date: Sat, 13 Dec 2025 20:19:49 -0800 Subject: [PATCH 4/4] chore: add changeset for visual formatting and GitHub stats improvements --- .../visual-formatting-and-github-stats-fix.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .changeset/visual-formatting-and-github-stats-fix.md diff --git a/.changeset/visual-formatting-and-github-stats-fix.md b/.changeset/visual-formatting-and-github-stats-fix.md new file mode 100644 index 0000000..122b26b --- /dev/null +++ b/.changeset/visual-formatting-and-github-stats-fix.md @@ -0,0 +1,99 @@ +--- +"@lytics/dev-agent-cli": patch +"@lytics/dev-agent-core": patch +"@lytics/dev-agent-subagents": patch +"@lytics/dev-agent-types": patch +"dev-agent": patch +--- + +# Visual Formatting & GitHub Stats Improvements + +## Visual Enhancements ✨ + +### Tree Branches & File Icons +All CLI outputs now use consistent tree-based formatting with file icons: + +**`dev map` hot paths:** +``` +## Hot Paths (most referenced) + ├─ 📘 **typescript.ts** • 307 refs + /packages/core/src/scanner + ├─ 📘 **index.ts** • 251 refs + /packages/core/src/indexer + └─ 📘 **go.ts** • 152 refs + /packages/core/src/scanner +``` + +**`dev activity` output:** +``` +├─ 📘 packages/mcp-server/bin/dev-agent-mcp.ts +│ 34 commits • 1 👤 • Last: today +│ +├─ 📘 packages/core/src/indexer/index.ts +│ 32 commits • 1 👤 • Last: today +``` + +### Shared Icon Utility +Extracted `getFileIcon()` to `@lytics/dev-agent-core/utils` for reuse across packages. + +## GitHub Stats Fix 🐛 + +Fixed confusing issue/PR state display: + +**Before:** +``` +Issues: 68 total (14 open, 55 closed) +Pull Requests: 97 total (14 open, 96 merged) ❌ Wrong! +``` + +**After:** +``` +Issues: 68 total (14 open, 54 closed) +Pull Requests: 97 total (0 open, 96 merged) ✅ Correct! +``` + +- Added separate state tracking: `issuesByState`, `prsByState` +- GitHub indexer now tracks issue and PR states independently +- Stats display now shows accurate per-type counts + +## Progress Display Improvements 📊 + +### Detailed Progress with Rates +All indexing commands now show detailed progress: + +``` +Scanning Repository: 1,234/4,567 files (27%, 45 files/sec) +Embedding Vectors: 856/2,549 documents (34%, 122 docs/sec) +``` + +Applied to: +- `dev index` - scanning & embedding progress +- `dev update` - changed files & embedding progress +- `dev git index` - commit embedding progress +- `dev github index` - document embedding progress + +### Update Plan Display +`dev update` now shows what will change before starting: + +``` +Update plan: + • Changed: 3 files + • Added: 1 file + • Deleted: 0 files +``` + +### Code Quality +- Refactored progress logic into `ProgressRenderer.updateSectionWithRate()` +- Reduced ~40 lines of duplicated code +- Fixed NaN display (now shows "Discovering files..." initially) + +## Bug Fixes 🐛 + +- **`dev owners`**: Fixed "No ownership data" error when run from subdirectories +- **Progress Display**: Fixed NaN showing during initial file discovery phase +- **`dev update`**: Removed duplicate checkmark in success message + +## Breaking Changes + +None - all changes are backward compatible. Old GitHub state files will fall back to aggregate counts gracefully. +