3.4 KiB
3.4 KiB
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.
// 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.
// 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
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:thisfor 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
- Identify which tree node owns it
- Add the field to that node's interface
- Add getter + action functions in the store file
- Components read via getter, mutate via action
- NEVER add
$stateto a.sveltecomponent file