feat(ui): the Symbol view — callers, gutter-ported source, line-anchored callee rail (CG-44)
The core screen of `codegraph ui`: who calls a symbol on the left, its verbatim body in the middle with a port on every line that has an outgoing edge, and what it calls on the right — each callee row placed beside the line that makes the call, with a hairline connector between them. The callee rail is the part that is not a list. A row wants to sit at the centre of its first call-site line and is pushed down only when that would collide with the row above, so the rail keeps source order; the connector still runs to the real line, so the displacement is visible rather than silent. Positions come from measuring the laid-out DOM, so they are recomputed on resize, on font load and whenever a fold opens. Honesty is carried in the drawing, not in a footnote: a filled port means the resolver matched something on that line and a hollow one means it only guessed; uncertain connectors are dashed and their targets fold away behind their count; synthesized edges are dashed differently and tagged with the mechanism that made them; references that leave the index are text with a soft underline rather than links to nowhere, and they are counted. Long bodies keep their head plus a window round every call site — windowed on graph edges only, since a function calling `console.log` two hundred times would otherwise window round every line and buy nothing. Containers over 80 lines show a members outline with per-member fan-in/fan-out instead of 700 lines of braces. Two small additions to the read-only API this needed: * `/api/node` gives every outline member its own fanIn/fanOut (two batched queries for the whole outline). A class's own fan-out is nearly always zero because its methods do the calling, so without these the outline cannot say which member carries weight. * `/api/stats` gains `blastScale` — the denominator the blast bar is drawn against, so one symbol's radius reads as wide or narrow *for this repo*. It is measured across the index's 24 most-depended-on symbols (found with a new `getTopDependedOn`, distinct dependents rather than edges), memoised against the index stamp, and reported as sampled; a symbol wider than the sample becomes the scale instead of overflowing the track. Verified against a real index in a real browser: parity with the prototype on `CodeGraph.sync` (259 lines, 27 callee rows, no overlaps), `GraphTraverser` (20-member outline), a 773-line function (26 windows, 78 connectors), light and dark, hover linking in both directions, keyboard-only navigation, and reflow on resize and on fold toggles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7288ffa36
commit
5cecaabfc2
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* The project's own facts — loaded once, read everywhere.
|
||||
*
|
||||
* `/api/stats` describes the index rather than any one symbol, so every screen
|
||||
* that needs a piece of it (the top bar's counts, the Symbol view's blast
|
||||
* scale) would otherwise re-fetch the same payload. The promise is memoised,
|
||||
* not the value, so callers made before it lands still get the same request.
|
||||
*/
|
||||
|
||||
import { fetchStats, type WireStats } from './api';
|
||||
|
||||
let stats = $state<WireStats | null>(null);
|
||||
let error = $state<string | null>(null);
|
||||
let inflight: Promise<void> | null = null;
|
||||
|
||||
function load(): Promise<void> {
|
||||
if (inflight) return inflight;
|
||||
inflight = fetchStats()
|
||||
.then((value) => {
|
||||
stats = value;
|
||||
error = null;
|
||||
})
|
||||
.catch((cause: unknown) => {
|
||||
// A failure here costs a couple of numbers in the top bar and the blast
|
||||
// bar's denominator — never the screen. It is recorded, not thrown.
|
||||
error = cause instanceof Error ? cause.message : String(cause);
|
||||
});
|
||||
return inflight;
|
||||
}
|
||||
|
||||
export const project = {
|
||||
get stats(): WireStats | null {
|
||||
return stats;
|
||||
},
|
||||
get error(): string | null {
|
||||
return error;
|
||||
},
|
||||
/** "codegraph" — the indexed project's directory name. */
|
||||
get name(): string | null {
|
||||
return stats?.project.name ?? null;
|
||||
},
|
||||
/** "13,495 symbols · 47,433 edges · 632 files indexed". */
|
||||
get summary(): string | null {
|
||||
if (!stats) return null;
|
||||
const n = (value: number): string => value.toLocaleString();
|
||||
return `${n(stats.graph.nodes)} symbols · ${n(stats.graph.edges)} edges · ${n(stats.graph.files)} files indexed`;
|
||||
},
|
||||
ensure: load,
|
||||
};
|
||||
Reference in New Issue
Block a user