Skip to content

Commit 46ffe37

Browse files
committed
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.
1 parent 9da8107 commit 46ffe37

9 files changed

Lines changed: 117 additions & 68 deletions

File tree

packages/cli/src/commands/activity.ts

Lines changed: 23 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import * as path from 'node:path';
66
import {
77
type FileMetrics,
8+
getFileIcon,
89
getMostActive,
910
getStoragePath,
1011
MetricsStore,
@@ -31,43 +32,38 @@ function formatRelativeTime(date: Date): string {
3132
}
3233

3334
/**
34-
* Format files as a compact table
35+
* Format files with tree branches and icons
3536
*/
3637
function formatFileMetricsTable(files: FileMetrics[]): string {
3738
if (files.length === 0) return '';
3839

39-
// Calculate column widths
40-
const maxPathLen = Math.max(...files.map((f) => f.filePath.length), 40);
41-
const pathWidth = Math.min(maxPathLen, 55);
40+
let output = '';
4241

43-
// Header
44-
let output = chalk.bold(
45-
`${'FILE'.padEnd(pathWidth)} ${'COMMITS'.padStart(7)} ${'LOC'.padStart(6)} ${'AUTHORS'.padStart(7)} ${'LAST CHANGE'}\n`
46-
);
42+
for (let i = 0; i < files.length; i++) {
43+
const file = files[i];
44+
const isLast = i === files.length - 1;
45+
const branch = isLast ? '└─' : '├─';
46+
const connector = isLast ? ' ' : '│'; // Vertical line for non-last items
47+
const icon = getFileIcon(file.filePath);
4748

48-
// Separator line
49-
output += chalk.dim(`${'─'.repeat(pathWidth + 2 + 7 + 2 + 6 + 2 + 7 + 2 + 12)}\n`);
50-
51-
// Rows
52-
for (const file of files) {
53-
// Truncate path if too long
54-
let displayPath = file.filePath;
55-
if (displayPath.length > pathWidth) {
56-
displayPath = `...${displayPath.slice(-(pathWidth - 3))}`;
57-
}
58-
displayPath = displayPath.padEnd(pathWidth);
59-
60-
const commits = String(file.commitCount).padStart(7);
61-
const loc = String(file.linesOfCode).padStart(6);
62-
63-
// Author count with emoji
64-
const authorIcon = file.authorCount === 1 ? ' 👤' : file.authorCount === 2 ? ' 👥' : '👥👥';
65-
const authors = `${String(file.authorCount).padStart(5)}${authorIcon}`;
49+
// Author info
50+
const authorIcon = file.authorCount === 1 ? '👤' : file.authorCount === 2 ? '👥' : '👥👥';
6651

6752
// Relative time
6853
const lastChange = file.lastModified ? formatRelativeTime(file.lastModified) : 'unknown';
6954

70-
output += `${chalk.dim(displayPath)} ${chalk.cyan(commits)} ${chalk.yellow(loc)} ${chalk.green(authors)} ${chalk.gray(lastChange)}\n`;
55+
// File path line with icon and branch
56+
output += `${chalk.dim(branch)} ${icon} ${file.filePath}\n`;
57+
58+
// Metrics line with vertical connector
59+
output += chalk.dim(
60+
`${connector} ${file.commitCount} commits • ${file.authorCount} ${authorIcon} • Last: ${lastChange}\n`
61+
);
62+
63+
// Add vertical line separator between items (except after last)
64+
if (!isLast) {
65+
output += chalk.dim(`${connector}\n`);
66+
}
7167
}
7268

7369
return output;

packages/cli/src/commands/stats.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ async function loadCurrentStats(): Promise<{
100100
totalDocuments: state.totalDocuments || 0,
101101
byType: state.byType || {},
102102
byState: state.byState || {},
103+
issuesByState: state.issuesByState,
104+
prsByState: state.prsByState,
103105
lastIndexed: state.lastIndexed || '',
104106
indexDuration: state.indexDuration || 0,
105107
};
@@ -217,6 +219,8 @@ What You'll See:
217219
totalDocuments: number;
218220
byType: { issue?: number; pull_request?: number };
219221
byState: { open?: number; closed?: number; merged?: number };
222+
issuesByState?: { open: number; closed: number };
223+
prsByState?: { open: number; closed: number; merged: number };
220224
lastIndexed: string;
221225
} | null) || undefined,
222226
});

packages/cli/src/utils/output.ts

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,26 @@ export function formatGitHubSummary(githubStats: {
142142
totalDocuments: number;
143143
byType: { issue?: number; pull_request?: number };
144144
byState: { open?: number; closed?: number; merged?: number };
145+
issuesByState?: { open: number; closed: number };
146+
prsByState?: { open: number; closed: number; merged: number };
145147
lastIndexed: string;
146148
}): string {
147149
const issues = githubStats.byType.issue || 0;
148150
const prs = githubStats.byType.pull_request || 0;
149-
const open = githubStats.byState.open || 0;
150-
const merged = githubStats.byState.merged || 0;
151+
152+
// Use per-type state counts if available (new format), fall back to aggregate (old format)
153+
const issuesOpen = githubStats.issuesByState?.open ?? 0;
154+
const issuesClosed = githubStats.issuesByState?.closed ?? 0;
155+
const prsOpen = githubStats.prsByState?.open ?? 0;
156+
const prsMerged = githubStats.prsByState?.merged ?? 0;
151157

152158
const timeSince = getTimeSince(new Date(githubStats.lastIndexed));
153159

154160
return [
155161
`🔗 ${chalk.bold(githubStats.repository)}${formatNumber(githubStats.totalDocuments)} documents`,
156-
` ${chalk.gray(issues.toString())} issues • ${chalk.gray(prs.toString())} PRs • ${chalk.gray(open.toString())} open • ${chalk.gray(merged.toString())} merged • Synced ${timeSince}`,
162+
` Issues: ${chalk.gray(issues.toString())} total (${issuesOpen} open, ${issuesClosed} closed)`,
163+
` Pull Requests: ${chalk.gray(prs.toString())} total (${prsOpen} open, ${prsMerged} merged)`,
164+
` Last synced: ${timeSince}`,
157165
].join('\n');
158166
}
159167

@@ -369,6 +377,8 @@ export function printRepositoryStats(data: {
369377
totalDocuments: number;
370378
byType: { issue?: number; pull_request?: number };
371379
byState: { open?: number; closed?: number; merged?: number };
380+
issuesByState?: { open: number; closed: number };
381+
prsByState?: { open: number; closed: number; merged: number };
372382
lastIndexed: string;
373383
} | null;
374384
}): void {
@@ -471,19 +481,21 @@ export function printRepositoryStats(data: {
471481
const issues = githubStats.byType.issue || 0;
472482
const prs = githubStats.byType.pull_request || 0;
473483

484+
// Use per-type state counts if available (new format), fall back to aggregate (old format)
485+
const issuesOpen = githubStats.issuesByState?.open ?? githubStats.byState.open ?? 0;
486+
const issuesClosed = githubStats.issuesByState?.closed ?? githubStats.byState.closed ?? 0;
487+
const prsOpen = githubStats.prsByState?.open ?? githubStats.byState.open ?? 0;
488+
const prsMerged = githubStats.prsByState?.merged ?? githubStats.byState.merged ?? 0;
489+
474490
if (issues > 0) {
475-
const openIssues = githubStats.byState.open || 0;
476-
const closedIssues = githubStats.byState.closed || 0;
477491
output.log(
478-
` Issues: ${chalk.bold(issues.toString())} total (${chalk.green(`${openIssues} open`)}, ${chalk.gray(`${closedIssues} closed`)})`
492+
` Issues: ${chalk.bold(issues.toString())} total (${issuesOpen} open, ${issuesClosed} closed)`
479493
);
480494
}
481495

482496
if (prs > 0) {
483-
const openPRs = githubStats.byState.open || 0;
484-
const mergedPRs = githubStats.byState.merged || 0;
485497
output.log(
486-
` Pull Requests: ${chalk.bold(prs.toString())} total (${chalk.green(`${openPRs} open`)}, ${chalk.magenta(`${mergedPRs} merged`)})`
498+
` Pull Requests: ${chalk.bold(prs.toString())} total (${prsOpen} open, ${prsMerged} merged)`
487499
);
488500
}
489501

@@ -905,16 +917,21 @@ export function printGitHubStats(githubStats: {
905917
totalDocuments: number;
906918
byType: { issue?: number; pull_request?: number; discussion?: number };
907919
byState: { open?: number; closed?: number; merged?: number };
920+
issuesByState?: { open: number; closed: number };
921+
prsByState?: { open: number; closed: number; merged: number };
908922
lastIndexed: string;
909923
indexDuration?: number;
910924
}): void {
911925
const issues = githubStats.byType.issue || 0;
912926
const prs = githubStats.byType.pull_request || 0;
913927
const discussions = githubStats.byType.discussion || 0;
914928

915-
const openCount = githubStats.byState.open || 0;
916-
const closedCount = githubStats.byState.closed || 0;
917-
const mergedCount = githubStats.byState.merged || 0;
929+
// Use per-type state counts if available (new format), fall back to aggregate (old format)
930+
const issueOpen = githubStats.issuesByState?.open ?? 0;
931+
const issueClosed = githubStats.issuesByState?.closed ?? 0;
932+
const prOpen = githubStats.prsByState?.open ?? 0;
933+
const prClosed = githubStats.prsByState?.closed ?? 0;
934+
const prMerged = githubStats.prsByState?.merged ?? 0;
918935

919936
const timeSince = getTimeSince(new Date(githubStats.lastIndexed));
920937

@@ -928,9 +945,6 @@ export function printGitHubStats(githubStats: {
928945
// Issues breakdown
929946
if (issues > 0) {
930947
const issueStates: string[] = [];
931-
// Calculate issue-specific counts (open + closed, no merged for issues)
932-
const issueOpen = openCount > 0 ? openCount : 0;
933-
const issueClosed = closedCount > 0 ? closedCount : 0;
934948

935949
if (issueOpen > 0) {
936950
issueStates.push(`${chalk.green('●')} ${issueOpen} open`);
@@ -949,13 +963,13 @@ export function printGitHubStats(githubStats: {
949963
// Pull requests breakdown
950964
if (prs > 0) {
951965
const prStates: string[] = [];
952-
// For PRs, we show open and merged
953-
const prOpen = openCount > 0 ? openCount : 0;
954-
const prMerged = mergedCount > 0 ? mergedCount : 0;
955966

956967
if (prOpen > 0) {
957968
prStates.push(`${chalk.green('●')} ${prOpen} open`);
958969
}
970+
if (prClosed > 0) {
971+
prStates.push(`${chalk.gray('●')} ${prClosed} closed`);
972+
}
959973
if (prMerged > 0) {
960974
prStates.push(`${chalk.magenta('●')} ${prMerged} merged`);
961975
}

packages/core/src/map/__tests__/map.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,9 @@ describe('Codebase Map', () => {
485485
const output = formatCodebaseMap(map, { includeHotPaths: true });
486486

487487
expect(output).toContain('## Hot Paths');
488-
expect(output).toContain('src/core.ts');
488+
expect(output).toContain('**core.ts**'); // Filename in bold
489489
expect(output).toContain('2 refs');
490+
expect(output).toContain('src'); // Directory path on separate line
490491
});
491492
});
492493

packages/core/src/map/index.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as path from 'node:path';
77
import type { Logger } from '@lytics/kero';
88
import type { LocalGitExtractor } from '../git/extractor';
99
import type { RepositoryIndexer } from '../indexer';
10+
import { getFileIcon } from '../utils/icons';
1011
import type { SearchResult } from '../vector/types';
1112
import type {
1213
ChangeFrequency,
@@ -486,26 +487,6 @@ function computeHotPaths(docs: SearchResult[], maxPaths: number): HotPath[] {
486487
return sorted;
487488
}
488489

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-
509490
/**
510491
* Format codebase map as readable text
511492
*/

packages/core/src/utils/icons.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Utility functions for file and UI icons
3+
*/
4+
5+
/**
6+
* Get file icon based on extension
7+
*/
8+
export function getFileIcon(filePathOrExt: string): string {
9+
// Extract extension (handle both full paths and just extensions)
10+
const ext = filePathOrExt.includes('.') ? filePathOrExt.split('.').pop() || '' : filePathOrExt;
11+
12+
const iconMap: Record<string, string> = {
13+
ts: '📘',
14+
tsx: '⚛️',
15+
js: '📜',
16+
jsx: '⚛️',
17+
go: '🐹',
18+
py: '🐍',
19+
rs: '🦀',
20+
md: '📝',
21+
json: '📋',
22+
yaml: '⚙️',
23+
yml: '⚙️',
24+
};
25+
26+
return iconMap[ext] || '📄';
27+
}

packages/core/src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44

55
export * from './concurrency';
66
export * from './file-validator';
7+
export * from './icons';
78
export * from './retry';
89
export * from './wasm-resolver';

packages/subagents/src/github/indexer.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,21 @@ export class GitHubIndexer {
178178
{} as Record<string, number>
179179
);
180180

181+
// Calculate states per type for accurate reporting
182+
const issuesByState = { open: 0, closed: 0 };
183+
const prsByState = { open: 0, closed: 0, merged: 0 };
184+
185+
for (const doc of enrichedDocs) {
186+
if (doc.type === 'issue') {
187+
if (doc.state === 'open') issuesByState.open++;
188+
else if (doc.state === 'closed') issuesByState.closed++;
189+
} else if (doc.type === 'pull_request') {
190+
if (doc.state === 'open') prsByState.open++;
191+
else if (doc.state === 'closed') prsByState.closed++;
192+
else if (doc.state === 'merged') prsByState.merged++;
193+
}
194+
}
195+
181196
// Update state
182197
this.state = {
183198
version: INDEXER_VERSION,
@@ -186,6 +201,8 @@ export class GitHubIndexer {
186201
totalDocuments: enrichedDocs.length,
187202
byType: byType as Record<'issue' | 'pull_request' | 'discussion', number>,
188203
byState: byState as Record<'open' | 'closed' | 'merged', number>,
204+
issuesByState,
205+
prsByState,
189206
};
190207

191208
// Save state to disk
@@ -202,6 +219,8 @@ export class GitHubIndexer {
202219
totalDocuments: enrichedDocs.length,
203220
byType: byType as Record<'issue' | 'pull_request' | 'discussion', number>,
204221
byState: byState as Record<'open' | 'closed' | 'merged', number>,
222+
issuesByState,
223+
prsByState,
205224
lastIndexed: this.state.lastIndexed,
206225
indexDuration: durationMs,
207226
};
@@ -398,6 +417,8 @@ export class GitHubIndexer {
398417
totalDocuments: this.state.totalDocuments,
399418
byType: this.state.byType,
400419
byState: this.state.byState,
420+
issuesByState: this.state.issuesByState,
421+
prsByState: this.state.prsByState,
401422
lastIndexed: this.state.lastIndexed,
402423
indexDuration: 0,
403424
};

packages/types/src/github.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ export interface GitHubIndexerState {
110110
lastIndexed: string; // ISO date
111111
totalDocuments: number;
112112
byType: Record<GitHubDocumentType, number>;
113-
byState: Record<GitHubState, number>;
113+
byState: Record<GitHubState, number>; // Deprecated: aggregate counts (kept for compatibility)
114+
issuesByState: { open: number; closed: number };
115+
prsByState: { open: number; closed: number; merged: number };
114116
}
115117

116118
/**
@@ -145,7 +147,9 @@ export interface GitHubIndexStats {
145147
repository: string;
146148
totalDocuments: number;
147149
byType: Record<GitHubDocumentType, number>;
148-
byState: Record<GitHubState, number>;
150+
byState: Record<GitHubState, number>; // Deprecated: aggregate counts (kept for compatibility)
151+
issuesByState: { open: number; closed: number };
152+
prsByState: { open: number; closed: number; merged: number };
149153
lastIndexed: string; // ISO date
150154
indexDuration: number; // milliseconds
151155
}

0 commit comments

Comments
 (0)