From a04cdfd577d659934b6fbaf454ac1c63833f4cd1 Mon Sep 17 00:00:00 2001 From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:58:11 +0500 Subject: [PATCH 1/6] fix(explorer): show a retryable error when the graph fails to load The dependency-pre-bundle overlay had no failure path: on a fetch error it kept rendering the last progress frame forever with no retry. Route isError/error out of the load query, surface a real error card with the underlying message, and let retry re-fetch without a full page reload. --- .../GraphWorkspace/GraphLoadingOverlay.tsx | 95 +++++++++++++++++++ .../GraphWorkspace/GraphWorkspace.tsx | 22 ++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx b/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx index 77a94130..a1ca4ea8 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState, type CSSProperties } from "react"; +import { AlertTriangle, RefreshCw } from "lucide-react"; import { GRAPH_THEME, withAlpha } from "./graphTheme"; import { GRAPH_LOAD_STAGE_SEQUENCE, createGraphLoadProgress, getGraphLoadStageLabel } from "./graphLoading"; @@ -121,6 +122,58 @@ const LOADING_OVERLAY_CSS = ` 0% { transform: translateX(-120%); } 100% { transform: translateX(360%); } } + .graph-stage-loader-card[data-error="true"] { + pointer-events: auto; + border-color: rgba(255, 123, 114, 0.32); + background: + radial-gradient(circle at top left, rgba(255, 123, 114, 0.12), transparent 32%), + linear-gradient(145deg, rgba(7, 17, 31, 0.96), rgba(24, 14, 18, 0.86)); + } + .graph-stage-loader-error-mark { + width: 38px; + height: 38px; + flex: 0 0 auto; + border-radius: 12px; + display: grid; + place-items: center; + color: #ff9e97; + background: rgba(255, 123, 114, 0.12); + border: 1px solid rgba(255, 123, 114, 0.28); + } + .graph-stage-loader-error-detail { + padding: 10px 12px; + border-radius: 10px; + background: rgba(0, 0, 0, 0.32); + border: 1px solid rgba(255, 123, 114, 0.18); + color: #ffb4ae; + font-family: "JetBrains Mono", "Fira Code", Consolas, monospace; + font-size: 12px; + line-height: 1.55; + word-break: break-word; + } + .graph-stage-loader-retry { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 9px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 700; + cursor: pointer; + border: 1px solid rgba(127, 208, 255, 0.4); + background: linear-gradient(135deg, rgba(74, 163, 255, 0.28), rgba(56, 210, 160, 0.16)); + color: #e8f6ff; + transition: 160ms ease; + } + .graph-stage-loader-retry:hover { + border-color: rgba(127, 208, 255, 0.62); + background: linear-gradient(135deg, rgba(74, 163, 255, 0.4), rgba(56, 210, 160, 0.24)); + transform: translateY(-1px); + } + .graph-stage-loader-retry:focus-visible { + outline: 2px solid #7fd0ff; + outline-offset: 2px; + } `; function formatLayoutSource(source: GraphLoadProgress["layoutSource"]) { @@ -170,10 +223,14 @@ export function GraphLoadingOverlay({ progress, visible, showGraphBehind, + error = null, + onRetry, }: { progress: GraphLoadProgress | null; visible: boolean; showGraphBehind: boolean; + error?: string | null; + onRetry?: () => void; }) { const [renderVisible, setRenderVisible] = useState(visible); const [exiting, setExiting] = useState(false); @@ -226,6 +283,44 @@ export function GraphLoadingOverlay({ return null; } + if (error) { + return ( +
+ +
+
+ +
+
+ Could not load the graph +
+
+ The Explorer API did not return graph data. Check that the backend is running and reachable, then try again. +
+
+
+ +
{error}
+ + {onRetry ? ( +
+ +
+ ) : null} +
+
+ ); + } + const activeProgress = progress ?? displayProgress; const isLiveStage = activeProgress.phase === "stabilizing_layout" || activeProgress.showGraphBehind || showGraphBehind; const overlayBackground = isLiveStage diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index df2cd6ec..14a94416 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -1225,12 +1225,28 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap })); }, []); - const { data: summary, isLoading, isFetching } = useLoadGraph({ + const { + data: summary, + isLoading, + isFetching, + isError: isGraphLoadError, + error: graphLoadError, + refetch: refetchGraph, + } = useLoadGraph({ enabled: true, onGraphReady: applyGraphReadySummary, onProgress: handleLoadProgress, }); + const graphLoadErrorMessage = isGraphLoadError + ? (graphLoadError instanceof Error ? graphLoadError.message : "Unknown error while loading the graph.") + : null; + + const handleRetryGraphLoad = useCallback(() => { + setLoadingProgress(null); + void refetchGraph(); + }, [refetchGraph]); + useEffect(() => { if (isLayoutRunning) { return; @@ -1901,7 +1917,7 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap viewMode, ]); - const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress)); + const showLoadingOverlay = !graphReady && (isLoading || isFetching || Boolean(loadingProgress) || isGraphLoadError); const showSettlingStatus = graphReady && loadingProgress?.phase === "stabilizing_layout"; const hasGraphContent = Boolean(summary?.nodeCount); const activePath = pathResult?.path ?? EMPTY_PATH; @@ -2946,6 +2962,8 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap progress={loadingProgress} visible={showLoadingOverlay} showGraphBehind={hasGraphContent || Boolean(loadingProgress?.showGraphBehind)} + error={graphLoadErrorMessage} + onRetry={handleRetryGraphLoad} /> From 513ffdf8fe8447079240f6cb38013eeee8a03dba Mon Sep 17 00:00:00 2001 From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:58:19 +0500 Subject: [PATCH 2/6] fix(explorer): reflect real backend connectivity on the landing page The status dot and 'System Online' text were static, so a dead backend still looked healthy. Track checking/online/offline explicitly and drive both off the same state so they can't disagree. --- explorer/src/App.tsx | 50 ++++++++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index f3f97520..8f94d477 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -67,6 +67,14 @@ type GraphStatsPayload = { edges?: number; }; +type ConnectionStatus = 'checking' | 'online' | 'offline'; + +const CONNECTION_STATUS_LABEL: Record = { + checking: 'Connecting…', + online: 'System Online', + offline: 'Backend Unreachable', +}; + const queryClient = new QueryClient(); const PREVIEW_DOTS = Array.from({ length: 42 }, (_, i) => ({ @@ -719,19 +727,34 @@ const shellStyles = ` align-items: center; gap: 10px; margin-bottom: 24px; + --status-color: #4cc38a; + --status-shadow-a: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5); + --status-shadow-b: 0 0 0 5px rgba(76, 195, 138, 0.1), 0 0 20px rgba(76, 195, 138, 0.35); + } + + .landing-status-bar[data-status='checking'] { + --status-color: #f2b66d; + --status-shadow-a: 0 0 0 3px rgba(242, 182, 109, 0.22), 0 0 12px rgba(242, 182, 109, 0.5); + --status-shadow-b: 0 0 0 5px rgba(242, 182, 109, 0.1), 0 0 20px rgba(242, 182, 109, 0.35); + } + + .landing-status-bar[data-status='offline'] { + --status-color: #ff7b72; + --status-shadow-a: 0 0 0 3px rgba(255, 123, 114, 0.22), 0 0 12px rgba(255, 123, 114, 0.5); + --status-shadow-b: 0 0 0 5px rgba(255, 123, 114, 0.1), 0 0 20px rgba(255, 123, 114, 0.35); } .landing-status-dot { width: 8px; height: 8px; border-radius: 999px; - background: #4cc38a; - box-shadow: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5); + background: var(--status-color); + box-shadow: var(--status-shadow-a); animation: landing-pulse 2.4s ease-in-out infinite; } .landing-status-text { - color: #4cc38a; + color: var(--status-color); font: 700 11px/1 "JetBrains Mono", monospace; letter-spacing: 0.1em; text-transform: uppercase; @@ -1323,8 +1346,8 @@ const shellStyles = ` } @keyframes landing-pulse { - 0%, 100% { box-shadow: 0 0 0 3px rgba(76, 195, 138, 0.22), 0 0 12px rgba(76, 195, 138, 0.5); } - 50% { box-shadow: 0 0 0 5px rgba(76, 195, 138, 0.1), 0 0 20px rgba(76, 195, 138, 0.35); } + 0%, 100% { box-shadow: var(--status-shadow-a); } + 50% { box-shadow: var(--status-shadow-b); } } .workspace-loading { @@ -1494,10 +1517,10 @@ function WelcomeScreen({ onOpenDecisions: () => void; onOpenManage: () => void; }) { - const [stats, setStats] = useState<{ nodes: number | null; edges: number | null; ready: boolean }>({ + const [stats, setStats] = useState<{ nodes: number | null; edges: number | null; status: ConnectionStatus }>({ nodes: null, edges: null, - ready: false, + status: 'checking', }); useEffect(() => { @@ -1507,31 +1530,32 @@ function WelcomeScreen({ .then((response) => (response.ok ? response.json() as Promise : null)) .then((payload) => { if (!payload) { - setStats((current) => ({ ...current, ready: false })); + setStats((current) => ({ ...current, status: 'offline' })); return; } setStats({ nodes: getNumberStat(payload, ['node_count', 'nodeCount', 'nodes']), edges: getNumberStat(payload, ['edge_count', 'edgeCount', 'edges']), - ready: true, + status: 'online', }); }) .catch((error: unknown) => { if (error instanceof DOMException && error.name === 'AbortError') { return; } - setStats((current) => ({ ...current, ready: false })); + setStats((current) => ({ ...current, status: 'offline' })); }); return () => controller.abort(); }, []); + const isOnline = stats.status === 'online'; const metrics: LandingMetric[] = [ { label: 'Knowledge nodes', value: formatMetric(stats.nodes, 'Live'), tone: 'cyan' }, { label: 'Relationships mapped', value: formatMetric(stats.edges, 'Ready'), tone: 'mint' }, { label: 'Graph modes', value: '3', tone: 'amber' }, - { label: stats.ready ? 'Dataset online' : 'Ready to explore', value: stats.ready ? 'Active' : 'Standby', tone: 'rose' }, + { label: isOnline ? 'Dataset online' : 'Ready to explore', value: isOnline ? 'Active' : 'Standby', tone: 'rose' }, ]; const secondaryLaunchers: LandingAction[] = [ @@ -1574,9 +1598,9 @@ function WelcomeScreen({ {/* ── Hero ── */}
-
+
- System Online + {CONNECTION_STATUS_LABEL[stats.status]}
Semantica v2 · Semantic Intelligence
From 8df3abf6a0d6004bdbff31e232f5261f60d0ca61 Mon Sep 17 00:00:00 2001 From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:58:59 +0500 Subject: [PATCH 3/6] feat(explorer): let search results be dismissed, round relevance scores The results strip had no close affordance and stayed pinned until the next search. Add a header row with a dismiss button, and round scores to whole numbers instead of showing three decimals of a raw relevance value nobody can act on. --- .../GraphWorkspace/GraphWorkspace.tsx | 48 ++++++++++++++----- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index 14a94416..54f3f8c4 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -12,6 +12,7 @@ import { RefreshCw, Search, Users, + X, ZoomIn, ZoomOut, } from "lucide-react"; @@ -1539,6 +1540,11 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap } }, [searchQuery]); + const handleClearSearchResults = useCallback(() => { + setSearchResults([]); + setSearchError(""); + }, []); + const handleRunPredictions = useCallback(async () => { if (!inspectableNodeId) return; setIsRunningPredictions(true); @@ -2855,20 +2861,36 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap {searchError ?
{searchError}
: null} {searchResults.length ? ( -
- {searchResults.map((result) => ( - - ))} +
+
+ {searchResults.map((result) => ( + + ))} +
) : null} From b2095c2561d7ecc587dddf2e02207eb31836fe3f Mon Sep 17 00:00:00 2001 From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:59:14 +0500 Subject: [PATCH 4/6] feat(explorer): add typeahead suggestions to graph search Typing in the search box now debounces a query against the existing search endpoint and shows a combobox dropdown, with arrow-key navigation, Enter/click to jump straight to a node, and Escape to dismiss. Previously nothing happened until the full form was submitted. --- .../GraphWorkspace/GraphWorkspace.tsx | 179 +++++++++++++++++- 1 file changed, 176 insertions(+), 3 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index 54f3f8c4..bb6ebf59 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType, type ReactNode } from "react"; +import { useCallback, useEffect, useId, useMemo, useRef, useState, type ComponentType, type ReactNode } from "react"; import { Activity, Clock3, @@ -283,37 +283,161 @@ function SegmentedModeControl({ items }: { items: GraphToolbarItem[] }) { ); } +const SUGGESTION_DEBOUNCE_MS = 250; +const SUGGESTION_LIMIT = 6; + function SearchCommandBar({ value, disabled, onChange, onSubmit, + onSelectSuggestion, }: { value: string; disabled: boolean; onChange: (value: string) => void; onSubmit: () => void; + onSelectSuggestion: (result: SearchResult) => void; }) { + const [suggestions, setSuggestions] = useState([]); + const [suggestionsOpen, setSuggestionsOpen] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const abortRef = useRef(null); + const debounceRef = useRef(null); + const listboxId = useId(); + + useEffect(() => { + if (debounceRef.current !== null) { + window.clearTimeout(debounceRef.current); + } + + const query = value.trim(); + if (disabled || !query) { + setSuggestions([]); + setSuggestionsOpen(false); + setHighlightedIndex(-1); + return; + } + + debounceRef.current = window.setTimeout(() => { + abortRef.current?.abort(); + const controller = new AbortController(); + abortRef.current = controller; + + fetch("/api/graph/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ query, limit: SUGGESTION_LIMIT }), + signal: controller.signal, + }) + .then((response) => (response.ok ? response.json() : null)) + .then((data: { results?: SearchResult[] } | null) => { + if (!data) return; + setSuggestions(data.results ?? []); + setSuggestionsOpen(true); + setHighlightedIndex(-1); + }) + .catch((suggestionError: unknown) => { + if (suggestionError instanceof DOMException && suggestionError.name === "AbortError") { + return; + } + }); + }, SUGGESTION_DEBOUNCE_MS); + + return () => { + if (debounceRef.current !== null) { + window.clearTimeout(debounceRef.current); + } + }; + }, [value, disabled]); + + useEffect(() => () => abortRef.current?.abort(), []); + + const closeSuggestions = () => { + setSuggestionsOpen(false); + setHighlightedIndex(-1); + }; + + const selectSuggestion = (result: SearchResult) => { + setSuggestions([]); + closeSuggestions(); + onSelectSuggestion(result); + }; + return (
0} + aria-haspopup="listbox" + aria-owns={listboxId} onSubmit={(event) => { event.preventDefault(); - if (!disabled) { - onSubmit(); + if (disabled) return; + if (suggestionsOpen && highlightedIndex >= 0 && suggestions[highlightedIndex]) { + selectSuggestion(suggestions[highlightedIndex]); + return; } + closeSuggestions(); + onSubmit(); }} > onChange(event.target.value)} + onFocus={() => { + if (suggestions.length > 0) { + setSuggestionsOpen(true); + } + }} + onBlur={() => { + window.setTimeout(closeSuggestions, 120); + }} + onKeyDown={(event) => { + if (!suggestionsOpen || suggestions.length === 0) return; + if (event.key === "ArrowDown") { + event.preventDefault(); + setHighlightedIndex((current) => (current + 1) % suggestions.length); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + setHighlightedIndex((current) => (current <= 0 ? suggestions.length - 1 : current - 1)); + } else if (event.key === "Escape") { + event.preventDefault(); + closeSuggestions(); + } + }} placeholder="Search command, node, or concept" aria-label="Search graph nodes" + aria-autocomplete="list" + aria-controls={listboxId} + aria-activedescendant={highlightedIndex >= 0 ? `${listboxId}-${highlightedIndex}` : undefined} /> + + {suggestionsOpen && suggestions.length > 0 ? ( +
    + {suggestions.map((result, index) => ( +
  • { + event.preventDefault(); + selectSuggestion(result); + }} + onMouseEnter={() => setHighlightedIndex(index)} + > + {result.node.content || result.node.id} + {result.node.type} +
  • + ))} +
+ ) : null} ); } @@ -578,6 +702,7 @@ const HUD_CSS = ` gap: 10px; } .explore-search-command { + position: relative; min-width: 0; height: 43px; display: grid; @@ -593,6 +718,50 @@ const HUD_CSS = ` color: ${GRAPH_THEME.ui.text.muted}; box-shadow: inset 0 1px 0 rgba(255,255,255,0.045), 0 14px 30px rgba(0,0,0,0.16); } + .explore-search-suggestions { + position: absolute; + top: calc(100% + 6px); + left: 0; + right: 0; + z-index: 30; + margin: 0; + padding: 6px; + list-style: none; + max-height: 288px; + overflow-y: auto; + border-radius: 14px; + border: 1px solid ${GRAPH_THEME.ui.control.inputBorder}; + background: ${GRAPH_THEME.ui.surface.cardStrong}; + box-shadow: 0 18px 40px rgba(0,0,0,0.32), inset 0 1px 0 rgba(255,255,255,0.04); + } + .explore-search-suggestions li { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + padding: 8px 10px; + border-radius: 10px; + cursor: pointer; + color: ${GRAPH_THEME.ui.text.body}; + } + .explore-search-suggestions li[data-highlighted="true"] { + background: ${GRAPH_THEME.ui.control.hoverBg}; + } + .explore-search-suggestion-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-weight: 600; + } + .explore-search-suggestion-type { + flex-shrink: 0; + font-size: 11px; + color: ${GRAPH_THEME.ui.text.subtle}; + text-transform: uppercase; + letter-spacing: 0.04em; + } .explore-search-command:focus-within { border-color: ${GRAPH_THEME.ui.control.activeBorder}; box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 0 0 1px ${GRAPH_THEME.ui.control.focusRing}, 0 16px 32px rgba(0,0,0,0.18); @@ -2786,6 +2955,10 @@ export function GraphWorkspace({ externalFocusNodeId, externalFocusToken }: Grap disabled={searchDisabled} onChange={setSearchQuery} onSubmit={() => void handleSearch()} + onSelectSuggestion={(result) => { + setSearchQuery(""); + focusNode(result.node.id); + }} />
From bc75758929da77f0cef2f387c095d0ee42518e69 Mon Sep 17 00:00:00 2001 From: Zohaib Hassnain <109234410+ZohaibHassan16@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:27:21 +0500 Subject: [PATCH 5/6] fix(explorer): abort stale typeahead requests and clear suggestions on error Clearing the search box while a suggestion fetch was in flight never aborted it, so a late response could reopen the dropdown with results for a query that was no longer typed. A non-OK response also left whatever suggestions were already on screen untouched instead of clearing them. Abort on every effect cleanup (not just unmount) and clear suggestions on any non-abort failure. --- .../GraphWorkspace/GraphWorkspace.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx index bb6ebf59..d38d21ad 100644 --- a/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx +++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx @@ -313,6 +313,7 @@ function SearchCommandBar({ const query = value.trim(); if (disabled || !query) { + abortRef.current?.abort(); setSuggestions([]); setSuggestionsOpen(false); setHighlightedIndex(-1); @@ -330,9 +331,13 @@ function SearchCommandBar({ body: JSON.stringify({ query, limit: SUGGESTION_LIMIT }), signal: controller.signal, }) - .then((response) => (response.ok ? response.json() : null)) - .then((data: { results?: SearchResult[] } | null) => { - if (!data) return; + .then((response) => { + if (!response.ok) { + throw new Error(`Search failed with status ${response.status}`); + } + return response.json(); + }) + .then((data: { results?: SearchResult[] }) => { setSuggestions(data.results ?? []); setSuggestionsOpen(true); setHighlightedIndex(-1); @@ -341,6 +346,9 @@ function SearchCommandBar({ if (suggestionError instanceof DOMException && suggestionError.name === "AbortError") { return; } + setSuggestions([]); + setSuggestionsOpen(false); + setHighlightedIndex(-1); }); }, SUGGESTION_DEBOUNCE_MS); @@ -348,11 +356,10 @@ function SearchCommandBar({ if (debounceRef.current !== null) { window.clearTimeout(debounceRef.current); } + abortRef.current?.abort(); }; }, [value, disabled]); - useEffect(() => () => abortRef.current?.abort(), []); - const closeSuggestions = () => { setSuggestionsOpen(false); setHighlightedIndex(-1); From 262cae4000c0f1cb52e8f68b9a109017dadf6294 Mon Sep 17 00:00:00 2001 From: KaifAhmad1 Date: Fri, 14 Aug 2026 17:28:00 +0530 Subject: [PATCH 6/6] docs(changelog): add entry for Explorer backend failure states fix Documents the (#980, closes #977) fix in the Unreleased/Fixed section. --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 023332ee..76a189d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Explorer UI hid backend failures: graph load hung forever, landing page always showed "System Online"** (#980, closes #977) by @ZohaibHassan16, reviewed by @Sameer6305 + - `GraphWorkspace.tsx` only destructured `{ data, isLoading, isFetching }` from `useLoadGraph()`, ignoring the `isError`/`error`/`refetch` that `useQuery` (`retry: 0`) already returned. Combined with `GraphLoadingOverlay` having no error prop and `showLoadingOverlay` staying true whenever `loadingProgress` held a stale frame, a backend-down or failed fetch left the graph workspace stuck on the last progress frame indefinitely, with no error message and no way to recover short of a full page reload + - `GraphLoadingOverlay` now accepts `error`/`onRetry` and renders an error card with the real fetch error message and a Retry button (`refetch()`) instead of the stuck progress UI + - The landing page's `WelcomeScreen` replaced its hardcoded `ready: boolean` (and hardcoded "System Online" text) with a real `checking` / `online` / `offline` status derived from the same connectivity probe already driving the 4th metric card, so the status dot, text, and metric can no longer drift apart or lie about connectivity + - **Smaller fixes bundled in the same PR**: search results are now dismissible (previously stayed open indefinitely, pushing the graph down); relevance scores display as rounded whole numbers instead of `96.900`/`138.000`; added a debounced (250ms) typeahead combobox to graph search with arrow-key navigation, `aria-activedescendant`, and Escape-to-close, using the existing `/api/graph/search` endpoint + - **Fixed during review** (Qodo): the typeahead's debounced fetch had no `AbortController`, so a fast-typing user could have a stale suggestion response resolve after a newer one, replacing correct suggestions with outdated ones. In-flight requests are now aborted on every re-debounce and when the query is cleared after a selection + - **Noted during review** (@Sameer6305): `GraphWorkspaceShell.tsx` contains a third, unused implementation of the same graph-loading/error-handling logic this PR fixes — the issue itself named "two copies that drifted apart" as the root cause the original bug slipped through. Deliberately left out of this PR's scope and tracked separately in #981 rather than blocking this fix + - `npx tsc -b`: clean; `test:graph-store`/`test:graph-workspace`/`test:plugin-registry`: 42 passed; `npm run build`: succeeds + - **`VectorManager.maintain_store()`/`collect_statistics()` crashed with `AttributeError` on persistent `VectorStore` backends** (#914, closes #855) by @yunaremaia, with fixes by @Sameer6305 - Both methods accessed `store.vectors`/`store.metadata` directly, which are only initialized for the `inmemory` backend — any persistent backend (FAISS, Qdrant, Pinecone, Milvus, SQLite, PgVector, Weaviate) crashed immediately. Same root cause as the #839/#843/#845/#848 cluster, but `VectorManager` operates on a `VectorStore` instance from the outside, so the fix needed a public accessor rather than another internal guard - Added a backend-agnostic `VectorStore.count()`: the `inmemory` backend counts its local dict; persistent backends delegate to a `count()` on the wrapped backend store when one exists, or raise `NotImplementedError` — following the `get_vector()`/`get_metadata()` precedent from #843, a missing/uninitialized backend store is never silently reported as an empty, healthy store