Skip to content

Commit da8509e

Browse files
committed
feat: diff mode added
1 parent b82efea commit da8509e

8 files changed

Lines changed: 222 additions & 10 deletions

File tree

understand-anything-plugin/packages/dashboard/src/App.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import CodeViewer from "./components/CodeViewer";
66
import SearchBar from "./components/SearchBar";
77
import NodeInfo from "./components/NodeInfo";
88
import LayerLegend from "./components/LayerLegend";
9+
import DiffToggle from "./components/DiffToggle";
910
import LearnPanel from "./components/LearnPanel";
1011
import PersonaSelector from "./components/PersonaSelector";
1112
import ProjectOverview from "./components/ProjectOverview";
@@ -18,6 +19,7 @@ function App() {
1819
const persona = useDashboardStore((s) => s.persona);
1920
const codeViewerOpen = useDashboardStore((s) => s.codeViewerOpen);
2021
const closeCodeViewer = useDashboardStore((s) => s.closeCodeViewer);
22+
const setDiffOverlay = useDashboardStore((s) => s.setDiffOverlay);
2123
const [loadError, setLoadError] = useState<string | null>(null);
2224

2325
useEffect(() => {
@@ -39,6 +41,32 @@ function App() {
3941
});
4042
}, [setGraph]);
4143

44+
useEffect(() => {
45+
fetch("/diff-overlay.json")
46+
.then((res) => {
47+
if (!res.ok) return null;
48+
return res.json();
49+
})
50+
.then((data: unknown) => {
51+
if (
52+
data &&
53+
typeof data === "object" &&
54+
"changedNodeIds" in data &&
55+
"affectedNodeIds" in data &&
56+
Array.isArray((data as Record<string, unknown>).changedNodeIds) &&
57+
Array.isArray((data as Record<string, unknown>).affectedNodeIds)
58+
) {
59+
const d = data as { changedNodeIds: string[]; affectedNodeIds: string[] };
60+
if (d.changedNodeIds.length > 0) {
61+
setDiffOverlay(d.changedNodeIds, d.affectedNodeIds);
62+
}
63+
}
64+
})
65+
.catch(() => {
66+
// Silently ignore - diff overlay is optional
67+
});
68+
}, [setDiffOverlay]);
69+
4270
// Determine sidebar content
4371
// Learn persona always shows LearnPanel; tour active overrides everything
4472
const sidebarContent = tourActive || persona === "junior" ? (
@@ -61,6 +89,7 @@ function App() {
6189
<PersonaSelector />
6290
</div>
6391
<div className="flex items-center gap-4">
92+
<DiffToggle />
6493
<LayerLegend />
6594
</div>
6695
</header>

understand-anything-plugin/packages/dashboard/src/components/CustomNode.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export interface CustomNodeData extends Record<string, unknown> {
3232
searchScore?: number;
3333
isSelected: boolean;
3434
isTourHighlighted: boolean;
35+
isDiffChanged: boolean;
36+
isDiffAffected: boolean;
37+
isDiffFaded: boolean;
3538
onNodeClick?: (nodeId: string) => void;
3639
}
3740

@@ -61,6 +64,15 @@ export default function CustomNode({
6164
}
6265
}
6366

67+
// Diff overlay styling (composes with above)
68+
if (data.isDiffChanged) {
69+
extraClass += " ring-2 ring-[var(--color-diff-changed)] diff-changed-glow";
70+
} else if (data.isDiffAffected) {
71+
extraClass += " ring-1 ring-[var(--color-diff-affected)] diff-affected-glow";
72+
} else if (data.isDiffFaded) {
73+
extraClass += " diff-faded";
74+
}
75+
6476
const name = data.label ?? "unnamed";
6577
const truncatedName =
6678
name.length > 24 ? name.slice(0, 22) + "..." : name;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useDashboardStore } from "../store";
2+
3+
export default function DiffToggle() {
4+
const diffMode = useDashboardStore((s) => s.diffMode);
5+
const toggleDiffMode = useDashboardStore((s) => s.toggleDiffMode);
6+
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
7+
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
8+
9+
const hasDiff = changedNodeIds.size > 0;
10+
11+
return (
12+
<div className="flex items-center gap-2">
13+
<button
14+
onClick={toggleDiffMode}
15+
disabled={!hasDiff}
16+
className={`px-2 py-0.5 rounded text-[11px] font-medium transition-colors ${
17+
diffMode && hasDiff
18+
? "bg-[var(--color-diff-changed-dim)] text-[var(--color-diff-changed)]"
19+
: hasDiff
20+
? "bg-elevated text-text-secondary hover:bg-surface"
21+
: "bg-elevated text-text-muted cursor-not-allowed"
22+
}`}
23+
title={
24+
hasDiff
25+
? diffMode
26+
? "Hide diff overlay"
27+
: "Show diff overlay"
28+
: "No diff data loaded"
29+
}
30+
>
31+
Diff {diffMode && hasDiff ? "ON" : "OFF"}
32+
</button>
33+
34+
{diffMode && hasDiff && (
35+
<div className="flex items-center gap-3">
36+
<div className="flex items-center gap-1">
37+
<span
38+
className="inline-block w-2 h-2 rounded-full"
39+
style={{ backgroundColor: "var(--color-diff-changed)" }}
40+
/>
41+
<span className="text-text-secondary text-[11px]">
42+
Changed
43+
<span className="text-text-muted ml-0.5">
44+
({changedNodeIds.size})
45+
</span>
46+
</span>
47+
</div>
48+
<div className="flex items-center gap-1">
49+
<span
50+
className="inline-block w-2 h-2 rounded-full"
51+
style={{ backgroundColor: "var(--color-diff-affected)" }}
52+
/>
53+
<span className="text-text-secondary text-[11px]">
54+
Affected
55+
<span className="text-text-muted ml-0.5">
56+
({affectedNodeIds.size})
57+
</span>
58+
</span>
59+
</div>
60+
</div>
61+
)}
62+
</div>
63+
);
64+
}

understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export default function GraphView() {
3030
const showLayers = useDashboardStore((s) => s.showLayers);
3131
const tourHighlightedNodeIds = useDashboardStore((s) => s.tourHighlightedNodeIds);
3232
const persona = useDashboardStore((s) => s.persona);
33+
const diffMode = useDashboardStore((s) => s.diffMode);
34+
const changedNodeIds = useDashboardStore((s) => s.changedNodeIds);
35+
const affectedNodeIds = useDashboardStore((s) => s.affectedNodeIds);
3336

3437
const handleNodeSelect = useCallback(
3538
(nodeId: string) => {
@@ -78,20 +81,41 @@ export default function GraphView() {
7881
searchScore: matchResult?.score,
7982
isSelected: selectedNodeId === node.id,
8083
isTourHighlighted: tourHighlightedNodeIds.includes(node.id),
84+
isDiffChanged: diffMode && changedNodeIds.has(node.id),
85+
isDiffAffected: diffMode && affectedNodeIds.has(node.id),
86+
isDiffFaded: diffMode && !changedNodeIds.has(node.id) && !affectedNodeIds.has(node.id),
8187
onNodeClick: handleNodeSelect,
8288
},
8389
};
8490
});
8591

86-
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => ({
87-
id: `e-${i}`,
88-
source: edge.source,
89-
target: edge.target,
90-
label: edge.type,
91-
animated: edge.type === "calls",
92-
style: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
93-
labelStyle: { fill: "#a39787", fontSize: 10 },
94-
}));
92+
const diffNodeIds = new Set([...changedNodeIds, ...affectedNodeIds]);
93+
const flowEdges: Edge[] = filteredGraphEdges.map((edge, i) => {
94+
const sourceInDiff = diffNodeIds.has(edge.source);
95+
const targetInDiff = diffNodeIds.has(edge.target);
96+
const isImpacted = diffMode && (sourceInDiff || targetInDiff);
97+
98+
return {
99+
id: `e-${i}`,
100+
source: edge.source,
101+
target: edge.target,
102+
label: edge.type,
103+
animated: edge.type === "calls" || isImpacted,
104+
style: isImpacted
105+
? {
106+
stroke: sourceInDiff && targetInDiff
107+
? "rgba(224, 82, 82, 0.7)"
108+
: "rgba(212, 160, 48, 0.5)",
109+
strokeWidth: 2.5,
110+
}
111+
: diffMode
112+
? { stroke: "rgba(212,165,116,0.08)", strokeWidth: 1 }
113+
: { stroke: "rgba(212,165,116,0.3)", strokeWidth: 1.5 },
114+
labelStyle: diffMode && !isImpacted
115+
? { fill: "rgba(163,151,135,0.3)", fontSize: 10 }
116+
: { fill: "#a39787", fontSize: 10 },
117+
};
118+
});
95119

96120
// Run dagre layout on all nodes (without groups)
97121
const laid = applyDagreLayout(flowNodes, flowEdges);
@@ -190,7 +214,7 @@ export default function GraphView() {
190214
];
191215

192216
return { initialNodes: allNodes, initialEdges: laid.edges };
193-
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect]);
217+
}, [graph, searchResults, selectedNodeId, showLayers, tourHighlightedNodeIds, persona, handleNodeSelect, diffMode, changedNodeIds, affectedNodeIds]);
194218

195219
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
196220
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

understand-anything-plugin/packages/dashboard/src/index.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828
--color-node-module: #c9a06c;
2929
--color-node-concept: #b07a8a;
3030

31+
/* Diff overlay colors */
32+
--color-diff-changed: #e05252;
33+
--color-diff-affected: #d4a030;
34+
--color-diff-changed-dim: rgba(224, 82, 82, 0.25);
35+
--color-diff-affected-dim: rgba(212, 160, 48, 0.25);
36+
3137
/* Fonts */
3238
--font-serif: 'DM Serif Display', Georgia, serif;
3339
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
@@ -113,6 +119,22 @@ body {
113119
box-shadow: 0 0 20px rgba(212, 165, 116, 0.15);
114120
}
115121

122+
/* Diff overlay glow effects */
123+
.diff-changed-glow {
124+
box-shadow: 0 0 16px rgba(224, 82, 82, 0.25);
125+
}
126+
127+
.diff-affected-glow {
128+
box-shadow: 0 0 12px rgba(212, 160, 48, 0.2);
129+
}
130+
131+
/* Diff fade for unrelated nodes */
132+
.diff-faded {
133+
opacity: 0.25;
134+
filter: saturate(0.3);
135+
transition: opacity 0.3s ease, filter 0.3s ease;
136+
}
137+
116138
/* Custom scrollbar for dark luxury theme */
117139
::-webkit-scrollbar {
118140
width: 6px;

understand-anything-plugin/packages/dashboard/src/store.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ interface DashboardStore {
2828

2929
persona: Persona;
3030

31+
diffMode: boolean;
32+
changedNodeIds: Set<string>;
33+
affectedNodeIds: Set<string>;
34+
3135
setGraph: (graph: KnowledgeGraph) => void;
3236
selectNode: (nodeId: string | null) => void;
3337
setSearchQuery: (query: string) => void;
@@ -36,6 +40,10 @@ interface DashboardStore {
3640
openCodeViewer: (nodeId: string) => void;
3741
closeCodeViewer: () => void;
3842

43+
setDiffOverlay: (changed: string[], affected: string[]) => void;
44+
toggleDiffMode: () => void;
45+
clearDiffOverlay: () => void;
46+
3947
startTour: () => void;
4048
stopTour: () => void;
4149
setTourStep: (step: number) => void;
@@ -67,6 +75,10 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
6775

6876
persona: "junior",
6977

78+
diffMode: false,
79+
changedNodeIds: new Set<string>(),
80+
affectedNodeIds: new Set<string>(),
81+
7082
setGraph: (graph) => {
7183
const searchEngine = new SearchEngine(graph.nodes);
7284
const query = get().searchQuery;
@@ -96,6 +108,22 @@ export const useDashboardStore = create<DashboardStore>()((set, get) => ({
96108
openCodeViewer: (nodeId) => set({ codeViewerOpen: true, codeViewerNodeId: nodeId }),
97109
closeCodeViewer: () => set({ codeViewerOpen: false, codeViewerNodeId: null }),
98110

111+
setDiffOverlay: (changed, affected) =>
112+
set({
113+
diffMode: true,
114+
changedNodeIds: new Set(changed),
115+
affectedNodeIds: new Set(affected),
116+
}),
117+
118+
toggleDiffMode: () => set((state) => ({ diffMode: !state.diffMode })),
119+
120+
clearDiffOverlay: () =>
121+
set({
122+
diffMode: false,
123+
changedNodeIds: new Set<string>(),
124+
affectedNodeIds: new Set<string>(),
125+
}),
126+
99127
startTour: () => {
100128
const { graph } = get();
101129
if (!graph || !graph.tour || graph.tour.length === 0) return;

understand-anything-plugin/packages/dashboard/vite.config.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,26 @@ export default defineConfig({
3838
}
3939
}
4040
}
41+
if (req.url === "/diff-overlay.json") {
42+
const graphDir = process.env.GRAPH_DIR;
43+
const candidates = [
44+
...(graphDir
45+
? [path.resolve(graphDir, ".understand-anything/diff-overlay.json")]
46+
: []),
47+
path.resolve(process.cwd(), ".understand-anything/diff-overlay.json"),
48+
path.resolve(process.cwd(), "../../../.understand-anything/diff-overlay.json"),
49+
];
50+
for (const candidate of candidates) {
51+
if (fs.existsSync(candidate)) {
52+
res.setHeader("Content-Type", "application/json");
53+
fs.createReadStream(candidate).pipe(res);
54+
return;
55+
}
56+
}
57+
res.statusCode = 404;
58+
res.end();
59+
return;
60+
}
4161
next();
4262
});
4363
},

understand-anything-plugin/skills/understand-diff/SKILL.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,16 @@ The knowledge graph JSON has this structure:
5555
- **Affected Layers**: Which architectural layers are touched and cross-layer concerns
5656
- **Risk Assessment**: Based on node `complexity` values, number of cross-layer edges, and blast radius (number of affected components)
5757
- Suggest what to review carefully and any potential issues
58+
59+
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:
60+
```json
61+
{
62+
"version": "1.0.0",
63+
"baseBranch": "<the base branch used>",
64+
"generatedAt": "<ISO timestamp>",
65+
"changedFiles": ["<list of changed file paths>"],
66+
"changedNodeIds": ["<node IDs from step 4>"],
67+
"affectedNodeIds": ["<node IDs from step 5, excluding changedNodeIds>"]
68+
}
69+
```
70+
After writing, tell the user they can run `/understand-anything:understand-dashboard` to see the diff overlay visually.

0 commit comments

Comments
 (0)