From da8509e2b1f5bb22199d1181583472a319d7a49a Mon Sep 17 00:00:00 2001 From: Kyle Miller Date: Mon, 16 Mar 2026 16:40:19 -0700 Subject: [PATCH] feat: diff mode added --- .../packages/dashboard/src/App.tsx | 29 +++++++++ .../dashboard/src/components/CustomNode.tsx | 12 ++++ .../dashboard/src/components/DiffToggle.tsx | 64 +++++++++++++++++++ .../dashboard/src/components/GraphView.tsx | 44 ++++++++++--- .../packages/dashboard/src/index.css | 22 +++++++ .../packages/dashboard/src/store.ts | 28 ++++++++ .../packages/dashboard/vite.config.ts | 20 ++++++ .../skills/understand-diff/SKILL.md | 13 ++++ 8 files changed, 222 insertions(+), 10 deletions(-) create mode 100644 understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx diff --git a/understand-anything-plugin/packages/dashboard/src/App.tsx b/understand-anything-plugin/packages/dashboard/src/App.tsx index 3d29e05..50df460 100644 --- a/understand-anything-plugin/packages/dashboard/src/App.tsx +++ b/understand-anything-plugin/packages/dashboard/src/App.tsx @@ -6,6 +6,7 @@ import CodeViewer from "./components/CodeViewer"; import SearchBar from "./components/SearchBar"; import NodeInfo from "./components/NodeInfo"; import LayerLegend from "./components/LayerLegend"; +import DiffToggle from "./components/DiffToggle"; import LearnPanel from "./components/LearnPanel"; import PersonaSelector from "./components/PersonaSelector"; import ProjectOverview from "./components/ProjectOverview"; @@ -18,6 +19,7 @@ function App() { const persona = useDashboardStore((s) => s.persona); const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen); const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer); + const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay); const [loadError, setLoadError] = useState(null); useEffect(() => { @@ -39,6 +41,32 @@ function App() { }); }, [setGraph]); + useEffect(() => { + fetch("/diff-overlay.json") + .then((res) => { + if (!res.ok) return null; + return res.json(); + }) + .then((data: unknown) => { + if ( + data && + typeof data === "object" && + "changedNodeIds" in data && + "affectedNodeIds" in data && + Array.isArray((data as Record).changedNodeIds) && + Array.isArray((data as Record).affectedNodeIds) + ) { + const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] }; + if (d.changedNodeIds.length > 0) { + setDiffOverlay(d.changedNodeIds, d.affectedNodeIds); + } + } + }) + .catch(() => { + // Silently ignore - diff overlay is optional + }); + }, [setDiffOverlay]); + // Determine sidebar content // Learn persona always shows LearnPanel; tour active overrides everything const sidebarContent = tourActive || persona === "junior" ? ( @@ -61,6 +89,7 @@ function App() {
+
diff --git a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx index 369105c..cf88c98 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx @@ -32,6 +32,9 @@ export interface CustomNodeData extends Record { searchScore?: number; isSelected: boolean; isTourHighlighted: boolean; + isDiffChanged: boolean; + isDiffAffected: boolean; + isDiffFaded: boolean; onNodeClick?: (nodeId: string) => void; } @@ -61,6 +64,15 @@ export default function CustomNode({ } } + // Diff overlay styling (composes with above) + if (data.isDiffChanged) { + extraClass += " ring-2 ring-[var(--color-diff-changed)] diff-changed-glow"; + } else if (data.isDiffAffected) { + extraClass += " ring-1 ring-[var(--color-diff-affected)] diff-affected-glow"; + } else if (data.isDiffFaded) { + extraClass += " diff-faded"; + } + const name = data.label ?? "unnamed"; const truncatedName = name.length > 24 ? name.slice(0, 22) + "..." : name; diff --git a/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx new file mode 100644 index 0000000..f912d1a --- /dev/null +++ b/understand-anything-plugin/packages/dashboard/src/components/DiffToggle.tsx @@ -0,0 +1,64 @@ +import { useDashboardStore } from "../store"; + +export default function DiffToggle() { + const diffMode = useDashboardStore((s) => s.diffMode); + const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode); + const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); + const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); + + const hasDiff = changedNodeIds.size > 0; + + return ( +
+ + + {diffMode && hasDiff && ( +
+
+ + + Changed + + ({changedNodeIds.size}) + + +
+
+ + + Affected + + ({affectedNodeIds.size}) + + +
+
+ )} +
+ ); +} diff --git a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx index f5df2c8..375a231 100644 --- a/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx +++ b/understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx @@ -30,6 +30,9 @@ export default function GraphView() { const showLayers = useDashboardStore((s) => s.showLayers); const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds); const persona = useDashboardStore((s) => s.persona); + const diffMode = useDashboardStore((s) => s.diffMode); + const changedNodeIds = useDashboardStore((s) => s.changedNodeIds); + const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds); const handleNodeSelect = useCallback( (nodeId: string) => { @@ -78,20 +81,41 @@ export default function GraphView() { searchScore: matchResult?.score, isSelected: selectedNodeId === node.id, isTourHighlighted: tourHighlightedNodeIds.includes(node.id), + isDiffChanged: diffMode && changedNodeIds.has(node.id), + isDiffAffected: diffMode && affectedNodeIds.has(node.id), + isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id), onNodeClick: handleNodeSelect, }, }; }); - const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => ({ - id: `e-${i}`, - source: edge.source, - target: edge.target, - label: edge.type, - animated: edge.type === "calls", - style: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 }, - labelStyle: { fill: "#a39787", fontSize: 10 }, - })); + const diffNodeIds = new Set([...changedNodeIds, ...affectedNodeIds]); + const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => { + const sourceInDiff = diffNodeIds.has(edge.source); + const targetInDiff = diffNodeIds.has(edge.target); + const isImpacted = diffMode && (sourceInDiff || targetInDiff); + + return { + id: `e-${i}`, + source: edge.source, + target: edge.target, + label: edge.type, + animated: edge.type === "calls" || isImpacted, + style: isImpacted + ? { + stroke: sourceInDiff && targetInDiff + ? "rgba(224, 82, 82, 0.7)" + : "rgba(212, 160, 48, 0.5)", + strokeWidth: 2.5, + } + : diffMode + ? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 } + : { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 }, + labelStyle: diffMode && !isImpacted + ? { fill: "rgba(163,151,135,0.3)", fontSize: 10 } + : { fill: "#a39787", fontSize: 10 }, + }; + }); // Run dagre layout on all nodes (without groups) const laid = applyDagreLayout(flowNodes, flowEdges); @@ -190,7 +214,7 @@ export default function GraphView() { ]; return { initialNodes: allNodes, initialEdges: laid.edges }; - }, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect]); + }, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); diff --git a/understand-anything-plugin/packages/dashboard/src/index.css b/understand-anything-plugin/packages/dashboard/src/index.css index d2ea312..a767a27 100644 --- a/understand-anything-plugin/packages/dashboard/src/index.css +++ b/understand-anything-plugin/packages/dashboard/src/index.css @@ -28,6 +28,12 @@ --color-node-module: #c9a06c; --color-node-concept: #b07a8a; + /* Diff overlay colors */ + --color-diff-changed: #e05252; + --color-diff-affected: #d4a030; + --color-diff-changed-dim: rgba(224, 82, 82, 0.25); + --color-diff-affected-dim: rgba(212, 160, 48, 0.25); + /* Fonts */ --font-serif: 'DM Serif Display', Georgia, serif; --font-mono: 'JetBrains Mono', 'Fira Code', monospace; @@ -113,6 +119,22 @@ body { box-shadow: 0 0 20px rgba(212, 165, 116, 0.15); } +/* Diff overlay glow effects */ +.diff-changed-glow { + box-shadow: 0 0 16px rgba(224, 82, 82, 0.25); +} + +.diff-affected-glow { + box-shadow: 0 0 12px rgba(212, 160, 48, 0.2); +} + +/* Diff fade for unrelated nodes */ +.diff-faded { + opacity: 0.25; + filter: saturate(0.3); + transition: opacity 0.3s ease, filter 0.3s ease; +} + /* Custom scrollbar for dark luxury theme */ ::-webkit-scrollbar { width: 6px; diff --git a/understand-anything-plugin/packages/dashboard/src/store.ts b/understand-anything-plugin/packages/dashboard/src/store.ts index eb66c8a..b01a15c 100644 --- a/understand-anything-plugin/packages/dashboard/src/store.ts +++ b/understand-anything-plugin/packages/dashboard/src/store.ts @@ -28,6 +28,10 @@ interface DashboardStore { persona: Persona; + diffMode: boolean; + changedNodeIds: Set; + affectedNodeIds: Set; + setGraph: (graph: KnowledgeGraph) => void; selectNode: (nodeId: string | null) => void; setSearchQuery: (query: string) => void; @@ -36,6 +40,10 @@ interface DashboardStore { openCodeViewer: (nodeId: string) => void; closeCodeViewer: () => void; + setDiffOverlay: (changed: string[], affected: string[]) => void; + toggleDiffMode: () => void; + clearDiffOverlay: () => void; + startTour: () => void; stopTour: () => void; setTourStep: (step: number) => void; @@ -67,6 +75,10 @@ export const useDashboardStore = create()((set, get) => ({ persona: "junior", + diffMode: false, + changedNodeIds: new Set(), + affectedNodeIds: new Set(), + setGraph: (graph) => { const searchEngine = new SearchEngine(graph.nodes); const query = get().searchQuery; @@ -96,6 +108,22 @@ export const useDashboardStore = create()((set, get) => ({ openCodeViewer: (nodeId) => set({ codeViewerOpen: true, codeViewerNodeId: nodeId }), closeCodeViewer: () => set({ codeViewerOpen: false, codeViewerNodeId: null }), + setDiffOverlay: (changed, affected) => + set({ + diffMode: true, + changedNodeIds: new Set(changed), + affectedNodeIds: new Set(affected), + }), + + toggleDiffMode: () => set((state) => ({ diffMode: !state.diffMode })), + + clearDiffOverlay: () => + set({ + diffMode: false, + changedNodeIds: new Set(), + affectedNodeIds: new Set(), + }), + startTour: () => { const { graph } = get(); if (!graph || !graph.tour || graph.tour.length === 0) return; diff --git a/understand-anything-plugin/packages/dashboard/vite.config.ts b/understand-anything-plugin/packages/dashboard/vite.config.ts index c05bc61..aa60f93 100644 --- a/understand-anything-plugin/packages/dashboard/vite.config.ts +++ b/understand-anything-plugin/packages/dashboard/vite.config.ts @@ -38,6 +38,26 @@ export default defineConfig({ } } } + if (req.url === "/diff-overlay.json") { + const graphDir = process.env.GRAPH_DIR; + const candidates = [ + ...(graphDir + ? [path.resolve(graphDir, ".understand-anything/diff-overlay.json")] + : []), + path.resolve(process.cwd(), ".understand-anything/diff-overlay.json"), + path.resolve(process.cwd(), "../../../.understand-anything/diff-overlay.json"), + ]; + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + res.setHeader("Content-Type", "application/json"); + fs.createReadStream(candidate).pipe(res); + return; + } + } + res.statusCode = 404; + res.end(); + return; + } next(); }); }, diff --git a/understand-anything-plugin/skills/understand-diff/SKILL.md b/understand-anything-plugin/skills/understand-diff/SKILL.md index 33adbfa..4f65df5 100644 --- a/understand-anything-plugin/skills/understand-diff/SKILL.md +++ b/understand-anything-plugin/skills/understand-diff/SKILL.md @@ -55,3 +55,16 @@ The knowledge graph JSON has this structure: - **Affected Layers**: Which architectural layers are touched and cross-layer concerns - **Risk Assessment**: Based on node `complexity` values, number of cross-layer edges, and blast radius (number of affected components) - Suggest what to review carefully and any potential issues + +8. **Write diff overlay for dashboard** — after producing the analysis, write the diff data to `.understand-anything/diff-overlay.json` so the dashboard can visualize changed and affected components. The file contains: + ```json + { + "version": "1.0.0", + "baseBranch": "", + "generatedAt": "", + "changedFiles": [""], + "changedNodeIds": [""], + "affectedNodeIds": [""] + } + ``` + After writing, tell the user they can run `/understand-anything:understand-dashboard` to see the diff overlay visually.