# Dashboard State Machine: Zustand Store & View Modes

> The dashboard's single Zustand store (store.ts) owns all runtime state: the loaded KnowledgeGraph, active Persona (non-technical / junior / experienced), ViewMode (structural / domain / knowledge), FilterState (node types, complexities, layers, edge categories), selected node, search results via SearchEngine (Fuse.js fuzzy search on name/tags/summary/languageNotes), and the React Flow instance. The store is the single source of truth — components never hold local graph state. Key boundary: dashboard imports only from @understand-anything/core/search, /types, and /schema (browser-safe subpath exports); never the core main entry point, which pulls in Node.js modules.

- Repository: Lum1104/Understand-Anything
- GitHub: https://github.com/Lum1104/Understand-Anything
- Human wiki: https://grok-wiki.com/public/wiki/lum1104-understand-anything-3b923df96896
- Complete Markdown: https://grok-wiki.com/public/wiki/lum1104-understand-anything-3b923df96896/llms-full.txt

## Source Files

- `understand-anything-plugin/packages/dashboard/src/store.ts`
- `understand-anything-plugin/packages/core/src/search.ts`
- `understand-anything-plugin/packages/dashboard/src/App.tsx`
- `understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx`
- `understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx`
- `understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:

- [understand-anything-plugin/packages/dashboard/src/store.ts](understand-anything-plugin/packages/dashboard/src/store.ts)
- [understand-anything-plugin/packages/core/src/search.ts](understand-anything-plugin/packages/core/src/search.ts)
- [understand-anything-plugin/packages/dashboard/src/App.tsx](understand-anything-plugin/packages/dashboard/src/App.tsx)
- [understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx](understand-anything-plugin/packages/dashboard/src/components/GraphView.tsx)
- [understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx](understand-anything-plugin/packages/dashboard/src/components/NodeInfo.tsx)
- [understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx](understand-anything-plugin/packages/dashboard/src/components/CodeViewer.tsx)
</details>

# Dashboard State Machine: Zustand Store & View Modes

The Understand-Anything dashboard is driven by a single Zustand store (`useDashboardStore`) that owns every piece of runtime state: the loaded knowledge graph, active persona, view mode, filter configuration, selected node, search results, navigation history, code viewer visibility, tour progress, diff overlay, container expand/collapse state, and the React Flow instance. No component holds its own graph-level state; all reads and mutations flow through this store.

This architecture matters because the graph is large and highly interconnected — multiple independent UI panels (graph canvas, sidebar, search bar, code viewer, tour overlay) must stay synchronized without prop drilling. The store is also the enforcement point for two structural invariants: the `nodeIdToLayerId` / `nodeIdToLayerIds` dual-index design that drives layer navigation, and the cache-invalidation discipline that keeps ELK layout results consistent when the visible node set changes.

---

## Store Shape

The `DashboardStore` interface (∼140 fields and methods) is defined in full at the top of `store.ts`. The logical groups are:

| Group | Key fields |
|---|---|
| **Graph data** | `graph`, `nodesById`, `nodeIdToLayerId`, `nodeIdToLayerIds` |
| **View mode** | `viewMode`, `isKnowledgeGraph`, `domainGraph`, `activeDomainId` |
| **Navigation** | `navigationLevel`, `activeLayerId`, `selectedNodeId`, `nodeHistory`, `focusNodeId` |
| **Search** | `searchQuery`, `searchResults`, `searchEngine`, `searchMode` |
| **Persona** | `persona` |
| **Filters** | `filters` (FilterState), `nodeTypeFilters` (NodeCategory booleans), `detailLevel` |
| **Code viewer** | `codeViewerOpen`, `codeViewerNodeId`, `codeViewerExpanded` |
| **Tour** | `tourActive`, `currentTourStep`, `tourHighlightedNodeIds`, `tourFitPending` |
| **Diff overlay** | `diffMode`, `changedNodeIds`, `affectedNodeIds` |
| **Layout cache** | `containerLayoutCache`, `containerSizeMemory`, `expandedContainers`, `stage1Tick` |
| **UI panels** | `filterPanelOpen`, `exportMenuOpen`, `pathFinderOpen` |
| **React Flow** | `reactFlowInstance` |
| **Issues** | `layoutIssues` |

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:100-240]()

---

## Graph Indexes: The Dual-Layer Map

When a `KnowledgeGraph` is loaded via `setGraph`, the helper `buildGraphIndexes` constructs three lookup structures:

```ts
// understand-anything-plugin/packages/dashboard/src/store.ts
function buildGraphIndexes(graph: KnowledgeGraph): {
  nodesById: Map<string, GraphNode>;
  nodeIdToLayerId: Map<string, string>;    // first-matching-layer wins
  nodeIdToLayerIds: Map<string, Set<string>>; // every layer the node belongs to
}
```

The design comment in the source explains why two separate maps are needed:

- **`nodeIdToLayerId`** uses "first matching layer wins" semantics — if a node appears in multiple layers, only the first occurrence in `graph.layers` order is recorded. This is correct for navigation: drilling into a layer or computing a tour's target layer needs exactly one canonical answer.
- **`nodeIdToLayerIds`** records every layer membership. This is correct for filtering: a node in layer L1 and L2 must remain visible when only L2 is selected. Collapsing to first-wins for filtering would silently hide nodes.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:54-95]()

---

## `setGraph`: The Initialization Transition

`setGraph` is the entry point for all graph-loading logic. It is called from `App.tsx` after the fetched JSON passes schema validation:

```ts
// understand-anything-plugin/packages/dashboard/src/App.tsx
fetch(dataUrl("knowledge-graph.json", accessToken))
  .then(res => res.json())
  .then((data: unknown) => {
    const result = validateGraph(data);
    if (result.success && result.data) {
      setGraph(result.data);
      ...
      if ((data as Record<string, unknown>).kind === "knowledge") {
        useDashboardStore.getState().setViewMode("knowledge");
        useDashboardStore.getState().setIsKnowledgeGraph(true);
      }
    }
  });
```

Inside `setGraph`, the store:
1. Runs `buildGraphIndexes` to build the three lookup maps.
2. Instantiates a new `SearchEngine` over the graph nodes.
3. Re-runs any pending `searchQuery` against the new engine.
4. Preserves the current `"domain"` view mode if a domain graph is already loaded (`keepDomainView`).
5. Resets navigation, selection, focus, and all layout caches to a clean initial state.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:365-394](), [understand-anything-plugin/packages/dashboard/src/App.tsx:132-163]()

---

## View Modes

Three view modes control which graph canvas component is mounted:

| `ViewMode` value | Graph component | When active |
|---|---|---|
| `"structural"` | `GraphView` | Default; shows the full structural graph with ELK layout |
| `"domain"` | `DomainGraphView` | When a `domain-graph.json` is available and the user switches to Domain view |
| `"knowledge"` | `KnowledgeGraphView` | When the loaded graph has `kind === "knowledge"` |

The App routes to the correct canvas at render time:

```tsx
// understand-anything-plugin/packages/dashboard/src/App.tsx
{viewMode === "knowledge" ? (
  <KnowledgeGraphView />
) : viewMode === "domain" && domainGraph ? (
  <DomainGraphView />
) : (
  <GraphView />
)}
```

`setViewMode` resets `selectedNodeId`, `focusNodeId`, and the code viewer when called — the new view starts clean. The domain graph is fetched separately from `domain-graph.json`; if absent, the domain mode button is hidden entirely (guarded by `domainGraph !== null` in App.tsx line 452).

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:685-694](), [understand-anything-plugin/packages/dashboard/src/App.tsx:638-644](), [understand-anything-plugin/packages/dashboard/src/App.tsx:452-483]()

---

## Navigation State Machine

Navigation within the structural graph has two levels, controlled by `navigationLevel: "overview" | "layer-detail"`.

```
┌──────────────────────────────────────────────────────────┐
│  navigationLevel = "overview"                            │
│  All layer-cluster nodes visible; no drill-down active   │
└────────────────┬─────────────────────────────────────────┘
                 │  drillIntoLayer(layerId)
                 │  navigateToNodeInLayer(nodeId) [if node has a layer]
                 ▼
┌──────────────────────────────────────────────────────────┐
│  navigationLevel = "layer-detail"                        │
│  activeLayerId = <id>; only this layer's nodes shown     │
└────────────────┬─────────────────────────────────────────┘
                 │  navigateToOverview()
                 │  Escape key (if nothing else to dismiss)
                 ▼
             (back to overview)
```

`navigateToNodeInLayer` resolves the node's canonical layer via `nodeIdToLayerId`, then sets both `navigationLevel = "layer-detail"` and `activeLayerId`. If the node has no layer, it only sets `selectedNodeId`.

Node selection history is maintained as a capped stack (`MAX_HISTORY = 50`). `selectNode`, `navigateToNode`, `goBackNode`, and `navigateToHistoryIndex` all manage this stack, pushing the outgoing `selectedNodeId` before navigating forward and popping it when going back.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:97-98](), [understand-anything-plugin/packages/dashboard/src/store.ts:409-490]()

---

## Persona

The `persona` field takes one of three string literals: `"non-technical"`, `"junior"`, or `"experienced"`. It is declared at the top of `store.ts`:

```ts
export type Persona = "non-technical" | "junior" | "experienced";
```

The default is `"junior"`. Changing persona via `setPersona` also clears the container layout cache — persona drives which node types are visible (and thus which containers have which children), so cached positions become invalid.

In `App.tsx`, persona feeds a derived `isLearnMode` flag: `const isLearnMode = tourActive || persona === "junior"`. When `isLearnMode` is true, the sidebar renders `LearnPanel` below `NodeInfo` instead of `ProjectOverview`.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:12](), [understand-anything-plugin/packages/dashboard/src/store.ts:534-542](), [understand-anything-plugin/packages/dashboard/src/App.tsx:391-402]()

---

## FilterState

Filtering is expressed through two complementary mechanisms:

### 1. `FilterState` (advanced filter panel)
```ts
export interface FilterState {
  nodeTypes: Set<NodeType>;       // which fine-grained types are visible
  complexities: Set<Complexity>;  // "simple" | "moderate" | "complex"
  layerIds: Set<string>;          // layer membership filter
  edgeCategories: Set<EdgeCategory>; // which edge groups to show
}
```

All sets default to "all enabled" (`ALL_NODE_TYPES`, `ALL_COMPLEXITIES`, etc.). `hasActiveFilters()` checks whether any set has been narrowed from its default to drive UI indicator badges.

### 2. `nodeTypeFilters` (category toggles in the header)
A flat `Record<NodeCategory, boolean>` with categories: `"code" | "config" | "docs" | "infra" | "data" | "domain" | "knowledge"`. These map to groups of `NodeType` values as defined by `NODE_TYPE_TO_CATEGORY` in `GraphView.tsx`.

Both mechanisms are in the store; `GraphView` applies them when deriving the visible node set for React Flow.

### Edge category grouping
The `EDGE_CATEGORY_MAP` constant provides the canonical grouping of edge types into categories:

```ts
export const EDGE_CATEGORY_MAP: Record<EdgeCategory, string[]> = {
  structural: ["imports", "exports", "contains", "inherits", "implements"],
  behavioral: ["calls", "subscribes", "publishes", "middleware"],
  "data-flow": ["reads_from", "writes_to", "transforms", "validates"],
  dependencies: ["depends_on", "tested_by", "configures"],
  semantic: ["related", "similar_to"],
  infrastructure: ["deploys", "serves", "provisions", "triggers", ...],
  domain: ["contains_flow", "flow_step", "cross_domain"],
  knowledge: ["cites", "contradicts", "builds_on", "exemplifies", ...],
};
```

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:20-49](), [understand-anything-plugin/packages/dashboard/src/store.ts:596-602]()

---

## SearchEngine Integration

`SearchEngine` is instantiated in `setGraph` and stored as `searchEngine` in the store. It wraps Fuse.js with a fixed field-weight configuration:

| Field | Weight |
|---|---|
| `name` | 0.4 |
| `tags` | 0.3 |
| `summary` | 0.2 |
| `languageNotes` | 0.1 |

```ts
// understand-anything-plugin/packages/core/src/search.ts
const FUSE_OPTIONS: IFuseOptions<GraphNode> = {
  keys: [
    { name: "name", weight: 0.4 },
    { name: "tags", weight: 0.3 },
    { name: "summary", weight: 0.2 },
    { name: "languageNotes", weight: 0.1 },
  ],
  threshold: 0.4,
  includeScore: true,
  ignoreLocation: true,
  useExtendedSearch: true,
};
```

When `setSearchQuery` is called, the store calls `engine.search(query)` if both `searchEngine` is non-null and `query.trim()` is non-empty; otherwise `searchResults` is set to `[]`. The `searchMode` field (`"fuzzy" | "semantic"`) is stored but currently both modes route to the same Fuse.js engine — a comment in `setSearchQuery` marks this as the placeholder for a future embeddings-based path.

Sources: [understand-anything-plugin/packages/core/src/search.ts:14-25](), [understand-anything-plugin/packages/dashboard/src/store.ts:520-532]()

---

## Code Viewer State

The code viewer has three states that form a mini state machine:

```
closed ──openCodeViewer(nodeId)──► collapsed-overlay
                                       │
                               expandCodeViewer()
                                       │
                                       ▼
                                 expanded-modal
                                       │
                               collapseCodeViewer()
                                       │
                                       ▼
                               collapsed-overlay
                                       │
                               closeCodeViewer()
                                       │
                                       ▼
                                    closed
```

`App.tsx` renders the collapsed overlay as an absolutely-positioned 40vh panel at the bottom, and the expanded state as a fixed full-screen modal with a backdrop. Both mount the `CodeViewer` component (lazy-loaded), which fetches source via `/file-content.json?token=…&path=…`.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:544-549](), [understand-anything-plugin/packages/dashboard/src/App.tsx:656-684]()

---

## Tour State

The tour is driven by `graph.tour` — an array of `TourStep` objects sorted by `step.order` at runtime. `startTour`, `nextTourStep`, `prevTourStep`, and `setTourStep` all call `navigateTourToLayer` to automatically drill into the correct layer for the first highlighted node in each step. When a step crosses layers, `layerResetIfChanged` clears the container layout cache so the new layer's ELK layout runs fresh.

`tourFitPending` is a boolean flag set to `true` while the graph is waiting for highlighted nodes to materialize after a layer change; it drives a "Computing layout…" overlay in the UI.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:247-287](), [understand-anything-plugin/packages/dashboard/src/store.ts:604-670]()

---

## Layout Cache Invalidation Discipline

Every state transition that changes the visible node set **must** reset the container layout cache. The store enforces this consistently by including these four resets in every relevant action:

```ts
containerLayoutCache: new Map(),
containerSizeMemory: new Map(),
expandedContainers: new Set(),
pendingFocusContainer: null,
```

This pattern appears in: `drillIntoLayer`, `navigateToOverview`, `setFocusNode`, `setPersona`, `toggleNodeTypeFilter`, `setDetailLevel`, `toggleShowFunctionsInClassView`, `startTour` / `setTourStep` (when layer changes), and `setGraph`. Failure to reset in any of these would cause ELK to position new nodes using stale positions from the previous visible set.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:474-505](), [understand-anything-plugin/packages/dashboard/src/store.ts:327-363]()

---

## Module Boundary: Browser-Safe Core Imports

The dashboard enforces a strict import boundary: it may only import from core's browser-safe subpath exports, never from the main entry point. The store itself demonstrates this at its top:

```ts
import { SearchEngine } from "@understand-anything/core/search";
import type { SearchResult } from "@understand-anything/core/search";
import type { GraphIssue } from "@understand-anything/core/schema";
import type { GraphNode, KnowledgeGraph, TourStep } from "@understand-anything/core/types";
```

The main core entry point pulls in Node.js modules (tree-sitter, filesystem utilities) that would fail in the browser. Using subpath exports (`/search`, `/types`, `/schema`) keeps the browser bundle free of those dependencies. This boundary is enforced by TypeScript module resolution and documented in CLAUDE.md.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:1-10]()

---

## Summary

The `useDashboardStore` in `store.ts` is the single source of truth for all dashboard runtime state. Its design encodes several invariants: the dual-index layer maps ensure correctness for both navigation (first-layer-wins) and filtering (all-layers membership); layout cache resets are co-located with every state mutation that changes node visibility; and view mode transitions start with a clean selection state. `SearchEngine` (backed by Fuse.js with weighted fields) is embedded in the store and rebuilt on each graph load. The import boundary — all dashboard code importing only from core's browser-safe subpaths — is enforced structurally rather than by convention.

Sources: [understand-anything-plugin/packages/dashboard/src/store.ts:289-393]()
