diff --git a/src/dashboard.tsx b/src/dashboard.tsx index b46dbcc..6a95da4 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -247,16 +247,19 @@ function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSumma ) } -const _homeEncoded = homedir().replace(/\//g, '-') - -function shortProject(encoded: string): string { - let path = encoded.replace(/^-/, '') - if (path.startsWith(_homeEncoded.replace(/^-/, ''))) { - path = path.slice(_homeEncoded.replace(/^-/, '').length).replace(/^-/, '') - } - path = path.replace(/^private-tmp-[^-]+-[^-]+-/, '').replace(/^private-tmp-/, '').replace(/^tmp-/, '') +const _home = homedir() +const _homePrefix = _home.endsWith('/') ? _home : _home + '/' + +export function shortProject(absPath: string): string { + const normalized = absPath.replace(/\\/g, '/') + let path: string + if (normalized === _home) path = '' + else if (normalized.startsWith(_homePrefix)) path = normalized.slice(_homePrefix.length) + else path = normalized + path = path.replace(/^\/+/, '') + path = path.replace(/^private\/tmp\/[^/]+\/[^/]+\//, '').replace(/^private\/tmp\//, '').replace(/^tmp\//, '') if (!path) return 'home' - const parts = path.split('-').filter(Boolean) + const parts = path.split('/').filter(Boolean) if (parts.length <= 3) return parts.join('/') return parts.slice(-3).join('/') } @@ -282,7 +285,7 @@ function ProjectBreakdown({ projects, pw, bw, budgets }: { projects: ProjectSumm return ( - {fit(shortProject(project.project), nw)} + {fit(shortProject(project.projectPath), nw)} {formatCost(project.totalCostUSD).padStart(8)} {avgCost.padStart(PROJECT_COL_AVG)} {String(project.sessions.length).padStart(6)} @@ -442,7 +445,7 @@ const TOP_SESSIONS_CALLS_COL = 6 function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) { const allSessions = projects.flatMap(p => - p.sessions.map(s => ({ ...s, projectName: p.project })) + p.sessions.map(s => ({ ...s, projectPath: p.projectPath })) ) const top = [...allSessions].sort((a, b) => b.totalCostUSD - a.totalCostUSD).slice(0, 5) @@ -460,7 +463,7 @@ function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: num const date = session.firstTimestamp ? session.firstTimestamp.slice(0, TOP_SESSIONS_DATE_LEN) : '----------' - const label = `${date} ${shortProject(session.projectName)}` + const label = `${date} ${shortProject(session.projectPath)}` return ( diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts index 0d36e2e..da802f1 100644 --- a/tests/dashboard.test.ts +++ b/tests/dashboard.test.ts @@ -1,5 +1,8 @@ +import { homedir } from 'os' + import { describe, it, expect } from 'vitest' +import { shortProject } from '../src/dashboard.js' import { formatCost } from '../src/format.js' import type { ProjectSummary, SessionSummary } from '../src/types.js' @@ -53,7 +56,7 @@ function makeProject(name: string, sessions: SessionSummary[]): ProjectSummary { // Logic replicated from TopSessions component function getTopSessions(projects: ProjectSummary[], n = 5) { - const all = projects.flatMap(p => p.sessions.map(s => ({ ...s, projectName: p.project }))) + const all = projects.flatMap(p => p.sessions.map(s => ({ ...s, projectPath: p.projectPath }))) return [...all].sort((a, b) => b.totalCostUSD - a.totalCostUSD).slice(0, n) } @@ -99,6 +102,36 @@ describe('TopSessions - top-5 selection', () => { }) }) +describe('shortProject - path shortening', () => { + const home = homedir() + + it('preserves directory names containing dashes', () => { + expect(shortProject(`${home}/work/my-project`)).toBe('work/my-project') + }) + + it('preserves directory names containing dots', () => { + expect(shortProject(`${home}/work/my.app.io`)).toBe('work/my.app.io') + }) + + it('returns "home" for the home dir itself', () => { + expect(shortProject(home)).toBe('home') + }) + + it('does not strip a sibling whose name shares the home prefix', () => { + const sibling = `${home}-backup/proj` + expect(shortProject(sibling).endsWith('proj')).toBe(true) + expect(shortProject(sibling)).not.toMatch(/^-/) + }) + + it('keeps only the last 3 segments for deeply nested paths', () => { + expect(shortProject(`${home}/a/b/c/d/e/f`)).toBe('d/e/f') + }) + + it('handles paths outside the home dir', () => { + expect(shortProject('/opt/myproject')).toBe('opt/myproject') + }) +}) + describe('avg/s in ProjectBreakdown', () => { it('returns dash for a project with no sessions', () => { const project = makeProject('proj', [])