Files
SuperCharged-Claude-Code-Up…/dexto/packages/webui/lib/stores/README.md
admin b52318eeae feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-28 00:27:56 +04:00

238 lines
7.4 KiB
Markdown

# WebUI State Management Architecture
## Hybrid Context + Zustand Approach
The WebUI uses a **hybrid architecture** combining React Context and Zustand stores. This isn't redundant—each serves a distinct purpose.
## Why Zustand?
### The Key Reason: Event Handlers Run Outside React
```typescript
// Event handlers are plain functions, not React components
function handleLLMThinking(event: EventByName<'llm:thinking'>): void {
const { sessionId } = event;
// ✅ Imperative access - works outside React
useChatStore.getState().setProcessing(sessionId, true);
useAgentStore.getState().setThinking(sessionId);
}
```
**Event handlers can't use React hooks.** They need imperative state access from outside the component tree. Zustand provides this through `.getState()`.
With React Context, you'd need hacky global variables or complex callback registration—exactly what Zustand does, but type-safe and battle-tested.
### Secondary Benefits
1. **Granular subscriptions** - Components only re-render when their specific slice changes
2. **Multi-session state** - Efficient Map-based per-session storage
3. **No provider hell** - No need for nested provider components
4. **DevTools** - Time-travel debugging and state inspection
## Architecture Pattern
```
┌─────────────────────────────────────────┐
│ React Components │
│ - Use ChatContext for actions │
│ - Use Zustand stores for state │
└────────────┬────────────────────────────┘
┌──────┴──────┐
▼ ▼
┌──────────┐ ┌──────────┐
│ Context │ │ Zustand │
│ │ │ │
│ Actions │ │ State │
│ Hooks │ │ │
│ Query │ │ Pure │
└──────────┘ └────┬─────┘
┌────────┴────────┐
│ Event Handlers │
│ (outside React) │
└─────────────────┘
```
## When to Use What
### Use Zustand Stores For:
- ✅ State that needs access from **outside React** (event handlers, middleware)
-**Per-session state** (messages, errors, processing)
-**High-frequency updates** (streaming, real-time events)
- ✅ State accessed by **many components** (current session, notifications)
**Examples**: `chatStore`, `sessionStore`, `approvalStore`, `notificationStore`, `agentStore`
### Use React Context For:
-**React-specific orchestration** (combining hooks, effects, callbacks)
-**API integration** (TanStack Query, mutations)
-**Lifecycle management** (connection setup, cleanup)
-**Derived state** that depends on multiple sources
**Examples**: `ChatContext`, `EventBusProvider`
## Store Organization
```
lib/stores/
├── README.md # This file
├── index.ts # Barrel exports
├── chatStore.ts # Per-session messages, streaming, errors
├── sessionStore.ts # Current session, navigation state
├── approvalStore.ts # Approval requests with queueing
├── notificationStore.ts # Toast notifications
├── agentStore.ts # Agent status, connection, heartbeat
└── eventLogStore.ts # Event history for debugging
```
## Usage Patterns
### Reading State in Components
```typescript
import { useChatStore } from '@/lib/stores/chatStore';
import { useSessionStore } from '@/lib/stores/sessionStore';
function ChatApp() {
// Granular subscription - only re-renders when messages change
const messages = useChatStore((s) => {
if (!currentSessionId) return EMPTY_MESSAGES;
const session = s.sessions.get(currentSessionId);
return session?.messages ?? EMPTY_MESSAGES;
});
// Simple value access
const currentSessionId = useSessionStore((s) => s.currentSessionId);
// ...
}
```
### Updating State from Event Handlers
```typescript
import { useChatStore } from '@/lib/stores/chatStore';
import { useAgentStore } from '@/lib/stores/agentStore';
function handleLLMChunk(event: EventByName<'llm:chunk'>): void {
const { sessionId, content, chunkType } = event;
// Imperative access from outside React
const chatStore = useChatStore.getState();
chatStore.appendToStreamingMessage(sessionId, content, chunkType);
}
```
### Updating State from Context/Components
```typescript
// In ChatContext or components with hooks
const setProcessing = useCallback((sessionId: string, isProcessing: boolean) => {
useChatStore.getState().setProcessing(sessionId, isProcessing);
}, []);
```
## Event Flow
```
SSE Stream (server)
useChat.ts (line 219)
eventBus.dispatch(event)
Middleware Pipeline (logging, activity, notifications)
Event Handlers (handlers.ts)
Zustand Stores (.getState() imperative updates)
React Components (via selectors, triggers re-render)
```
## Best Practices
### 1. Use Stable References for Empty Arrays
```typescript
// ✅ DO: Prevents infinite re-render loops
const EMPTY_MESSAGES: Message[] = [];
const messages = useChatStore((s) => {
if (!currentSessionId) return EMPTY_MESSAGES;
return s.sessions.get(currentSessionId)?.messages ?? EMPTY_MESSAGES;
});
// ❌ DON'T: Creates new array reference on every render
const messages = useChatStore((s) => {
if (!currentSessionId) return []; // New reference each time!
return s.sessions.get(currentSessionId)?.messages ?? [];
});
```
### 2. Selector Efficiency
```typescript
// ✅ DO: Narrow selectors for specific data
const processing = useChatStore((s) => {
const session = s.sessions.get(currentSessionId);
return session?.processing ?? false;
});
// ❌ DON'T: Selecting entire store triggers unnecessary re-renders
const store = useChatStore(); // Re-renders on any store change!
const processing = store.sessions.get(currentSessionId)?.processing;
```
### 3. Imperative vs Hook Usage
```typescript
// ✅ In React components - use hook
function MyComponent() {
const messages = useChatStore((s) => s.getMessages(sessionId));
// ...
}
// ✅ In event handlers - use .getState()
function handleEvent(event) {
useChatStore.getState().addMessage(sessionId, message);
}
// ❌ DON'T: Use hooks outside React
function handleEvent(event) {
const store = useChatStore(); // ❌ Can't use hooks here!
}
```
## Testing
All stores have comprehensive test coverage. See `*.test.ts` files for examples:
```typescript
import { useChatStore } from './chatStore';
describe('chatStore', () => {
beforeEach(() => {
useChatStore.setState({ sessions: new Map() });
});
it('should add message to session', () => {
const store = useChatStore.getState();
store.addMessage('session-1', { id: 'msg-1', ... });
const messages = store.getMessages('session-1');
expect(messages).toHaveLength(1);
});
});
```
## Related Documentation
- Event system: `packages/webui/lib/events/README.md`
- Event handlers: `packages/webui/lib/events/handlers.ts`
- Event middleware: `packages/webui/lib/events/middleware/`
- Main architecture: `/docs` (to be updated)