From dad726c777e6c0f93693817a13546c70f89379b1 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 13 Aug 2026 16:48:01 -0700 Subject: [PATCH] FN-9038: add Knowledge Graph tab to Memory view Expose knowledge graph data in the Memory view through a localized, resilient dashboard panel. - Add protected knowledge graph API routes and access helpers. - Add a Memory view tab, graph panel, loading/error states, and localized labels. - Document graph navigation and cover access, route, hook, and component behavior. Files changed: .changeset/fn-9038-knowledge-graph-tab.md | 7 + docs/dashboard-guide.md | 2 + docs/knowledge-graph.md | 10 ++ packages/dashboard/app/api/legacy.ts | 17 ++- packages/dashboard/app/api/system/index.ts | 1 + .../dashboard/app/api/system/knowledge-graph.ts | 23 +++ .../app/components/KnowledgeGraphPanel.css | 35 +++++ .../app/components/KnowledgeGraphPanel.tsx | 68 +++++++++ packages/dashboard/app/components/MemoryView.tsx | 15 +- .../__tests__/KnowledgeGraphPanel.test.tsx | 49 +++++++ .../app/components/__tests__/MemoryView.test.tsx | 13 ++ .../app/hooks/__tests__/useKnowledgeGraph.test.ts | 66 +++++++++ packages/dashboard/app/hooks/useKnowledgeGraph.ts | 94 ++++++++++++ .../src/__tests__/knowledge-graph-access.test.ts | 88 ++++++++++++ packages/dashboard/src/knowledge-graph-access.ts | 160 +++++++++++++++++++++ .../register-knowledge-graph-routes.test.ts | 75 ++++++++++ .../src/routes/register-knowledge-routes.ts | 120 ++++++++++++++++ packages/i18n/locales/en/app.json | 15 +- packages/i18n/locales/es/app.json | 15 +- packages/i18n/locales/fr/app.json | 15 +- packages/i18n/locales/ko/app.json | 15 +- packages/i18n/locales/pt-BR/app.json | 15 +- packages/i18n/locales/zh-CN/app.json | 15 +- packages/i18n/locales/zh-TW/app.json | 15 +- 24 files changed, 939 insertions(+), 9 deletions(-) Fusion-Task-Id: FN-9038 Fusion-Task-Lineage: 4200e214-ad22-4f92-b980-c9bcb8ffd3b9 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-9038-knowledge-graph-tab.md | 7 + docs/dashboard-guide.md | 2 + docs/knowledge-graph.md | 10 ++ packages/dashboard/app/api/legacy.ts | 17 +- packages/dashboard/app/api/system/index.ts | 1 + .../app/api/system/knowledge-graph.ts | 23 +++ .../app/components/KnowledgeGraphPanel.css | 35 ++++ .../app/components/KnowledgeGraphPanel.tsx | 68 ++++++++ .../dashboard/app/components/MemoryView.tsx | 15 +- .../__tests__/KnowledgeGraphPanel.test.tsx | 49 ++++++ .../components/__tests__/MemoryView.test.tsx | 13 ++ .../hooks/__tests__/useKnowledgeGraph.test.ts | 66 ++++++++ .../dashboard/app/hooks/useKnowledgeGraph.ts | 94 ++++++++++ .../__tests__/knowledge-graph-access.test.ts | 88 ++++++++++ .../dashboard/src/knowledge-graph-access.ts | 160 ++++++++++++++++++ .../register-knowledge-graph-routes.test.ts | 75 ++++++++ .../src/routes/register-knowledge-routes.ts | 120 +++++++++++++ packages/i18n/locales/en/app.json | 15 +- packages/i18n/locales/es/app.json | 15 +- packages/i18n/locales/fr/app.json | 15 +- packages/i18n/locales/ko/app.json | 15 +- packages/i18n/locales/pt-BR/app.json | 15 +- packages/i18n/locales/zh-CN/app.json | 15 +- packages/i18n/locales/zh-TW/app.json | 15 +- 24 files changed, 939 insertions(+), 9 deletions(-) create mode 100644 .changeset/fn-9038-knowledge-graph-tab.md create mode 100644 packages/dashboard/app/api/system/knowledge-graph.ts create mode 100644 packages/dashboard/app/components/KnowledgeGraphPanel.css create mode 100644 packages/dashboard/app/components/KnowledgeGraphPanel.tsx create mode 100644 packages/dashboard/app/components/__tests__/KnowledgeGraphPanel.test.tsx create mode 100644 packages/dashboard/app/hooks/__tests__/useKnowledgeGraph.test.ts create mode 100644 packages/dashboard/app/hooks/useKnowledgeGraph.ts create mode 100644 packages/dashboard/src/__tests__/knowledge-graph-access.test.ts create mode 100644 packages/dashboard/src/knowledge-graph-access.ts create mode 100644 packages/dashboard/src/routes/__tests__/register-knowledge-graph-routes.test.ts diff --git a/.changeset/fn-9038-knowledge-graph-tab.md b/.changeset/fn-9038-knowledge-graph-tab.md new file mode 100644 index 0000000000..3e7d647a19 --- /dev/null +++ b/.changeset/fn-9038-knowledge-graph-tab.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add Memory Knowledge Graph navigation and bounded path search. +category: feature +dev: Adds /knowledge/graph endpoints; path searches cap at 10 hops and 20,000 expansions. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index ede18baa32..9851f9d5c1 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2450,3 +2450,5 @@ The shared Task Detail Definition view shows the persisted spec alignment, lates `GET /api/models` bounds each catalog refresh to 15 seconds and continues serving the registry's retained `getAvailable()` rows when a provider stalls or fails. Refreshes are single-flight per registry instance: a timed-out operation can continue in the provider runtime, but Fusion never starts another concurrently. A successful refresh is fresh for 60 seconds from its successful settlement; a failed refresh uses a separate 60-second retry window measured from its attempt start, so a failure is never reported as fresh. After a failed refresh settles, the next attempt starts only after both settlement and that retry interval. Saving or removing API keys, completing OAuth login/manual-code flows, logging out, and removing credential instances invalidate that registry's generation and clear both windows. If a credential change happens while an uncancellable refresh is already running, the model list temporarily serves its retained rows rather than overlapping the refresh. Once that old refresh settles, the first following request starts a current-credential refresh with no additional cache-window wait. + +The Memory view also includes a fourth **Knowledge Graph** tab. It provides capped search, node detail, edge and neighbor drill-down, bounded shortest-path navigation, and explicit artifact rebuilding for the deterministic project knowledge graph. diff --git a/docs/knowledge-graph.md b/docs/knowledge-graph.md index 0f4a7b9c8e..6dfc5ed06b 100644 --- a/docs/knowledge-graph.md +++ b/docs/knowledge-graph.md @@ -43,3 +43,13 @@ Memory capture is optional and detached. `RecallCaptureWriter.capture()` returns ## Non-goals This layer has no LLM calls, embeddings, vector recall, MCP tools, source-validity diagnostics, language support beyond TypeScript/TSX symbols, CommonMark parser, cross-rename identity, or capability-fabric bundle. The FR-29/FR-34 bundle format is deferred. + +## Dashboard navigation + +The dashboard's **Memory → Knowledge Graph** tab provides list-and-detail navigation without transferring the full artifact or rendering a whole-graph canvas. It shows graph status, counts, directory and provenance data; operators can rebuild the artifact explicitly (including force rebuild). Reads use a small manifest-validated artifact cache and never rebuild the graph. + +The tab queries capped node pages (200 nodes), node edges (200 per direction), and neighbors with direction, edge-kind, and depth controls. Node IDs are query parameters, so IDs containing `/`, `#`, `@`, or `~` remain safe to navigate. It also supports node filters for kind, path/id prefix, regular-expression name, FNXC area, symbol kind, and owner. + +The HTTP surface is `GET /api/knowledge/graph/status`, `/nodes`, `/node`, `/neighbors`, and `/path`, plus `POST /api/knowledge/graph/build`. Missing artifacts are a recoverable status state; query endpoints report them as 404 rather than silently returning empty results. + +Dashboard shortest paths use a bounded undirected BFS, not the core unbounded helper. The default is six hops, the maximum is ten hops, and at most 20,000 nodes are expanded. Results are `found`, exhaustive `not-found`, or `limit-reached`; the latter has `truncated: true` and is intentionally distinct from no path. Unknown node IDs return 404. Whole-graph force-directed/canvas rendering remains an explicit non-goal. diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index c4c6b1664c..15699865eb 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -272,7 +272,22 @@ export type { MemoryRetrievalTestResult, QmdInstallResult, } from "./system/memory.js"; - +export { + fetchKnowledgeGraphStatus, + queryKnowledgeGraphNodes, + fetchKnowledgeGraphNode, + fetchKnowledgeGraphNeighbors, + fetchKnowledgeGraphPath, + buildKnowledgeGraphArtifacts, +} from "./system/knowledge-graph.js"; +export type { + KnowledgeGraphStatus, + KnowledgeGraphNode, + KnowledgeGraphEdge, + KnowledgeGraphNeighbor, + KnowledgeGraphNodeQuery, + KnowledgeGraphPathResult, +} from "./system/knowledge-graph.js"; // Re-export skills types so hooks/components keep stable import paths via this barrel. import type { diff --git a/packages/dashboard/app/api/system/index.ts b/packages/dashboard/app/api/system/index.ts index 0e9cb12b61..0b55da11bc 100644 --- a/packages/dashboard/app/api/system/index.ts +++ b/packages/dashboard/app/api/system/index.ts @@ -6,6 +6,7 @@ export * from "./system-panel.js"; export * from "./insights.js"; export * from "./research.js"; export * from "./memory.js"; +export * from "./knowledge-graph.js"; export * from "./scheduling.js"; export * from "./workflows.js"; export * from "./plugins-and-skills.js"; diff --git a/packages/dashboard/app/api/system/knowledge-graph.ts b/packages/dashboard/app/api/system/knowledge-graph.ts new file mode 100644 index 0000000000..3e6c81dd0c --- /dev/null +++ b/packages/dashboard/app/api/system/knowledge-graph.ts @@ -0,0 +1,23 @@ +import { api } from "../client/client.js"; +import { withProjectId } from "../client/health.js"; + +export type KnowledgeGraphNode = { id: string; kind: "file" | "module" | "symbol" | "doc-concept" | "rationale"; name: string; owner: "file" | "derived"; ownerPath: string; source: { path: string; line: number; column: number }; attributes: Record }; +export type KnowledgeGraphEdge = { id: string; kind: "contains" | "imports" | "re-exports" | "relates-to" | "rationale-supports"; from: string; to: string; provenance: "extracted" | "inferred"; owner: "file" | "derived"; ownerPath: string; source: { path: string; line: number; column: number }; attributes: Record }; +export type KnowledgeGraphNeighbor = { node: KnowledgeGraphNode; distance: number; edges: KnowledgeGraphEdge[] }; +export type KnowledgeGraphNodeQuery = { kinds?: string[]; pathPrefix?: string; idPrefix?: string; namePattern?: string; fnxcArea?: string; symbolKind?: string; owner?: string; limit?: number; offset?: number }; +export type KnowledgeGraphStatus = { available: boolean; recoveryReason?: string | null; graphDir?: string; nodeCount?: number; edgeCount?: number; nodeKindCounts?: Record; edgeKindCounts?: Record; provenanceCounts?: Record; ownerCounts?: Record; fnxcAreas?: string[]; pathLimits: { defaultMaxHops: number; maxHops: number; maxExpansions: number } }; +export type KnowledgeGraphPathResult = + | { outcome: "found"; path: { nodes: KnowledgeGraphNode[]; edges: KnowledgeGraphEdge[] }; hops: number; maxHops: number; expansions: number; truncated: false } + | { outcome: "not-found"; path: null; maxHops: number; expansions: number; truncated: false } + | { outcome: "limit-reached"; path: null; maxHops: number; expansions: number; truncated: true; limit: "max-hops" | "max-expansions" }; + +function query(params: Record): string { + const entries = Object.entries(params).flatMap(([key, value]) => Array.isArray(value) ? value.filter(Boolean).map(item => [key, item] as const) : value === undefined || value === "" ? [] : [[key, String(value)] as const]); + return entries.length ? `?${entries.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&")}` : ""; +} +export function fetchKnowledgeGraphStatus(projectId?: string): Promise { return api(withProjectId("/knowledge/graph/status", projectId)); } +export function queryKnowledgeGraphNodes(projectId: string | undefined, filters: KnowledgeGraphNodeQuery = {}): Promise<{ nodes: KnowledgeGraphNode[]; total: number; limit: number; offset: number }> { return api(withProjectId(`/knowledge/graph/nodes${query(filters)}`, projectId)); } +export function fetchKnowledgeGraphNode(projectId: string | undefined, id: string): Promise<{ node: KnowledgeGraphNode; outgoing: KnowledgeGraphEdge[]; incoming: KnowledgeGraphEdge[]; outgoingTotal: number; incomingTotal: number }> { return api(withProjectId(`/knowledge/graph/node${query({ id })}`, projectId)); } +export function fetchKnowledgeGraphNeighbors(projectId: string | undefined, options: { nodeId: string; direction?: "out" | "in" | "both"; edgeKinds?: string[]; depth?: number; limit?: number }): Promise<{ neighbors: KnowledgeGraphNeighbor[]; total: number }> { return api(withProjectId(`/knowledge/graph/neighbors${query(options)}`, projectId)); } +export function fetchKnowledgeGraphPath(projectId: string | undefined, options: { fromId: string; toId: string; maxHops?: number }): Promise { return api(withProjectId(`/knowledge/graph/path${query(options)}`, projectId)); } +export function buildKnowledgeGraphArtifacts(projectId?: string, force = false): Promise<{ changed: boolean; nodes: number; edges: number; stats: Record }> { return api(withProjectId("/knowledge/graph/build", projectId), { method: "POST", body: JSON.stringify({ force }) }); } diff --git a/packages/dashboard/app/components/KnowledgeGraphPanel.css b/packages/dashboard/app/components/KnowledgeGraphPanel.css new file mode 100644 index 0000000000..c29dd12194 --- /dev/null +++ b/packages/dashboard/app/components/KnowledgeGraphPanel.css @@ -0,0 +1,35 @@ +/* +FNXC:KnowledgeGraphDashboard 2026-08-13-23:30: +The graph view deliberately presents capped list/detail slices rather than a whole-graph canvas. Responsive stacking keeps every filter and drill-down control usable below the Memory view mobile breakpoint. +*/ +.knowledge-graph-panel { display: flex; flex: 1; min-height: 0; flex-direction: column; gap: var(--space-md); overflow: auto; } +.knowledge-graph-empty { align-items: center; justify-content: center; } +.knowledge-graph-status, .knowledge-graph-filters, .knowledge-graph-neighbors, .knowledge-graph-path { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-sm); padding: var(--space-md); } +.knowledge-graph-status > div { display: grid; gap: var(--space-xs); margin-right: auto; } +.knowledge-graph-status strong { color: var(--text); } +.knowledge-graph-status span, .knowledge-graph-muted { color: var(--text-muted); } +.knowledge-graph-status dl { display: flex; flex-wrap: wrap; gap: var(--space-sm); margin: 0; } +.knowledge-graph-status dl div { display: grid; gap: 0; } +.knowledge-graph-status dt { color: var(--text-muted); font-size: var(--font-size-xs); } +.knowledge-graph-status dd { margin: 0; font-size: var(--font-size-xs); } +.knowledge-graph-toggle, .knowledge-graph-kind-options { display: flex; flex-wrap: wrap; gap: var(--space-xs); align-items: center; } +.knowledge-graph-kind-options label { display: inline-flex; gap: var(--space-xs); align-items: center; color: var(--text-muted); font-size: var(--font-size-sm); } +.knowledge-graph-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--space-md); } +.knowledge-graph-results, .knowledge-graph-detail { min-width: 0; padding: var(--space-md); } +.knowledge-graph-results h3, .knowledge-graph-detail h3, .knowledge-graph-neighbors h3, .knowledge-graph-path h3 { width: 100%; margin: 0; } +.knowledge-graph-results ul, .knowledge-graph-edge-list ul, .knowledge-graph-neighbors ul { display: flex; flex-direction: column; gap: var(--space-xs); margin: var(--space-sm) 0; padding: 0; list-style: none; } +.knowledge-graph-results li .btn { display: grid; width: 100%; justify-content: start; gap: var(--space-xs); text-align: left; } +.knowledge-graph-results li span { color: var(--text-muted); font-size: var(--font-size-xs); overflow-wrap: anywhere; } +.knowledge-graph-pagination { display: flex; justify-content: space-between; gap: var(--space-sm); } +.knowledge-graph-edge-list h4 { display: flex; justify-content: space-between; margin: var(--space-md) 0 var(--space-xs); } +.knowledge-graph-edge-list h4 span { color: var(--text-muted); font-size: var(--font-size-xs); font-weight: normal; } +.knowledge-graph-edge-list li, .knowledge-graph-neighbors li { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs); } +.knowledge-graph-provenance { border-radius: var(--radius-full); padding: var(--space-xs) var(--space-xs); font-size: var(--font-size-xs); } +.knowledge-graph-provenance--extracted { color: var(--color-success); background: color-mix(in srgb, var(--color-success) 15%, transparent); } +.knowledge-graph-provenance--inferred { color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); } +.knowledge-graph-endpoint { max-width: 100%; overflow-wrap: anywhere; text-align: left; } +.knowledge-graph-path-chain { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-xs); margin-top: var(--space-sm); } +.knowledge-graph-path-chain span { display: inline-flex; align-items: center; gap: var(--space-xs); } +.knowledge-graph-path-chain em { color: var(--text-muted); font-size: var(--font-size-xs); font-style: normal; } +.knowledge-graph-error { width: 100%; color: var(--color-error); } +@media (max-width: 768px) { .knowledge-graph-layout { grid-template-columns: minmax(0, 1fr); } .knowledge-graph-status, .knowledge-graph-filters, .knowledge-graph-neighbors, .knowledge-graph-path { align-items: stretch; } .knowledge-graph-status > div { margin-right: 0; } .knowledge-graph-status dl { display: grid; grid-template-columns: minmax(0, 1fr); } } diff --git a/packages/dashboard/app/components/KnowledgeGraphPanel.tsx b/packages/dashboard/app/components/KnowledgeGraphPanel.tsx new file mode 100644 index 0000000000..a2392a4856 --- /dev/null +++ b/packages/dashboard/app/components/KnowledgeGraphPanel.tsx @@ -0,0 +1,68 @@ +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useKnowledgeGraph } from "../hooks/useKnowledgeGraph.js"; +import type { KnowledgeGraphEdge } from "../api/system/knowledge-graph.js"; +import "./KnowledgeGraphPanel.css"; + +const NODE_KINDS = ["file", "module", "symbol", "doc-concept", "rationale"] as const; +/* FNXC:KnowledgeGraphDashboard 2026-08-13-23:21: Symbol filter values must exactly match the core SymbolKind union because the API rejects unknown enum values rather than silently widening a query. */ +const SYMBOL_KINDS = ["function", "class", "interface", "type-alias", "enum", "variable", "namespace", "alias"] as const; +const EDGE_KINDS = ["contains", "imports", "re-exports", "relates-to", "rationale-supports"] as const; + +/** FNXC:KnowledgeGraphDashboard 2026-08-13-23:30: Each result is a bounded graph slice; node-to-node buttons are the navigation model instead of a whole-graph canvas. */ +function EndpointButton({ id, onSelect }: { id: string; onSelect: (id: string) => void }) { + return ; +} + +function EdgeList({ title, edges, total, endpoint, onSelect }: { title: string; edges: KnowledgeGraphEdge[]; total: number; endpoint: "from" | "to"; onSelect: (id: string) => void }) { + return

{title} showing {edges.length} of {total}

{edges.length === 0 ?

None

:
    {edges.map((edge) =>
  • {edge.kind}{edge.provenance}
  • )}
}
; +} + +export function KnowledgeGraphPanel({ projectId, addToast }: { projectId?: string; addToast: (message: string, type: "success" | "error" | "info") => void }) { + const { t } = useTranslation("app"); + const graph = useKnowledgeGraph({ projectId, enabled: true }); + const [force, setForce] = useState(false); + useEffect(() => { if (graph.error) addToast(graph.error, "error"); }, [addToast, graph.error]); + + /* FNXC:KnowledgeGraphDashboard 2026-08-13-23:21: A failed status request is an error state, not an indefinite loading state, so operators can distinguish an unavailable graph artifact from a failed dashboard request. */ + if (!graph.status && graph.error) return
{graph.error}
; + if (!graph.status) return
{t("memory.graphLoading", "Loading knowledge graph…")}
; + if (!graph.status.available) return

{t("memory.graphUnavailable", "Graph unavailable")}: {graph.status.recoveryReason}

; + + const setFilter = (patch: Partial) => graph.setFilters({ ...graph.filters, ...patch, offset: 0 }); + const updateKinds = (kind: string) => { + const kinds = graph.filters.kinds ?? []; + setFilter({ kinds: kinds.includes(kind) ? kinds.filter((item) => item !== kind) : [...kinds, kind] }); + }; + const updateEdgeKinds = (kind: string) => { + const edgeKinds = graph.neighborOptions.edgeKinds ?? []; + graph.setNeighborOptions({ ...graph.neighborOptions, edgeKinds: edgeKinds.includes(kind) ? edgeKinds.filter((item) => item !== kind) : [...edgeKinds, kind] }); + }; + const selected = graph.selectedId; + const limits = graph.status.pathLimits; + const foundPath = graph.pathResult?.outcome === "found" ? graph.pathResult.path : null; + const foundHops = graph.pathResult?.outcome === "found" ? graph.pathResult.hops : null; + + return
+
{graph.status.nodeCount} {t("memory.graphNodes", "nodes")} · {graph.status.edgeCount} {t("memory.graphEdges", "edges")}{graph.status.graphDir}
Node kinds
{Object.entries(graph.status.nodeKindCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
Edge kinds
{Object.entries(graph.status.edgeKindCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
Provenance
{Object.entries(graph.status.provenanceCounts ?? {}).map(([kind, count]) => `${kind}: ${count}`).join(" · ") || "—"}
+ +
+
{NODE_KINDS.map((kind) => )}
+ setFilter({ pathPrefix: event.target.value })} /> + setFilter({ idPrefix: event.target.value })} /> + setFilter({ namePattern: event.target.value })} /> + + + + + +
+ +

{graph.nodes?.total ?? 0} {t("memory.graphResults", "results")}

{graph.loading ?

Searching…

: graph.nodes?.nodes.length ? <>
    {graph.nodes.nodes.map((node) =>
  • )}
:

{t("memory.graphNoResults", "No matching nodes.")}

}
+
{graph.detail ? <>

{graph.detail.node.name}

{graph.detail.node.id}

{graph.detail.node.ownerPath}:{graph.detail.node.source.line}

void graph.selectNode(id)} /> void graph.selectNode(id)} /> :

Select a node to inspect its edges.

}
+ + {graph.detail &&

Neighbors

{EDGE_KINDS.map((kind) => )}
{graph.neighborResults?.neighbors.length ?
    {graph.neighborResults.neighbors.map((neighbor) =>
  • distance {neighbor.distance} void graph.selectNode(id)} />
  • )}
:

No neighbors found.

}
} + +

Shortest path

graph.setPathInput({ ...graph.pathInput, fromId: event.target.value })} /> graph.setPathInput({ ...graph.pathInput, toId: event.target.value })} /> graph.setPathInput({ ...graph.pathInput, maxHops: Number(event.target.value) })} />{selected && <>}{foundPath &&
{foundHops} hops
{foundPath.nodes.map((node, index) => void graph.selectNode(id)} />{index < foundPath.edges.length && {foundPath.edges[index].kind}})}
}{graph.pathResult?.outcome === "not-found" &&

No path exists between these nodes.

}{graph.pathResult?.outcome === "limit-reached" &&

No path found within {graph.pathResult.maxHops} hops; {graph.pathResult.limit} was reached.

{graph.pathInput.maxHops < limits.maxHops && }
}{graph.error &&

{graph.error}

}
+
; +} diff --git a/packages/dashboard/app/components/MemoryView.tsx b/packages/dashboard/app/components/MemoryView.tsx index 947e94cf3a..4fe75b0a81 100644 --- a/packages/dashboard/app/components/MemoryView.tsx +++ b/packages/dashboard/app/components/MemoryView.tsx @@ -7,6 +7,7 @@ import type { MemoryFileInfo, MemoryRetrievalTestResult } from "../api"; import { FileEditor } from "./FileEditor"; import { ViewHeader } from "./ViewHeader"; import { useMemoryData } from "../hooks/useMemoryData"; +import { KnowledgeGraphPanel } from "./KnowledgeGraphPanel"; interface MemoryViewProps { projectId?: string; @@ -14,7 +15,7 @@ interface MemoryViewProps { onSendSelectionToTask?: (description: string) => void; } -type Tab = "working" | "insights" | "engines"; +type Tab = "working" | "insights" | "engines" | "graph"; /** Known category headers in the insights file */ const CATEGORY_HEADERS: Record = { @@ -413,6 +414,16 @@ export function MemoryView({ projectId, addToast, onSendSelectionToTask }: Memor > {t("memory.tabInsights", "Insights")} +