Просмотр исходного кода

feat: Improve visualizer graph quality and UI

- Bridge pass: connect isolated seed nodes that share callees
- Kind labels on nodes (fn, class, comp, etc.) for quick identification
- Quick action buttons in detail panel (Expand Callees, Callers, Call Graph, Impact)
- Wider detail panel (460px) for better code readability
- Multiline node labels showing name + kind

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Colby McHenry 3 месяцев назад
Родитель
Сommit
ba30c74461
2 измененных файлов с 70 добавлено и 8 удалено
  1. 22 4
      src/visualizer/public/index.html
  2. 48 4
      src/visualizer/server.ts

+ 22 - 4
src/visualizer/public/index.html

@@ -42,7 +42,7 @@
       --font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', Consolas, monospace;
       --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
       --sidebar-width: 320px;
-      --panel-width: 420px;
+      --panel-width: 460px;
       --header-height: 52px;
       --radius: 8px;
       --radius-sm: 6px;
@@ -991,8 +991,8 @@
               'height': 'label',
               'padding': '12px',
               'shape': 'data(shape)',
-              'text-wrap': 'none',
-              'text-max-width': '160px',
+              'text-wrap': 'wrap',
+              'text-max-width': '180px',
               'transition-property': 'background-opacity, border-color, border-opacity, opacity, text-opacity',
               'transition-duration': '0.2s',
             }
@@ -1130,16 +1130,24 @@
     // ====================================================================
     // Graph Operations
     // ====================================================================
+    const kindLabels = {
+      'function': 'fn', 'method': 'method', 'class': 'class', 'interface': 'iface',
+      'component': 'comp', 'route': 'route', 'enum': 'enum', 'type_alias': 'type',
+      'struct': 'struct', 'trait': 'trait', 'variable': 'var', 'constant': 'const',
+      'property': 'prop', 'field': 'field', 'file': 'file', 'module': 'mod',
+    };
+
     function addNodeToGraph(node) {
       if (cy.getElementById(node.id).length > 0) return;
       const color = kindColors[node.kind] || '#8b949e';
       const shape = kindShapes[node.kind] || 'round-rectangle';
+      const kindLabel = kindLabels[node.kind] || node.kind;
       cy.add({
         group: 'nodes',
         data: {
           id: node.id,
           nodeId: node.id,
-          label: node.name,
+          label: `${node.name}\n${kindLabel}`,
           color: color,
           shape: shape,
           kind: node.kind,
@@ -1541,6 +1549,16 @@
 
         let html = '';
 
+        // Quick actions
+        html += `<div class="detail-section" style="padding:8px 16px;">
+          <div style="display:flex;gap:6px;flex-wrap:wrap;">
+            <button class="toolbar-btn" onclick="expandCallees('${escapeAttr(node.id)}')" style="font-size:12px;">Expand Callees &rarr;</button>
+            <button class="toolbar-btn" onclick="expandCallers('${escapeAttr(node.id)}')" style="font-size:12px;">&larr; Expand Callers</button>
+            <button class="toolbar-btn" onclick="loadCallGraph('${escapeAttr(node.id)}')" style="font-size:12px;">Full Call Graph</button>
+            <button class="toolbar-btn" onclick="loadImpact('${escapeAttr(node.id)}')" style="font-size:12px;">Impact Analysis</button>
+          </div>
+        </div>`;
+
         // Meta info
         html += `<div class="detail-section">
           <div class="detail-section-title">Info</div>

+ 48 - 4
src/visualizer/server.ts

@@ -397,11 +397,55 @@ ${symbolIndex}`;
             addEdge(item.edge);
           }
 
-          // Seeds with no relevant neighbors stay isolated — user can
-          // right-click → expand to explore manually. No noise added.
+          // For seeds with no relevant neighbors, add top-3 callees
+          // so they're not completely floating
+          if (relevant.length === 0 && irrelevant.length > 0) {
+            for (const item of irrelevant.slice(0, 3)) {
+              if (nodeMap.size >= maxNodes) break;
+              // Only add if it connects to another node already in the graph
+              if (nodeMap.has(item.node.id)) {
+                addEdge(item.edge);
+              }
+            }
+          }
+        }
+
+        // Step 3: Bridge pass — find shared callees between isolated seeds
+        // If two seeds both call the same function, add it as a bridge node
+        const isolatedSeeds = Array.from(seedMap.keys()).filter(id => {
+          return !edgeList.some(e => e.source === id || e.target === id);
+        });
+
+        if (isolatedSeeds.length > 1) {
+          // Collect callees for each isolated seed
+          const seedCallees = new Map<string, { node: Node; seeds: string[] }>();
+          for (const seedId of isolatedSeeds) {
+            const callees = this.cg.getCallees(seedId, 1);
+            for (const item of callees) {
+              const existing = seedCallees.get(item.node.id);
+              if (existing) {
+                existing.seeds.push(seedId);
+              } else {
+                seedCallees.set(item.node.id, { node: item.node, seeds: [seedId] });
+              }
+            }
+          }
+          // Add bridge nodes that connect 2+ isolated seeds
+          for (const [bridgeId, { node: bridgeNode, seeds }] of seedCallees) {
+            if (seeds.length >= 2 && nodeMap.size < maxNodes) {
+              nodeMap.set(bridgeId, bridgeNode);
+              // Add edges from each seed to the bridge
+              for (const seedId of seeds) {
+                const callees = this.cg.getCallees(seedId, 1);
+                for (const item of callees) {
+                  if (item.node.id === bridgeId) addEdge(item.edge);
+                }
+              }
+            }
+          }
         }
 
-        // Step 3: Cross-connection pass — find edges between all result nodes
+        // Step 4: Cross-connection pass — find edges between all result nodes
         for (const [nodeId] of nodeMap) {
           const callers = this.cg.getCallers(nodeId, 1);
           const callees = this.cg.getCallees(nodeId, 1);
@@ -412,7 +456,7 @@ ${symbolIndex}`;
           }
         }
 
-        // Step 4: Filter edges and remove isolated non-root nodes
+        // Step 5: Filter edges and remove isolated non-root nodes
         const finalEdges = edgeList.filter(e => nodeMap.has(e.source) && nodeMap.has(e.target));
 
         const connectedIds = new Set<string>();