Skip to content

Commit 62d9cad

Browse files
heqikaicursoragent
andcommitted
feat(panorama): add call-flow graph with Delta, Trace, and export
Ship Panorama as a first-class view: subgraph call trees at a commit, delta-colored Graph tab, trace highlights, shareable drill-down URLs, and vector SVG/hi-DPI PNG export. Includes graph-subgraph package, server API, Android entry resolver, tests, and README updates. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e30396e commit 62d9cad

31 files changed

Lines changed: 4398 additions & 348 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1010
## [Unreleased]
1111

1212
### Added
13+
- **CodeDelta panorama view (全景图).** Interactive call-flow graph built from CodeGraph snapshots: entry exploration from routes/components/exported symbols, Delta View graph tab with change coloring, Trace View navigation with evidence highlighting, optional LLM node labels, and Android MAIN/LAUNCHER activity entry detection in CodeGraph.
1314
- **Java / Kotlin imports now resolve by fully-qualified name.** Extraction
1415
wraps every top-level declaration of a `.kt` / `.java` file in a `namespace`
1516
node carrying the file's `package` (so a class `Bar` in

README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
**Local-first, commit-aware structural code intelligence** — built on [CodeGraph](https://github.com/colbymchenry/codegraph).
77

8-
CodeDelta shows how a codebase’s **structure** changes between commits: symbols, dependency edges, blast radius, and review-oriented summaries. **Trace View** helps narrow down which commit may have introduced a behavior change, with evidence you can verify in **Delta View**.
8+
CodeDelta shows how a codebase’s **structure** changes between commits: symbols, dependency edges, blast radius, and review-oriented summaries. **Trace View** helps narrow down which commit may have introduced a behavior change, with evidence you can verify in **Delta View**. **Panorama** visualizes call-flow trees at a single commit (or a structural diff overlay between two commits).
99

1010
This repository is a fork: the **CodeGraph** engine lives under [`src/`](src/) (CLI + MCP + tree-sitter graph). The **CodeDelta** app lives under [`packages/`](packages/) and [`apps/web/`](apps/web/) (import, timeline, delta, trace, settings UI).
1111

@@ -35,6 +35,19 @@ Compare two commits (`Base` = before, `Head` = after):
3535
- Delta summary (main areas, risks, suggested review order)
3636
- File-level unified diff modal (click files or symbols)
3737
- Per-snapshot metadata (`codegraph` vs `fallback` extraction)
38+
- **Graph tab** — React Flow call tree at the head commit with added/modified/removed coloring on nodes and edges
39+
40+
### Panorama
41+
42+
Interactive call-flow graph from CodeGraph snapshots (React Flow):
43+
44+
- **Single commit** — top entry routes/components/exported symbols, expandable by call depth
45+
- **Branch + commit** selectors; auto-rebuild when either changes
46+
- **Drill-down***Expand from here*, breadcrumb trail, Back / All entry points
47+
- **Shareable URLs**`?branch=&commit=&depth=&focusPath=` preserves your drill-down path
48+
- **Export** — SVG (vector) or hi-DPI PNG generated from graph data (not a DOM screenshot)
49+
- Optional LLM node labels (non-authoritative)
50+
- Linked from **Delta View → Graph**, **Trace View**, and **Commit Timeline**
3851

3952
### Trace View
4053

@@ -44,13 +57,14 @@ Describe a bug, behavior change, or question in natural language:
4457
- Attach evidence per candidate (`previous → candidate` compare when a parent exists)
4558
- Return a direct answer, confidence, uncertainty, and suggested next steps
4659
- Jump to Delta View to verify each candidate
60+
- **View in Panorama** on a candidate commit (trace symbol highlights when available)
4761

4862
**Without any LLM configured**, Trace still returns candidates, evidence, and impact radius (evidence-first, no invented facts).
4963

5064
### Commit timeline & import
5165

5266
- Import a public GitHub repo (`owner/repo` or URL) or a **local git path**
53-
- Browse commits; open Delta or Trace from the timeline
67+
- Browse commits; open Delta, Trace, or Panorama from the timeline
5468

5569
## What CodeDelta is not
5670

@@ -77,6 +91,7 @@ Open [http://localhost:5173](http://localhost:5173).
7791
2. **Commit Timeline** — pick a branch and browse history
7892
3. **Delta View** — choose `Base (before)` and `Head (after)`, then compare
7993
4. **Trace View** — describe an issue; review candidates and open Delta to verify
94+
5. **Panorama** — pick branch/commit and explore call trees; drill down from any entry or route
8095

8196
## UI walkthrough
8297

@@ -105,6 +120,9 @@ API: [http://localhost:3847](http://localhost:3847)
105120
|----------|-------------|
106121
| `GET /api/health` | Health check |
107122
| `GET /api/repos/:id/compare?base=&head=` | Structural delta between commits |
123+
| `GET /api/repos/:id/panorama?commit=&depth=&root=` | Call-flow graph at one commit |
124+
| `GET /api/repos/:id/panorama?base=&head=&depth=` | Delta-colored call-flow graph (head commit tree) |
125+
| `POST /api/repos/:id/panorama/enrich` | Optional LLM labels for panorama nodes |
108126
| `GET /api/repos/:id/diff?base=&head=&file=` | Unified diff for one file |
109127
| `POST /api/repos/:id/trace` | Trace question → candidates + evidence |
110128
| `GET /api/settings/provider` | Current LLM provider settings |
@@ -191,11 +209,12 @@ packages/
191209
codedelta-server/ # REST API
192210
codedelta-snapshot-manager/
193211
codedelta-graph-diff/
212+
codedelta-graph-subgraph/ # Panorama call-tree + layout
194213
codedelta-impact-score/
195214
codedelta-delta-summary/
196215
codedelta-trace-engine/
197216
codedelta-provider-runtime/
198-
apps/web/ # React UI
217+
apps/web/ # React UI (Delta, Trace, Panorama)
199218
apps/desktop/ # macOS desktop shell (Tauri 2)
200219
```
201220

@@ -206,7 +225,8 @@ Roadmap and deferred work: [docs/codedelta/ROADMAP.md](docs/codedelta/ROADMAP.md
206225
- TypeScript/JavaScript-first practical path today
207226
- Delta and trace: **commit-to-commit** only (no PR/branch/working-tree compare yet)
208227
- Codex: local CLI session only (no in-browser OAuth)
209-
- UI: tables/lists (no full graph canvas yet)
228+
- Panorama overview shows **top entry surfaces** only on large repos — drill down with *Expand from here*; sparse graphs often mean mount points (`USE /api/*`) need expansion to see router internals
229+
- Panorama export is a simplified card layout (no live *Expand* buttons); prefer **SVG** for zoom/clarity
210230
- Symbol click opens **file** diff, not symbol-to-hunk mapping
211231

212232
## Desktop (macOS)
@@ -266,9 +286,10 @@ apps/desktop/
266286
npm run build:codedelta
267287
npm run dev:codedelta # API :3847, web :5173, watches provider-runtime
268288

269-
npm test -- packages/codedelta-graph-diff packages/codedelta-impact-score \
270-
packages/codedelta-server packages/codedelta-snapshot-manager \
271-
packages/codedelta-trace-engine packages/codedelta-provider-runtime
289+
npm test -- packages/codedelta-graph-diff packages/codedelta-graph-subgraph \
290+
packages/codedelta-impact-score packages/codedelta-server \
291+
packages/codedelta-snapshot-manager packages/codedelta-trace-engine \
292+
packages/codedelta-provider-runtime __tests__/codedelta
272293
```
273294

274295
Environment variables:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from 'vitest';
2+
import type { PanoramaGraph } from '@codedelta/types';
3+
import { buildPanoramaSvg } from '../../apps/web/src/lib/panorama-export';
4+
5+
function sampleGraph(): PanoramaGraph {
6+
return {
7+
repoId: 'r1',
8+
commit: 'abc1234567890',
9+
commitShortHash: 'abc1234',
10+
nodes: [
11+
{
12+
id: 'n1',
13+
kind: 'route',
14+
name: 'GET /',
15+
qualifiedName: 'GET /',
16+
filePath: 'src/app.ts',
17+
startLine: 1,
18+
endLine: 5,
19+
commitShortHash: 'abc1234',
20+
role: 'entry',
21+
position: { x: 0, y: 0 },
22+
},
23+
{
24+
id: 'n2',
25+
kind: 'function',
26+
name: 'handler',
27+
qualifiedName: 'handler',
28+
filePath: 'src/app.ts',
29+
startLine: 10,
30+
endLine: 20,
31+
commitShortHash: 'abc1234',
32+
role: 'leaf',
33+
position: { x: 0, y: 240 },
34+
},
35+
],
36+
edges: [{ id: 'e1', source: 'n1', target: 'n2', kind: 'calls' }],
37+
entryPoints: ['n1'],
38+
layout: 'layered',
39+
stats: { nodeCount: 2, edgeCount: 1, truncated: false },
40+
};
41+
}
42+
43+
describe('buildPanoramaSvg', () => {
44+
it('produces valid SVG with nodes and edges', () => {
45+
const svg = buildPanoramaSvg(sampleGraph());
46+
expect(svg).toContain('<svg');
47+
expect(svg).toContain('GET /');
48+
expect(svg).toContain('<path');
49+
expect(svg).toContain('CodeDelta Panorama');
50+
});
51+
52+
it('scales layout when renderScale is set', () => {
53+
const base = buildPanoramaSvg(sampleGraph());
54+
const scaled = buildPanoramaSvg(sampleGraph(), { renderScale: 2 });
55+
const baseW = base.match(/width="(\d+)"/)?.[1];
56+
const scaledW = scaled.match(/width="(\d+)"/)?.[1];
57+
expect(Number(scaledW)).toBeGreaterThan(Number(baseW));
58+
});
59+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
buildFocusTrail,
4+
focusAtTrailIndex,
5+
parseFocusPathParam,
6+
serializeFocusPath,
7+
} from '../../apps/web/src/lib/panorama-focus';
8+
9+
describe('panorama-focus URL trail', () => {
10+
it('round-trips a multi-hop focus path', () => {
11+
const trail = buildFocusTrail(['', 'MainActivity.onCreate'], 'handleClick');
12+
const encoded = serializeFocusPath(trail);
13+
expect(encoded).toBeTruthy();
14+
const parsed = parseFocusPathParam(encoded);
15+
expect(parsed.root).toBe('handleClick');
16+
expect(parsed.stack).toEqual(['', 'MainActivity.onCreate']);
17+
});
18+
19+
it('navigates breadcrumb index back to overview', () => {
20+
const next = focusAtTrailIndex(['', 'A'], 'B', 0);
21+
expect(next).toEqual({ stack: [], root: '' });
22+
});
23+
24+
it('navigates breadcrumb index to intermediate hop', () => {
25+
const next = focusAtTrailIndex(['', 'A'], 'B', 1);
26+
expect(next).toEqual({ stack: [''], root: 'A' });
27+
});
28+
29+
it('encodes symbols with special characters', () => {
30+
const trail = buildFocusTrail([], 'GET /api/users');
31+
const encoded = serializeFocusPath(trail)!;
32+
const parsed = parseFocusPathParam(encoded);
33+
expect(parsed.root).toBe('GET /api/users');
34+
});
35+
});

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"preview": "vite preview"
1010
},
1111
"dependencies": {
12+
"@xyflow/react": "^12.6.0",
1213
"react": "^19.0.0",
1314
"react-dom": "^19.0.0",
1415
"react-router-dom": "^7.1.1"

apps/web/src/api/client.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import type {
99
ImpactSummary,
1010
ImportRepoRequest,
1111
ModelProviderConfig,
12+
PanoramaEnrichResult,
13+
PanoramaGraph,
1214
ProviderKind,
1315
RepoRef,
1416
TraceAnswer,
@@ -84,6 +86,39 @@ export const api = {
8486
method: 'PUT',
8587
body: JSON.stringify(config),
8688
}),
89+
90+
getPanorama: (
91+
id: string,
92+
params: {
93+
commit?: string;
94+
base?: string;
95+
head?: string;
96+
root?: string;
97+
depth?: number;
98+
maxNodes?: number;
99+
highlight?: 'trace';
100+
traceSymbols?: string[];
101+
traceEntryPoints?: string[];
102+
},
103+
) => {
104+
const q = new URLSearchParams();
105+
if (params.commit) q.set('commit', params.commit);
106+
if (params.base) q.set('base', params.base);
107+
if (params.head) q.set('head', params.head);
108+
if (params.root) q.set('root', params.root);
109+
if (params.depth != null) q.set('depth', String(params.depth));
110+
if (params.maxNodes != null) q.set('maxNodes', String(params.maxNodes));
111+
if (params.highlight) q.set('highlight', params.highlight);
112+
if (params.traceSymbols?.length) q.set('traceSymbols', params.traceSymbols.join(','));
113+
if (params.traceEntryPoints?.length) q.set('traceEntryPoints', params.traceEntryPoints.join(','));
114+
return request<PanoramaGraph>(`/api/repos/${id}/panorama?${q.toString()}`);
115+
},
116+
117+
enrichPanorama: (id: string, body: { commit: string; nodeIds: string[] }) =>
118+
request<PanoramaEnrichResult>(`/api/repos/${id}/panorama/enrich`, {
119+
method: 'POST',
120+
body: JSON.stringify(body),
121+
}),
87122
};
88123

89124
export type {
@@ -100,4 +135,6 @@ export type {
100135
CodeNode,
101136
ModelProviderConfig,
102137
ProviderKind,
138+
PanoramaGraph,
139+
PanoramaEnrichResult,
103140
};

apps/web/src/components/AppShell.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ function ShellInner() {
6060
>
6161
Trace View
6262
</NavLink>
63+
<NavLink
64+
to={repoNavPath(repoId, 'panorama')}
65+
className={({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')}
66+
>
67+
Panorama
68+
</NavLink>
6369
</>
6470
) : (
6571
<>
@@ -69,6 +75,9 @@ function ShellInner() {
6975
<span className="nav-link disabled" title="Import a repository first">
7076
Trace View
7177
</span>
78+
<span className="nav-link disabled" title="Import a repository first">
79+
Panorama
80+
</span>
7281
</>
7382
)}
7483
</section>

0 commit comments

Comments
 (0)