104 lines
3.4 KiB
Markdown
104 lines
3.4 KiB
Markdown
# 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
|