From bb0e9f49e3572b299f4fc907a775febbe52ef300 Mon Sep 17 00:00:00 2001 From: Sameer6305 Date: Thu, 23 Apr 2026 00:11:38 +0530 Subject: [PATCH 1/5] feat(explorer): stabilize local graph interaction --- .gitignore | 3 + .../GraphWorkspace/GraphInspectorPanel.tsx | 8 +++ .../GraphWorkspace/GraphWorkspace.tsx | 62 ++++++++++++++----- 3 files changed, 56 insertions(+), 17 deletions(-) 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..ec1554df 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 grouped item is not directly inspectable. Activate Focused mode to resolve to a canonical node. +
+ ); + } + 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 48eeef03..404ebcad 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -823,9 +823,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 | { @@ -833,28 +841,44 @@ 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 = useMemo( + () => Boolean(resolveNodeIdForFocusedMode(selectedNodeId)), + [pluginRuntimeVersion, resolveNodeIdForFocusedMode, selectedNodeId], + ); + + const focusNode = useCallback((nodeId: string) => { + if (!nodeId) { + return; + } + setSelectedNodeId(nodeId); setSelectedEdgeId(""); setPathResult(null); setSearchResults([]); setSearchError(""); - if (nodeId) { - setIsLayoutRunning(false); - } - }, [viewMode]); + setIsLayoutRunning(false); + }, []); const handleEdgeSelect = useCallback((edgeId: string) => { setSelectedEdgeId(edgeId); @@ -1153,6 +1177,10 @@ export function GraphWorkspace() { focusNode(action.nodeId); return; case "setViewMode": + if (action.viewMode === "focused") { + activateFocusedMode(); + return; + } setViewMode(action.viewMode); return; case "collapseNeighborhood": @@ -1205,7 +1233,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) { @@ -1401,8 +1429,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: !canActivateFocusedMode, + onClick: activateFocusedMode, }, ], }); From f9e0bcf210fe8451c43f7b6dee6a25bd0a75fe9d Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 22 Apr 2026 15:50:34 -0400 Subject: [PATCH 2/5] fix(plugin): make local plugin install work out of the box Two separate schema issues blocked `/plugin marketplace add ./plugins` followed by `/plugin install semantica@semantica-local`: 1. `marketplace.json` was missing the required top-level `owner` object. Claude Code rejects with: `owner: Invalid input: expected object, received undefined`. 2. `plugin.json` declared `"agents": "./agents"` (string), but Claude Code's manifest schema rejects non-array `agents` with: `Validation errors: agents: Invalid input`. Auto-discovery from the default `agents/` directory works when the field is omitted, provided agents are flat `.md` files with frontmatter (Claude Code's subagent convention) rather than `/AGENT.md` subdirectories. Changes: - add `owner` object to `marketplace.json` - drop `agents` field from `plugin.json` (falls back to auto-discovery) - rename `agents//AGENT.md` -> `agents/.md` (frontmatter content is unchanged, just the path) After this, the documented local-install flow succeeds end-to-end. --- plugins/.claude-plugin/marketplace.json | 4 ++++ plugins/.claude-plugin/plugin.json | 1 - .../agents/{decision-advisor/AGENT.md => decision-advisor.md} | 0 plugins/agents/{explainability/AGENT.md => explainability.md} | 0 plugins/agents/{kg-assistant/AGENT.md => kg-assistant.md} | 0 5 files changed, 4 insertions(+), 1 deletion(-) rename plugins/agents/{decision-advisor/AGENT.md => decision-advisor.md} (100%) rename plugins/agents/{explainability/AGENT.md => explainability.md} (100%) rename plugins/agents/{kg-assistant/AGENT.md => kg-assistant.md} (100%) 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..4483838b 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" } 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 From 738480606c6372cc273541f99a97d4a9f80baef5 Mon Sep 17 00:00:00 2001 From: Serge Date: Wed, 22 Apr 2026 16:03:00 -0400 Subject: [PATCH 3/5] fix(plugin): drop hooks field from plugin.json (auto-loaded) Claude Code auto-loads hooks/hooks.json. Declaring it explicitly in manifest.hooks causes: 'Duplicate hooks file detected ... already-loaded'. Same pattern as agents: manifest should only reference *additional* hook files beyond the default. --- plugins/.claude-plugin/plugin.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/.claude-plugin/plugin.json b/plugins/.claude-plugin/plugin.json index 4483838b..fa0feddd 100644 --- a/plugins/.claude-plugin/plugin.json +++ b/plugins/.claude-plugin/plugin.json @@ -24,6 +24,5 @@ "extraction", "mcp" ], - "skills": "./skills", - "hooks": "./hooks/hooks.json" + "skills": "./skills" } From 92801c220e9c1efe97c52b7ac3070fe91b097e19 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Thu, 23 Apr 2026 11:34:38 +0530 Subject: [PATCH 4/5] docs(plugin): align Claude install commands with marketplace flow --- plugins/.claude-plugin/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: From 897d950bdce150c3db608119d36edbe82437f97e Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Thu, 23 Apr 2026 18:24:31 +0530 Subject: [PATCH 5/5] fix(explorer): address PR #487 review blockers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Prevent active-but-disabled Focused button by only disabling when viewMode is not already "focused" (viewMode !== "focused" && !canActivateFocusedMode). 2. Generalize inspector fallback copy — stale/invalid node IDs are not necessarily grouped items, so remove the misleading "Activate Focused mode" hint. 3. Move pluginRuntimeRef.current read out of render by converting canActivateFocusedMode from useMemo to useState + useEffect, resolving two ESLint "cannot access refs during render" errors and the missing toolbar-memo dependency warning. Co-authored-by: Sameer6305 Co-authored-by: KaifAhmad1 --- .../GraphWorkspace/GraphInspectorPanel.tsx | 2 +- .../src/workspaces/GraphWorkspace/GraphWorkspace.tsx | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx index ec1554df..e512e4bd 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx @@ -176,7 +176,7 @@ export function GraphInspectorPanel({ if (!graph.hasNode(nodeId)) { return (
- Selected grouped item is not directly inspectable. Activate Focused mode to resolve to a canonical node. + Selected item is not available for inspection in the current graph.
); } diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index 404ebcad..32daf9e2 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -862,10 +862,10 @@ export function GraphWorkspace() { } setViewMode("focused"); }, [resolveNodeIdForFocusedMode, selectedNodeId]); - const canActivateFocusedMode = useMemo( - () => Boolean(resolveNodeIdForFocusedMode(selectedNodeId)), - [pluginRuntimeVersion, resolveNodeIdForFocusedMode, selectedNodeId], - ); + const [canActivateFocusedMode, setCanActivateFocusedMode] = useState(false); + useEffect(() => { + setCanActivateFocusedMode(Boolean(resolveNodeIdForFocusedMode(selectedNodeId))); + }, [pluginRuntimeVersion, resolveNodeIdForFocusedMode, selectedNodeId]); const focusNode = useCallback((nodeId: string) => { if (!nodeId) { @@ -1429,7 +1429,7 @@ export function GraphWorkspace() { label: "Focused", title: "Inspect the selected node in a focused local graph", active: viewMode === "focused", - disabled: !canActivateFocusedMode, + disabled: viewMode !== "focused" && !canActivateFocusedMode, onClick: activateFocusedMode, }, ], @@ -1532,6 +1532,8 @@ export function GraphWorkspace() { return groups; }, [ + activateFocusedMode, + canActivateFocusedMode, displayState.groupedViewAvailable, handlePluginAction, hasGraphContent,