# State Tree Architecture (MANDATORY) All application state lives in a single hierarchical state tree. Components are pure renderers — they read from the tree via getter functions and dispatch actions. No component-local `$state` except DOM refs (`bind:this`). ## Tree Structure ``` AppState (root) → app-state.svelte.ts ├── theme, locale, appReady ├── ui: settings, palette, search, wizard, notifications ├── workspace │ ├── groups[], activeGroupId │ └── projects[] (per project ↓) │ ├── agent: session, messages, status, cost │ ├── terminals: tabs[], collapsed, activeTabId │ ├── files: tree, selectedPath, content, dirty │ ├── comms: channels[], messages[], mode │ ├── tasks: items[], dragTarget │ └── tab: activeTab, activatedTabs └── health: per-project trackers, attention queue ``` ## Rules ### 1. State ownership follows the tree Each node owns its children. A node NEVER reads siblings or ancestors directly — it receives what it needs via getter functions scoped to its level. ```typescript // RIGHT — scoped access through parent function getProjectAgent(projectId: string) { return getProjectState(projectId).agent; } // WRONG — cross-branch reach function getAgentForComms() { return agentStore.getSession(commsStore.activeAgentId); } ``` ### 2. Actions bubble up, state flows down Components dispatch actions to the store that owns the state. State changes propagate downward via Svelte's reactive reads. ```typescript // Component dispatches action onclick={() => projectActions.setActiveTab(projectId, 'files')} // Store mutates owned state function setActiveTab(projectId: string, tab: ProjectTab) { getProjectState(projectId).tab.activeTab = tab; } ``` ### 3. One store file per tree level | Level | File | Owns | |-------|------|------| | Root | `app-state.svelte.ts` | theme, locale, appReady | | UI | `ui-store.svelte.ts` | drawers, overlays, modals | | Workspace | `workspace-store.svelte.ts` | groups, projects list | | Project | `project-state.svelte.ts` | per-project sub-states | | Health | `health-store.svelte.ts` | trackers, attention | ### 4. Per-project state is a typed object, not scattered stores ```typescript interface ProjectState { agent: AgentState; terminals: TerminalState; files: FileState; comms: CommsState; tasks: TaskState; tab: TabState; } // Accessed via: function getProjectState(id: string): ProjectState ``` ### 5. No `$state` in components Components use ONLY: - `$props()` for parent-passed values - Getter functions from stores (e.g., `getProjectAgent(id)`) - Action functions from stores (e.g., `projectActions.sendMessage(id, text)`) - `bind:this` for DOM element references (the sole exception) - Ephemeral interaction state (hover, drag coordinates) via plain `let` — NOT `$state` ### 6. No `$derived` with new objects (Rule 57) All computed values are plain functions. See Rule 57 for details. ### 7. Initialization in `onMount`, never `$effect` All async data loading, timer setup, and event listener registration goes in `onMount`. See Rule 57. ## Adding New State 1. Identify which tree node owns it 2. Add the field to that node's interface 3. Add getter + action functions in the store file 4. Components read via getter, mutate via action 5. NEVER add `$state` to a `.svelte` component file