Skip to content
Merged
1 change: 1 addition & 0 deletions Makefile.cbm
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@ TEST_EXTRACTION_SRCS = \
tests/test_extraction.c \
tests/test_extraction_inheritance.c \
tests/test_extraction_imports.c \
tests/test_parse_coverage.c \
tests/test_grammar_regression.c \
tests/test_grammar_labels.c \
tests/test_grammar_imports.c \
Expand Down
33 changes: 33 additions & 0 deletions graph-ui/src/components/FilterPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ interface FilterPanelProps {
onToggleShowOnlyDead: () => void;
onToggleHideEntryPoints: () => void;
onToggleHideTests: () => void;
/* Missed skeleton (#963): white satellite of not-fully-indexed files */
missedView: boolean;
missedCount: number;
onToggleMissedView: () => void;
}

/* Checkbox row matching the existing "Show labels" toggle style */
Expand Down Expand Up @@ -76,6 +80,9 @@ export function FilterPanel({
onToggleShowOnlyDead,
onToggleHideEntryPoints,
onToggleHideTests,
missedView,
missedCount,
onToggleMissedView,
}: FilterPanelProps) {
const { labelCounts, edgeTypeCounts, statusCounts } = useMemo(() => {
const lc = new Map<string, number>();
Expand Down Expand Up @@ -163,6 +170,32 @@ export function FilterPanel({
</div>
</ScrollArea>

{/* Missed skeleton (#963): white satellite cluster of files the indexer
could not fully cover, shown beside the code galaxy. Click it to
focus; click the code galaxy to come back. */}
<div className="px-4 pt-2 border-t border-border/30 space-y-2 shrink-0">
<div className="flex items-center justify-between">
<span className="text-[10px] text-foreground/30 uppercase tracking-widest">
Missed files
</span>
{missedCount > 0 && (
<span className="text-[10px] text-foreground/50 tabular-nums">
{missedCount.toLocaleString()} files
</span>
)}
</div>
<CheckRow
checked={missedView}
onToggle={onToggleMissedView}
label="Show missed skeleton"
/>
<p className="text-[9px] leading-snug text-foreground/30">
{missedCount > 0
? "White satellite = files not fully indexed (best-effort). Click it to focus, click the galaxy to return."
: "No known misses (best-effort — not a completeness guarantee)."}
</p>
</div>

{/* Dead-code view */}
<div className="px-4 pt-2 border-t border-border/30 space-y-2 shrink-0">
<div className="flex items-center justify-between">
Expand Down
34 changes: 34 additions & 0 deletions graph-ui/src/components/GraphScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,31 @@ function IdleAutoRotate({

interface GraphSceneProps {
data: GraphData;
/* Missed skeleton (#963): pre-offset, pre-painted white nodes + edges of
* the not-fully-indexed files, rendered as a ghost cluster beside the
* galaxy. null hides it. */
missed?: { nodes: GraphNode[]; edges: GraphData["edges"] } | null;
highlightedIds: Set<number> | null;
cameraTarget: CameraTarget | null;
showLabels: boolean;
display?: DisplaySettings;
onNodeClick: (node: GraphNode) => void;
/* Fired when a click hits empty space (no node). Used to fly back to the
* overview after focusing the missed skeleton. */
onBackgroundClick?: () => void;
}

export type { CameraTarget };

export function GraphScene({
data,
missed = null,
highlightedIds,
cameraTarget,
showLabels,
display = DEFAULT_DISPLAY_SETTINGS,
onNodeClick,
onBackgroundClick,
}: GraphSceneProps) {
const [hovered, setHovered] = useState<GraphNode | null>(null);
const controlsRef = useRef<OrbitControlsImpl | null>(null);
Expand All @@ -151,6 +160,7 @@ export function GraphScene({
alpha: false,
powerPreference: "high-performance",
}}
onPointerMissed={onBackgroundClick}
>
<color attach="background" args={["#06090f"]} />
<ambientLight intensity={0.5} />
Expand All @@ -176,6 +186,30 @@ export function GraphScene({
/>
{showLabels && <NodeLabels nodes={data.nodes} highlightedIds={highlightedIds} />}

{/* Missed skeleton (#963): white ghost of the not-fully-indexed files.
* Clicks route through the same handler — GraphTab re-centers the
* camera on the whole skeleton cluster. */}
{missed && missed.nodes.length > 0 && (
<group>
<EdgeLines
nodes={missed.nodes}
edges={missed.edges}
highlightedIds={null}
opacity={0.28}
brightness={display.edgeBrightness}
/>
<NodeCloud
nodes={missed.nodes}
highlightedIds={null}
onHover={setHovered}
onClick={onNodeClick}
opacity={0.6}
boost={nodeBoost * 0.75}
/>
{showLabels && <NodeLabels nodes={missed.nodes} highlightedIds={null} />}
</group>
)}

{/* Satellite galaxies for cross-repo linked projects */}
{data.linked_projects?.map((lp: LinkedProject) => {
const offsetNodes = lp.nodes.map((n) => ({
Expand Down
110 changes: 96 additions & 14 deletions graph-ui/src/components/GraphTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
import { Sidebar } from "./Sidebar";
import { FilterPanel } from "./FilterPanel";
import { NodeDetailPanel } from "./NodeDetailPanel";
import { MissedCallout } from "./MissedCallout";
import { ResizeHandle } from "./ResizeHandle";
import { ErrorBoundary } from "./ErrorBoundary";
import type { GraphNode, GraphData, RepoInfo } from "../lib/types";
Expand Down Expand Up @@ -102,6 +103,12 @@ export function GraphTab({ project }: GraphTabProps) {
const [enabledLabels, setEnabledLabels] = useState<Set<string>>(new Set());
const [enabledEdgeTypes, setEnabledEdgeTypes] = useState<Set<string>>(new Set());

/* Missed skeleton (#963): the file structure of files the indexer could
* not fully cover, shown as a white satellite cluster beside the code
* galaxy. Toggle only hides/shows it — the data rides along with every
* code-graph layout. */
const [showMissedSkeleton, setShowMissedSkeleton] = useState(true);

/* Dead-code view: recolor by status + status-based filters */
const [deadCodeView, setDeadCodeView] = useState(false);
const [showOnlyDead, setShowOnlyDead] = useState(false);
Expand Down Expand Up @@ -190,6 +197,48 @@ export function GraphTab({ project }: GraphTabProps) {
}
}, [project, budget, fetchOverview]);

/* Missed skeleton: offset into place and paint white — a ghost of the
* files the graph could not fully cover, sitting beside the galaxy. */
const missedSkeleton = useMemo(() => {
const mg = data?.missed_graph;
if (!mg || mg.nodes.length === 0) return null;
const nodes = mg.nodes.map((n) => ({
...n,
x: n.x + mg.offset.x,
y: n.y + mg.offset.y,
z: n.z + mg.offset.z,
color: "#e9eef5",
}));
return { nodes, edges: mg.edges, ids: new Set(nodes.map((n) => n.id)) };
}, [data]);

/* Overview framing: both clusters (galaxy + skeleton) in one shot. */
const overviewTarget = useMemo(() => {
if (!data) return null;
const all = missedSkeleton ? [...data.nodes, ...missedSkeleton.nodes] : data.nodes;
return computeCameraTarget(all, new Set(all.map((n) => n.id)));
}, [data, missedSkeleton]);

/* With a skeleton beside the galaxy, auto-frame BOTH clusters on load so
* the side-by-side composition is visible without manual zooming. */
useEffect(() => {
if (missedSkeleton && overviewTarget) {
setCameraTarget(overviewTarget);
}
}, [missedSkeleton, overviewTarget]);

/* Clicking empty space while the skeleton has focus flies back to the
* overview (the galaxy may be entirely off-screen at that point, so there
* is no code node to click). No-op during normal galaxy exploration. */
const handleBackgroundClick = useCallback(() => {
if (selectedNode && missedSkeleton?.ids.has(selectedNode.id) && overviewTarget) {
setSelectedNode(null);
setHighlightedIds(null);
setSelectedPath(null);
setCameraTarget(overviewTarget);
}
}, [selectedNode, missedSkeleton, overviewTarget]);

/* Fetch git remote metadata for GitHub deep-links */
useEffect(() => {
if (!project) {
Expand Down Expand Up @@ -226,6 +275,19 @@ export function GraphTab({ project }: GraphTabProps) {
const handleNodeClick = useCallback(
(node: GraphNode) => {
if (!filteredData) return;

/* Clicking the missed skeleton re-centers the camera on that whole
* cluster (it's small — the natural focus unit is the skeleton, not a
* single node); clicking any code node flies back to the code galaxy
* via the normal per-node focus below. */
if (missedSkeleton?.ids.has(node.id)) {
setSelectedNode(node);
setHighlightedIds(null);
setSelectedPath(node.file_path ?? null);
setCameraTarget(computeCameraTarget(missedSkeleton.nodes, missedSkeleton.ids));
return;
}

setSelectedNode(node);

/* Highlight the node and its direct connections */
Expand All @@ -238,7 +300,7 @@ export function GraphTab({ project }: GraphTabProps) {
setSelectedPath(node.file_path ?? null);
setCameraTarget(computeCameraTarget(filteredData.nodes, connectedIds));
},
[filteredData],
[filteredData, missedSkeleton],
);

const handleNavigateToNode = useCallback(
Expand Down Expand Up @@ -351,6 +413,9 @@ export function GraphTab({ project }: GraphTabProps) {
onToggleShowOnlyDead={() => setShowOnlyDead((v) => !v)}
onToggleHideEntryPoints={() => setHideEntryPoints((v) => !v)}
onToggleHideTests={() => setHideTests((v) => !v)}
missedView={showMissedSkeleton}
missedCount={data?.missed_graph?.nodes.filter((n) => n.label === "File").length ?? 0}
onToggleMissedView={() => setShowMissedSkeleton((v) => !v)}
/>
<Sidebar
nodes={filteredData.nodes}
Expand Down Expand Up @@ -385,11 +450,13 @@ export function GraphTab({ project }: GraphTabProps) {
<ErrorBoundary>
<GraphScene
data={filteredData}
missed={showMissedSkeleton ? missedSkeleton : null}
highlightedIds={highlightedIds}
cameraTarget={cameraTarget}
showLabels={showLabels}
display={display}
onNodeClick={handleNodeClick}
onBackgroundClick={handleBackgroundClick}
/>
</ErrorBoundary>

Expand Down Expand Up @@ -490,19 +557,34 @@ export function GraphTab({ project }: GraphTabProps) {
className="border-l border-border shrink-0 h-full overflow-hidden"
style={{ width: rightWidth, maxHeight: "100%" }}
>
<NodeDetailPanel
node={selectedNode}
allNodes={filteredData.nodes}
allEdges={filteredData.edges}
project={project}
repoInfo={repoInfo}
onClose={() => {
setSelectedNode(null);
setHighlightedIds(null);
setSelectedPath(null);
}}
onNavigate={handleNavigateToNode}
/>
{missedSkeleton?.ids.has(selectedNode.id) ? (
/* Skeleton node: the standard panel (code snippet, callers) is
* meaningless for a not-fully-indexed file — show the coverage
* callout with its report-the-edge-case actions instead. */
<MissedCallout
node={selectedNode}
project={project}
onClose={() => {
setSelectedNode(null);
setHighlightedIds(null);
setSelectedPath(null);
}}
/>
) : (
<NodeDetailPanel
node={selectedNode}
allNodes={filteredData.nodes}
allEdges={filteredData.edges}
project={project}
repoInfo={repoInfo}
onClose={() => {
setSelectedNode(null);
setHighlightedIds(null);
setSelectedPath(null);
}}
onNavigate={handleNavigateToNode}
/>
)}
</div>
</>
)}
Expand Down
Loading
Loading