Repository navigation
Architecture
Code Canvas runs in two processes with different capabilities, and the split determines where every piece of logic belongs.
┌─────────────────────────────────────┐ ┌──────────────────────────────────┐
│ EXTENSION HOST (Node.js) │ │ WEBVIEW (browser sandbox) │
│ extension/src/*.ts → dist/extension.js│ │ webview/src/* → media/assets/*.js│
│ │ │ │
│ • filesystem access (fast-glob, fs) │ post │ • React 18 + React Flow v11 │
│ • VS Code API (config, editors, LSP)│Message │ • layout engines (ELK worker) │
│ • import parsing + resolution │◄──────►│ • edge routing (A*) │
│ • auto-description generation │ │ • rendering & interaction │
│ • .code-canvas/ persistence │ │ • NO filesystem, NO VS Code API │
└─────────────────────────────────────┘ └──────────────────────────────────┘
The webview cannot read files or call the VS Code API. Anything requiring either must be a message round-trip. Conversely, layout and rendering must stay in the webview — they need DOM measurements and would block the extension host.
npm workspaces, two packages:
| Package | Build | Output |
|---|---|---|
extension/ |
tsup (CJS bundle) |
extension/dist/extension.js |
webview/ |
vite (ESM, minified) |
extension/media/assets/* |
npm run build at the root builds the webview first, then the extension —
the order matters because Vite writes into extension/media/, which is packaged
into the VSIX.
⚠️ webview/vite.config.tssetsemptyOutDir: true, so a webview build wipesextension/media/. Everything there is generated; never hand-edit that folder.
| File | Responsibility |
|---|---|
extension.ts |
Activation, commands, webview lifecycle, message dispatch |
graph.ts |
buildIndex, subgraph, describeGraph — the whole indexing pipeline |
imports.ts |
Regex scanners for import/export statements and their bindings |
describe.ts |
Doc extraction and description composition (pure, no I/O) |
lsp.ts |
Document symbol lookup via the VS Code LSP |
git.ts |
Changed-file detection and repository state watching |
meta.ts |
.code-canvas/meta.json read/merge/write |
util.ts |
Shared types (Graph, GraphNode, GraphEdge, EdgeLink) and webview HTML loading |
| File | Responsibility |
|---|---|
App.tsx |
State, message handling, projection→React Flow conversion, interaction |
model/graphModel.ts |
Collapse-first model, edge projection, initial expansion |
layout.ts |
The one layout pipeline; container recursion and non-overlap enforcement |
layout/radial.ts |
Radial engine — concentric rings, orphans in the centre |
edges/route.ts |
Circuit routing: Hanan grid construction and A* search |
edges/CircuitEdge.tsx |
Edge component; paints a precomputed path |
nodeTypes.tsx |
file / folder / group node components, module-scope nodeTypes
|
code/CodeCard.tsx |
Syntax-highlighted, scrollable code preview |
components/ |
Description panel, tag bar, tag filter, edge tooltip overlay |
store/metaStore.ts |
Zustand store for per-file metadata |
text.ts |
firstSentence — shared by the panel and folder nodes |
workspace files
│ fast-glob (+ excludeGlobs)
▼
buildIndex ─────────────────────────────► Index
│ parse imports/exports { nodes, imports, importLines,
│ resolve specifiers → abs paths importBindings, lang, exports, doc }
▼
subgraph(seeds, cap) ──────────────────► Graph { nodes[], edges[] }
│ BFS over imports, then top-up
│ build folder-group hierarchy
│ attach EdgeLinks (symbol names)
▼
describeGraph ─────────────────────────► AutoDescriptions
│
│ postMessage { type: 'graph' | 'autoDescriptions' }
▼
══════════════ webview boundary ══════════════
│
buildModel(rawNodes, rawEdges) ────────► GraphModel (full tree, kept as data)
│
initialExpansion(model) ───────────────► Set<folderId> (opening depth)
│
projectGraph(model, expanded, hidden) ─► Projection { nodes[], edges[] }
│ endpoints mapped to nearest visible ancestor
▼
toRfNode / toRfEdge ───────────────────► React Flow arrays
│
getLayoutedElements(nodes, edges, algo) ► positioned nodes
│
routeEdges(...) ───────────────────────► SVG paths baked into edge.data.d
▼
render
The critical structural point: the full graph never enters React Flow. It
lives in GraphModel, and only a projection of it is handed over. See
03 — Graph model.
| Message | Payload | Effect |
|---|---|---|
requestGraph |
— | Index the workspace and return a subgraph |
requestCodeMany |
paths[] |
Return file contents for previews |
requestMeta |
— | Return .code-canvas/meta.json
|
updateFileMeta |
path, fields |
Merge and persist file metadata |
expand |
ids[] |
Grow the graph around the given nodes |
loadMore |
— | Raise the node cap and re-send |
openFile |
path, line?
|
Open the file in an editor |
requestDefOpen |
path, line, character
|
Go to definition |
requestRefs |
path, line, character
|
Find references |
requestChanged |
— | Return Git-changed files |
seedFolder |
— | Prompt for a folder and reseed |
toggleRefs |
— | Toggle the references panel |
| Message | Payload |
|---|---|
graph |
{ nodes, edges } — the subgraph |
expandResult |
Additional nodes/edges to merge |
autoDescriptions |
{ path: { kind, autoDescription } } |
code / codeMany
|
File contents |
metaLoaded / metaSaved
|
Persisted metadata |
refs |
Reference search results |
changedFiles / gitChanged
|
Git state |
docChanged |
A saved document, for live preview refresh |
layout |
Layout algorithm change from a command |
progress / empty
|
Status feedback |
The webview HTML declares a strict CSP, with __CSP__ substituted at load time
by htmlForWebview() in util.ts:
script-src __CSP__;
worker-src blob:;
child-src blob:;
worker-src blob: is required because ELK runs in a Web Worker that Vite inlines
as a base64 blob. Without it, layout silently falls back to Dagre.