diff --git a/.gitignore b/.gitignore index c53c2817..ed5349ee 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ node_modules/ # Frontend build artifacts (generated by Vite — do not track in git) semantica/static/ + +# Local graph explorer test datasets +demo_out/ diff --git a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx index df30b0e5..e512e4bd 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx @@ -173,6 +173,14 @@ export function GraphInspectorPanel({ ); } + if (!graph.hasNode(nodeId)) { + return ( +
+ Selected item is not available for inspection in the current graph. +
+ ); + } + const attributes = graph.getNodeAttributes(nodeId) as { color?: string; content?: string; diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index b8e9c6b8..06183c7d 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -13,6 +13,7 @@ import { GraphLoadingOverlay } from "./GraphLoadingOverlay"; import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading"; import { GRAPH_THEME, withAlpha } from "./graphTheme"; import { resolveDisplayGraph, resolveDisplayStateSnapshot } from "./graphSceneState"; +import { computeGraphAnalyticsBase } from "./graphAnalytics"; import { type GraphPlugin, type GraphPluginActionRequest, @@ -844,9 +845,17 @@ export function GraphWorkspace() { }; }, [debouncedTime, isLoading]); - const focusNode = useCallback((nodeId: string) => { + const resolveNodeIdForFocusedMode = useCallback((nodeId: string): string | null => { + if (!nodeId) { + return null; + } + + if (graph.hasNode(nodeId)) { + return nodeId; + } + const currentDisplayGraph = pluginRuntimeRef.current?.displayGraph ?? graph; - if (viewMode === "grouped" && currentDisplayGraph.hasNode(nodeId)) { + if (currentDisplayGraph.hasNode(nodeId)) { const displayAttrs = currentDisplayGraph.getNodeAttributes(nodeId) as NodeAttributes; const communityGroup = displayAttrs.properties?.__communityGroup as | { @@ -854,19 +863,37 @@ export function GraphWorkspace() { sampleNodeIds?: string[]; } | undefined; + const anchorNodeId = communityGroup?.anchorNodeId || communityGroup?.sampleNodeIds?.[0] || ""; if (anchorNodeId && graph.hasNode(anchorNodeId)) { - setViewMode("focused"); - setSelectedNodeId(anchorNodeId); - setSelectedEdgeId(""); - setPathResult(null); - setSearchResults([]); - setSearchError(""); - setIsLayoutRunning(false); - return; + return anchorNodeId; } } + return null; + }, []); + + const activateFocusedMode = useCallback(() => { + const canonicalNodeId = resolveNodeIdForFocusedMode(selectedNodeId); + if (!canonicalNodeId) { + return; + } + + if (canonicalNodeId !== selectedNodeId) { + setSelectedNodeId(canonicalNodeId); + } + setViewMode("focused"); + }, [resolveNodeIdForFocusedMode, selectedNodeId]); + const [canActivateFocusedMode, setCanActivateFocusedMode] = useState(false); + useEffect(() => { + setCanActivateFocusedMode(Boolean(resolveNodeIdForFocusedMode(selectedNodeId))); + }, [pluginRuntimeVersion, resolveNodeIdForFocusedMode, selectedNodeId]); + + const focusNode = useCallback((nodeId: string) => { + if (!nodeId) { + return; + } + setSelectedNodeId(nodeId); setSelectedEdgeId(""); setPathResult(null); @@ -1041,15 +1068,21 @@ export function GraphWorkspace() { }, [collapsedNeighborhoodNodeIds, selectedNodeId, viewMode]); const structuralActivePath = structuralSelectedNodeId ? activePath : EMPTY_PATH; const structuralActivePathEdgeIds = structuralSelectedNodeId ? activePathEdgeIds : EMPTY_PATH; + const groupedViewAvailable = useMemo( + () => computeGraphAnalyticsBase(graph, { computeCommunities: true, computeCentrality: false }).communitiesByNode.size > 0, + [graphVersion], + ); const displayResult = useMemo( () => resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, { aggregationEnabled, collapsedNeighborhoodNodeIds, + groupedViewAvailable, }), [ aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion, + groupedViewAvailable, structuralActivePath, structuralActivePathEdgeIds, structuralSelectedNodeId, @@ -1060,8 +1093,9 @@ export function GraphWorkspace() { () => resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, { aggregationEnabled, collapsedNeighborhoodNodeIds, + groupedViewAvailable, }), - [activePath, aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion, selectedNodeId, viewMode], + [activePath, aggregationEnabled, collapsedNeighborhoodNodeIds, groupedViewAvailable, selectedNodeId, viewMode], ); const displayMeta = displayResult.meta; const previousDisplayGraphRef = useRef(displayResult.graph); @@ -1228,6 +1262,10 @@ export function GraphWorkspace() { focusNode(action.nodeId); return; case "setViewMode": + if (action.viewMode === "focused") { + activateFocusedMode(); + return; + } setViewMode(action.viewMode); return; case "collapseNeighborhood": @@ -1280,7 +1318,7 @@ export function GraphWorkspace() { setActiveDockPanelId((previous) => (previous === action.panelId ? null : previous)); return; } - }, [focusNode, selectedNodeId, setEffectToggle]); + }, [activateFocusedMode, focusNode, selectedNodeId, setEffectToggle]); const diagnosticsSnapshot = useMemo(() => { if (!GRAPH_THEME.effects.diagnostics.enabledInDev || !graphDiagnosticsState) { @@ -1476,8 +1514,8 @@ export function GraphWorkspace() { label: "Focused", title: "Inspect the selected node in a focused local graph", active: viewMode === "focused", - disabled: !selectedNodeId, - onClick: () => setViewMode("focused"), + disabled: viewMode !== "focused" && !canActivateFocusedMode, + onClick: activateFocusedMode, }, ], }); @@ -1567,6 +1605,8 @@ export function GraphWorkspace() { return groups; }, [ + activateFocusedMode, + canActivateFocusedMode, displayState.groupedViewAvailable, handlePluginAction, hasGraphContent, diff --git a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts index bea7f64c..c70bb734 100644 --- a/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts +++ b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts @@ -1242,6 +1242,7 @@ export function resolveDisplayStateSnapshot( options?: { aggregationEnabled?: boolean; collapsedNeighborhoodNodeIds?: Iterable; + groupedViewAvailable?: boolean; }, ): GraphDisplayStateSnapshot { const aggregationEnabled = options?.aggregationEnabled ?? true; @@ -1249,7 +1250,7 @@ export function resolveDisplayStateSnapshot( Array.from(options?.collapsedNeighborhoodNodeIds ?? []).filter((nodeId) => typeof nodeId === "string"), ); const displayState = createEmptyDisplayState(selectedNodeId, aggregationEnabled); - displayState.groupedViewAvailable = computeGraphAnalyticsBase(graph, { + displayState.groupedViewAvailable = options?.groupedViewAvailable ?? computeGraphAnalyticsBase(graph, { computeCommunities: true, computeCentrality: false, }).communitiesByNode.size > 0; @@ -1395,6 +1396,7 @@ export function resolveDisplayGraph( options?: { aggregationEnabled?: boolean; collapsedNeighborhoodNodeIds?: Iterable; + groupedViewAvailable?: boolean; }, ): GraphDisplayResult { const aggregationEnabled = options?.aggregationEnabled ?? true; @@ -1404,6 +1406,7 @@ export function resolveDisplayGraph( const displayState = resolveDisplayStateSnapshot(selectedNodeId, activePath, viewMode, { aggregationEnabled, collapsedNeighborhoodNodeIds, + groupedViewAvailable: options?.groupedViewAvailable, }); const isFocusedView = viewMode === "focused" && Boolean(selectedNodeId) && graph.hasNode(selectedNodeId); const isGroupedView = viewMode === "grouped"; diff --git a/plugins/.claude-plugin/README.md b/plugins/.claude-plugin/README.md index bdf90fcb..6fa56eef 100644 --- a/plugins/.claude-plugin/README.md +++ b/plugins/.claude-plugin/README.md @@ -91,7 +91,8 @@ claude --plugin-dir ./plugins Or inside a session: ```bash -/plugin install ./plugins +/plugin marketplace add ./plugins +/plugin install semantica@semantica-local ``` Verify: diff --git a/plugins/.claude-plugin/marketplace.json b/plugins/.claude-plugin/marketplace.json index a4e48a56..cbe8b924 100644 --- a/plugins/.claude-plugin/marketplace.json +++ b/plugins/.claude-plugin/marketplace.json @@ -1,5 +1,9 @@ { "name": "semantica-local", + "owner": { + "name": "Hawksight AI", + "url": "https://github.com/Hawksight-AI/semantica" + }, "plugins": [ { "name": "semantica", diff --git a/plugins/.claude-plugin/plugin.json b/plugins/.claude-plugin/plugin.json index 2f21a77d..fd5d35e7 100644 --- a/plugins/.claude-plugin/plugin.json +++ b/plugins/.claude-plugin/plugin.json @@ -25,6 +25,5 @@ "mcp" ], "skills": "./skills", - "agents": "./agents", - "hooks": "./hooks/hooks.json" + "agents": "./agents" } diff --git a/plugins/agents/decision-advisor/AGENT.md b/plugins/agents/decision-advisor.md similarity index 100% rename from plugins/agents/decision-advisor/AGENT.md rename to plugins/agents/decision-advisor.md diff --git a/plugins/agents/explainability/AGENT.md b/plugins/agents/explainability.md similarity index 100% rename from plugins/agents/explainability/AGENT.md rename to plugins/agents/explainability.md diff --git a/plugins/agents/kg-assistant/AGENT.md b/plugins/agents/kg-assistant.md similarity index 100% rename from plugins/agents/kg-assistant/AGENT.md rename to plugins/agents/kg-assistant.md