feat(ui): the File view — outline in source order between two dependency rails (CG-46)

Clicking a file path now opens the file itself: what reaches into it, its
symbols in source order, and what it reaches.

The two rails count DEPENDENCIES, not import statements. The prototype drew
`imports` edges; on this repo `src/graph/traversal.ts` imports two files and
depends on four, because it reaches the LRU cache through a call no import
names. A rail headed "Imports 2" would be quietly wrong about what changing the
file would touch, which is the only question the screen answers — so the rails
read `getFileDependencies` / `getFileDependents` and merge the import rows in
for the symbol names. Imports that resolved to nothing indexed keep their own
section rather than vanishing.

The outline is windowed above 250 rows against a fixed 28px row: this repo's
own fixtures hold a 1,681-symbol `.d.ts`, and paging it would hide the one
thing an outline is for. `src/mcp/tools.ts` draws its 135 rows whole.

`/api/file` gains `topLevel.calls` — module-level calls out of the file node —
so a file that RUNS something offers the badge that opens it as a symbol, the
only place code belonging to no symbol can be read.

File results in the search palette and the entry-point list now land here
rather than on the file node's Symbol view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 01:43:00 -05:00
co-authored by Claude Opus 5
parent 2ad836d935
commit 58dad12f89
11 changed files with 1454 additions and 11 deletions
+18
View File
@@ -127,6 +127,16 @@ export function buildFile(cg: CodeGraph, projectRoot: string, requested: string)
// records a file-level import.
const unresolvedImports = fileNode ? unresolvedImportsOf(cg, fileNode.id) : [];
// Whether the file RUNS anything at its top level. Extraction records a
// statement outside any definition as an edge out of the FILE node, so a
// module that only defines things has none and a CLI entry point has many —
// the same signal `/api/entrypoints` ranks on. It is worth a line on this
// screen because the outline cannot show it: top-level code belongs to no
// symbol, so the only way to read it is to open the file node itself.
const topLevelEdges = fileNode
? cg.getOutgoingEdgesFrom([fileNode.id], ['calls', 'instantiates'])
: [];
return {
file: {
path: toPosixPath(storedPath),
@@ -142,6 +152,14 @@ export function buildFile(cg: CodeGraph, projectRoot: string, requested: string)
/** The file node itself, so the viewer can navigate to it as a symbol. */
id: fileNode?.id ?? null,
},
/**
* Calls made at the top level of the file, outside every definition.
* Counted as distinct call SITES — `(target, line, column)` — so a call
* two resolvers both recorded is one thing to read, not two.
*/
topLevel: {
calls: new Set(topLevelEdges.map((e) => `${e.target}:${e.line ?? 0}:${e.column ?? 0}`)).size,
},
/** The file changed on disk since it was indexed — the outline's lines may be shifted. */
drift: hasDriftedOnDisk(projectRoot, storedPath, record),
outline: wireList(outline, outlineNodes.length),