Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to Ledger are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **FileGraph Visualization** - New `file-graph` viz panel type
- Treemap visualization showing repository code sized by line count
- Files colored by programming language (25+ languages supported)
- Click folders to drill down, breadcrumb navigation to go back up
- Language legend showing line counts per language
- Hover tooltips with file path, line count, and language
- Respects `.gitignore` (uses `git ls-files`)
- Available as a panel type in the Graph canvas

## [0.1.0] - 2024-12-27

### Added
Expand Down
23 changes: 22 additions & 1 deletion app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ export default function App() {
const [stashes, setStashes] = useState<StashEntry[]>([])
const [loadingDiff, setLoadingDiff] = useState(false)
const [sidebarFocus, setSidebarFocus] = useState<SidebarFocus | null>(null)
// FileGraph state
const [fileGraph, setFileGraph] = useState<import('./types/electron').FileGraphData | null>(null)
const [fileGraphLoading, setFileGraphLoading] = useState(false)
// History/Commits panel filters (shared between Radar and Focus modes)
// Graph display options
const [showCheckpoints] = useState(false) // Hide Conductor checkpoints by default
Expand Down Expand Up @@ -350,6 +353,8 @@ export default function App() {
setCommits([])
setPullRequests([])
setWorkingStatus(null)
setFileGraph(null)
setFileGraphLoading(false)
setRepoPath(path)
setStatus({ type: 'info', message: 'Loading repository...' })
await refresh(path)
Expand Down Expand Up @@ -433,6 +438,20 @@ export default function App() {
.catch(() => {
// Silently ignore - we already have basic branch data
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FileGraph state not cleared when switching repositories

High Severity

When switching repositories, fileGraph and fileGraphLoading states are not cleared along with other repository-specific state. Since FileGraph loading is async and explicitly noted as slow, stale data from the previous repository can overwrite data from the new repository, displaying incorrect file treemap visualizations to the user.

Additional Locations (1)

Fix in Cursor Fix in Web

// Phase 3: FileGraph loading (can be slow on large repos)
setFileGraphLoading(true)
window.electronAPI
.getFileGraph()
.then((result) => {
setFileGraph(result)
})
.catch(() => {
setFileGraph(null)
})
.finally(() => {
setFileGraphLoading(false)
})
} catch (err) {
setError((err as Error).message)
} finally {
Expand Down Expand Up @@ -1338,7 +1357,9 @@ export default function App() {
workingStatus,
commitDiff,
loadingDiff,
}), [repoPath, pullRequests, prError, branches, currentBranch, worktrees, stashes, graphCommits, workingStatus, commitDiff, loadingDiff])
fileGraph,
fileGraphLoading,
}), [repoPath, pullRequests, prError, branches, currentBranch, worktrees, stashes, graphCommits, workingStatus, commitDiff, loadingDiff, fileGraph, fileGraphLoading])

const canvasSelection: CanvasSelection = useMemo(() => ({
selectedPR: sidebarFocus?.type === 'pr' ? (sidebarFocus.data as PullRequest) : null,
Expand Down
3 changes: 3 additions & 0 deletions app/components/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ const PANEL_OPTIONS: Record<SlotType, { value: PanelType; label: string }[]> = {
{ value: 'git-graph', label: 'Git Graph' },
{ value: 'timeline', label: 'Timeline' },
{ value: 'tech-tree', label: 'Tech Tree' },
{ value: 'erd-canvas', label: 'ERD' },
{ value: 'codegraph', label: 'Code Graph' },
{ value: 'file-graph', label: 'Code Map' },
],
}

Expand Down
27 changes: 24 additions & 3 deletions app/components/canvas/CanvasRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { EditorSlot } from './EditorSlot'

// Import panels
import { PRList, BranchList, WorktreeList, StashList, CommitList, Sidebar, RepoList } from '../panels/list'
import { GitGraph, ContributorChart, TechTreeChart } from '../panels/viz'
import { GitGraph, ContributorChart, TechTreeChart, FileGraph } from '../panels/viz'
import { ERDCanvasPanel } from '../panels/viz/erd'
import { CodeGraphPanel } from '../panels/viz/codegraph'

Expand Down Expand Up @@ -69,6 +69,10 @@ export interface CanvasData {
// Commit diff (for viewing diffs)
commitDiff: CommitDiff | null
loadingDiff: boolean

// FileGraph data
fileGraph: import('../../../types/electron').FileGraphData | null
fileGraphLoading: boolean
}

/**
Expand Down Expand Up @@ -401,7 +405,8 @@ export function CanvasRenderer({
{ id: 'timeline', label: 'Timeline', icon: '◔' },
{ id: 'tech-tree', label: 'Tech Tree', icon: '⬡' },
{ id: 'erd-canvas', label: 'ERD', icon: '◫' },
{ id: 'codegraph', label: 'Code Graph', icon: '⬡' },
{ id: 'codegraph', label: 'Code Graph', icon: '⬢' },
{ id: 'file-graph', label: 'Code Map', icon: '▦' },
]

return (
Expand Down Expand Up @@ -531,14 +536,28 @@ export function CanvasRenderer({
<VizHeader
panel={column.panel}
label={column.label || 'Code Graph'}
icon={column.icon || ''}
icon={column.icon || ''}
/>
<div className="viz-panel-content codegraph-content">
<CodeGraphPanel repoPath={data.repoPath} />
</div>
</div>
)

case 'file-graph':
return (
<div className="viz-panel file-graph-panel">
<VizHeader
panel={column.panel}
label={column.label || 'Code Map'}
icon={column.icon || '▦'}
/>
<div className="viz-panel-content file-graph-content">
<FileGraph data={data.fileGraph} loading={data.fileGraphLoading} />
</div>
</div>
)

default:
return (
<div className="empty-column">
Expand All @@ -550,6 +569,8 @@ export function CanvasRenderer({
[
data.commits,
data.repoPath,
data.fileGraph,
data.fileGraphLoading,
selection.selectedCommit,
handlers,
activeCanvas,
Expand Down
18 changes: 9 additions & 9 deletions app/components/panels/viz/ContributorChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,8 @@ export function ContributorChart({
<defs>
{/* Gradient for the glow effect */}
<linearGradient id="ridge-glow" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor={invertedTheme ? '#fff' : 'var(--accent)'} stopOpacity="0.8" />
<stop offset="100%" stopColor={invertedTheme ? '#fff' : 'var(--accent)'} stopOpacity="0" />
<stop offset="0%" stopColor={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'} stopOpacity="0.8" />
<stop offset="100%" stopColor={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'} stopOpacity="0" />
</linearGradient>

{/* Clip path for clean edges */}
Expand All @@ -301,7 +301,7 @@ export function ContributorChart({
y="0"
width={chartWidth}
height={height}
fill={invertedTheme ? '#0a0a0a' : 'var(--bg-secondary)'}
fill={invertedTheme ? 'var(--bg-primary)' : 'var(--bg-secondary)'}
/>

{/* Ridgelines - render back to front for proper overlap */}
Expand All @@ -325,7 +325,7 @@ export function ContributorChart({
{/* Fill area - solid background to occlude lines behind */}
<path
d={fillPath}
fill={invertedTheme ? '#0a0a0a' : 'var(--bg-secondary)'}
fill={invertedTheme ? 'var(--bg-primary)' : 'var(--bg-secondary)'}
className="ridge-fill-bg"
/>

Expand All @@ -341,7 +341,7 @@ export function ContributorChart({
<path
d={linePath}
fill="none"
stroke={invertedTheme ? '#ffffff' : 'var(--accent)'}
stroke={invertedTheme ? 'var(--text-primary)' : 'var(--accent)'}
strokeWidth={isHovered ? 2.5 : 1.5}
opacity={isHovered ? 1 : 0.85}
className="ridge-line"
Expand All @@ -368,7 +368,7 @@ export function ContributorChart({
y={baseY + 4}
textAnchor="end"
className={`author-label ${isHovered ? 'hovered' : ''}`}
fill={invertedTheme ? '#ffffff' : 'var(--text-primary)'}
fill="var(--text-primary)"
opacity={isHovered ? 1 : 0.7}
fontSize="12"
fontFamily="var(--font-sans, system-ui)"
Expand All @@ -382,7 +382,7 @@ export function ContributorChart({
y={baseY + 18}
textAnchor="end"
className="commit-count"
fill={invertedTheme ? '#666' : 'var(--text-muted)'}
fill="var(--text-muted)"
fontSize="10"
fontFamily="var(--font-mono, monospace)"
>
Expand All @@ -400,7 +400,7 @@ export function ContributorChart({
y1={height - bottomPadding + 10}
x2={chartWidth - 20}
y2={height - bottomPadding + 10}
stroke={invertedTheme ? '#333' : 'var(--border)'}
stroke="var(--border)"
strokeWidth="1"
/>
{timeLabels.map((label, i) => (
Expand All @@ -409,7 +409,7 @@ export function ContributorChart({
x={label.x}
y={height - bottomPadding + 28}
textAnchor="middle"
fill={invertedTheme ? '#666' : 'var(--text-muted)'}
fill="var(--text-muted)"
fontSize="11"
fontFamily="var(--font-sans, system-ui)"
>
Expand Down
Loading