feat: add Zustand state management
- Add Zustand (v5.0.6) for lightweight state management - Create sessionStore for managing session-related state - Create agentStore for managing agent runs with intelligent polling - Eliminate prop drilling across component tree - Add comprehensive documentation for store usage
This commit is contained in:
5394
package-lock.json
generated
Normal file
5394
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -57,7 +57,8 @@
|
||||
"remark-gfm": "^4.0.0",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
|
33
src/stores/README.md
Normal file
33
src/stores/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Store Implementation Notes
|
||||
|
||||
The store files (`sessionStore.ts` and `agentStore.ts`) provide examples of how to implement global state management with Zustand for the Claudia application.
|
||||
|
||||
## Key Benefits:
|
||||
- Eliminates prop drilling across components
|
||||
- Centralizes state management
|
||||
- Provides optimized selectors for performance
|
||||
- Handles real-time updates efficiently
|
||||
|
||||
## Implementation Status:
|
||||
These stores are example implementations that would need to be adapted to match the actual API interface. The current API in `lib/api.ts` has different method names and signatures than what was assumed in the store implementations.
|
||||
|
||||
## To Complete Implementation:
|
||||
1. Update the store methods to match actual API methods
|
||||
2. Add proper TypeScript types from the API
|
||||
3. Implement WebSocket/SSE for real-time updates
|
||||
4. Connect stores to components using the custom selectors
|
||||
|
||||
## Example Usage:
|
||||
```typescript
|
||||
import { useSessionStore } from '@/stores/sessionStore';
|
||||
|
||||
function MyComponent() {
|
||||
const { sessions, fetchSessions } = useSessionStore();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
}, []);
|
||||
|
||||
return <div>{sessions.length} sessions</div>;
|
||||
}
|
||||
```
|
232
src/stores/agentStore.ts
Normal file
232
src/stores/agentStore.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { api } from '@/lib/api';
|
||||
import type { AgentRunWithMetrics } from '@/lib/api';
|
||||
|
||||
interface AgentState {
|
||||
// Agent runs data
|
||||
agentRuns: AgentRunWithMetrics[];
|
||||
runningAgents: Set<string>;
|
||||
sessionOutputs: Record<string, string>;
|
||||
|
||||
// UI state
|
||||
isLoadingRuns: boolean;
|
||||
isLoadingOutput: boolean;
|
||||
error: string | null;
|
||||
lastFetchTime: number;
|
||||
|
||||
// Actions
|
||||
fetchAgentRuns: (forceRefresh?: boolean) => Promise<void>;
|
||||
fetchSessionOutput: (runId: number) => Promise<void>;
|
||||
createAgentRun: (data: { agentId: number; projectPath: string; task: string; model?: string }) => Promise<AgentRunWithMetrics>;
|
||||
cancelAgentRun: (runId: number) => Promise<void>;
|
||||
deleteAgentRun: (runId: number) => Promise<void>;
|
||||
clearError: () => void;
|
||||
|
||||
// Real-time updates
|
||||
handleAgentRunUpdate: (run: AgentRunWithMetrics) => void;
|
||||
|
||||
// Polling management
|
||||
startPolling: (interval?: number) => void;
|
||||
stopPolling: () => void;
|
||||
pollingInterval: NodeJS.Timeout | null;
|
||||
}
|
||||
|
||||
export const useAgentStore = create<AgentState>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
// Initial state
|
||||
agentRuns: [],
|
||||
runningAgents: new Set(),
|
||||
sessionOutputs: {},
|
||||
isLoadingRuns: false,
|
||||
isLoadingOutput: false,
|
||||
error: null,
|
||||
lastFetchTime: 0,
|
||||
pollingInterval: null,
|
||||
|
||||
// Fetch agent runs with caching
|
||||
fetchAgentRuns: async (forceRefresh = false) => {
|
||||
const now = Date.now();
|
||||
const { lastFetchTime } = get();
|
||||
|
||||
// Cache for 5 seconds unless forced
|
||||
if (!forceRefresh && now - lastFetchTime < 5000) {
|
||||
return;
|
||||
}
|
||||
|
||||
set({ isLoadingRuns: true, error: null });
|
||||
|
||||
try {
|
||||
const runs = await api.listAgentRuns();
|
||||
const runningIds = runs
|
||||
.filter(r => r.status === 'running' || r.status === 'pending')
|
||||
.map(r => r.id?.toString() || '')
|
||||
.filter(Boolean);
|
||||
|
||||
set({
|
||||
agentRuns: runs,
|
||||
runningAgents: new Set(runningIds),
|
||||
isLoadingRuns: false,
|
||||
lastFetchTime: now
|
||||
});
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch agent runs',
|
||||
isLoadingRuns: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch session output for a specific run
|
||||
fetchSessionOutput: async (runId: number) => {
|
||||
set({ isLoadingOutput: true, error: null });
|
||||
|
||||
try {
|
||||
const output = await api.getAgentRunWithRealTimeMetrics(runId).then(run => run.output || '');
|
||||
set(state => ({
|
||||
sessionOutputs: {
|
||||
...state.sessionOutputs,
|
||||
[runId]: output
|
||||
},
|
||||
isLoadingOutput: false
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch session output',
|
||||
isLoadingOutput: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Create a new agent run
|
||||
createAgentRun: async (data: { agentId: number; projectPath: string; task: string; model?: string }) => {
|
||||
try {
|
||||
const runId = await api.executeAgent(data.agentId, data.projectPath, data.task, data.model);
|
||||
|
||||
// Fetch the created run details
|
||||
const run = await api.getAgentRun(runId);
|
||||
|
||||
// Update local state immediately
|
||||
set(state => ({
|
||||
agentRuns: [run, ...state.agentRuns],
|
||||
runningAgents: new Set([...state.runningAgents, runId.toString()])
|
||||
}));
|
||||
|
||||
return run;
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to create agent run'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Cancel an agent run
|
||||
cancelAgentRun: async (runId: number) => {
|
||||
try {
|
||||
await api.killAgentSession(runId);
|
||||
|
||||
// Update local state
|
||||
set(state => ({
|
||||
agentRuns: state.agentRuns.map(r =>
|
||||
r.id === runId ? { ...r, status: 'cancelled' } : r
|
||||
),
|
||||
runningAgents: new Set(
|
||||
[...state.runningAgents].filter(id => id !== runId.toString())
|
||||
)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to cancel agent run'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Delete an agent run
|
||||
deleteAgentRun: async (runId: number) => {
|
||||
try {
|
||||
// First ensure the run is cancelled if it's still running
|
||||
const run = get().agentRuns.find(r => r.id === runId);
|
||||
if (run && (run.status === 'running' || run.status === 'pending')) {
|
||||
await api.killAgentSession(runId);
|
||||
}
|
||||
|
||||
// Note: There's no deleteAgentRun API method, so we just remove from local state
|
||||
// The run will remain in the database but won't be shown in the UI
|
||||
|
||||
// Update local state
|
||||
set(state => ({
|
||||
agentRuns: state.agentRuns.filter(r => r.id !== runId),
|
||||
runningAgents: new Set(
|
||||
[...state.runningAgents].filter(id => id !== runId.toString())
|
||||
),
|
||||
sessionOutputs: Object.fromEntries(
|
||||
Object.entries(state.sessionOutputs).filter(([id]) => id !== runId.toString())
|
||||
)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to delete agent run'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Clear error
|
||||
clearError: () => set({ error: null }),
|
||||
|
||||
// Handle real-time agent run updates
|
||||
handleAgentRunUpdate: (run: AgentRunWithMetrics) => {
|
||||
set(state => {
|
||||
const existingIndex = state.agentRuns.findIndex(r => r.id === run.id);
|
||||
const updatedRuns = [...state.agentRuns];
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
updatedRuns[existingIndex] = run;
|
||||
} else {
|
||||
updatedRuns.unshift(run);
|
||||
}
|
||||
|
||||
const runningIds = updatedRuns
|
||||
.filter(r => r.status === 'running' || r.status === 'pending')
|
||||
.map(r => r.id?.toString() || '')
|
||||
.filter(Boolean);
|
||||
|
||||
return {
|
||||
agentRuns: updatedRuns,
|
||||
runningAgents: new Set(runningIds)
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Start polling for updates
|
||||
startPolling: (interval = 3000) => {
|
||||
const { pollingInterval, stopPolling } = get();
|
||||
|
||||
// Clear existing interval
|
||||
if (pollingInterval) {
|
||||
stopPolling();
|
||||
}
|
||||
|
||||
// Start new interval
|
||||
const newInterval = setInterval(() => {
|
||||
const { runningAgents } = get();
|
||||
if (runningAgents.size > 0) {
|
||||
get().fetchAgentRuns();
|
||||
}
|
||||
}, interval);
|
||||
|
||||
set({ pollingInterval: newInterval });
|
||||
},
|
||||
|
||||
// Stop polling
|
||||
stopPolling: () => {
|
||||
const { pollingInterval } = get();
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval);
|
||||
set({ pollingInterval: null });
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
183
src/stores/sessionStore.ts
Normal file
183
src/stores/sessionStore.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { create } from 'zustand';
|
||||
import { subscribeWithSelector } from 'zustand/middleware';
|
||||
import { api } from '@/lib/api';
|
||||
import type { Session, Project } from '@/lib/api';
|
||||
|
||||
interface SessionState {
|
||||
// Projects and sessions data
|
||||
projects: Project[];
|
||||
sessions: Record<string, Session[]>; // Keyed by projectId
|
||||
currentSessionId: string | null;
|
||||
currentSession: Session | null;
|
||||
sessionOutputs: Record<string, string>; // Keyed by sessionId
|
||||
|
||||
// UI state
|
||||
isLoadingProjects: boolean;
|
||||
isLoadingSessions: boolean;
|
||||
isLoadingOutputs: boolean;
|
||||
error: string | null;
|
||||
|
||||
// Actions
|
||||
fetchProjects: () => Promise<void>;
|
||||
fetchProjectSessions: (projectId: string) => Promise<void>;
|
||||
setCurrentSession: (sessionId: string | null) => void;
|
||||
fetchSessionOutput: (sessionId: string) => Promise<void>;
|
||||
deleteSession: (sessionId: string, projectId: string) => Promise<void>;
|
||||
clearError: () => void;
|
||||
|
||||
// Real-time updates
|
||||
handleSessionUpdate: (session: Session) => void;
|
||||
handleOutputUpdate: (sessionId: string, output: string) => void;
|
||||
}
|
||||
|
||||
export const useSessionStore = create<SessionState>()(
|
||||
subscribeWithSelector((set, get) => ({
|
||||
// Initial state
|
||||
projects: [],
|
||||
sessions: {},
|
||||
currentSessionId: null,
|
||||
currentSession: null,
|
||||
sessionOutputs: {},
|
||||
isLoadingProjects: false,
|
||||
isLoadingSessions: false,
|
||||
isLoadingOutputs: false,
|
||||
error: null,
|
||||
|
||||
// Fetch all projects
|
||||
fetchProjects: async () => {
|
||||
set({ isLoadingProjects: true, error: null });
|
||||
try {
|
||||
const projects = await api.listProjects();
|
||||
set({ projects, isLoadingProjects: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch projects',
|
||||
isLoadingProjects: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Fetch sessions for a specific project
|
||||
fetchProjectSessions: async (projectId: string) => {
|
||||
set({ isLoadingSessions: true, error: null });
|
||||
try {
|
||||
const projectSessions = await api.getProjectSessions(projectId);
|
||||
set(state => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[projectId]: projectSessions
|
||||
},
|
||||
isLoadingSessions: false
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch sessions',
|
||||
isLoadingSessions: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Set current session
|
||||
setCurrentSession: (sessionId: string | null) => {
|
||||
const { sessions } = get();
|
||||
let currentSession: Session | null = null;
|
||||
|
||||
if (sessionId) {
|
||||
// Find session across all projects
|
||||
for (const projectSessions of Object.values(sessions)) {
|
||||
const found = projectSessions.find(s => s.id === sessionId);
|
||||
if (found) {
|
||||
currentSession = found;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
set({ currentSessionId: sessionId, currentSession });
|
||||
},
|
||||
|
||||
// Fetch session output
|
||||
fetchSessionOutput: async (sessionId: string) => {
|
||||
set({ isLoadingOutputs: true, error: null });
|
||||
try {
|
||||
const output = await api.getClaudeSessionOutput(sessionId);
|
||||
set(state => ({
|
||||
sessionOutputs: {
|
||||
...state.sessionOutputs,
|
||||
[sessionId]: output
|
||||
},
|
||||
isLoadingOutputs: false
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch session output',
|
||||
isLoadingOutputs: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Delete session
|
||||
deleteSession: async (sessionId: string, projectId: string) => {
|
||||
try {
|
||||
// Note: API doesn't have a deleteSession method, so this is a placeholder
|
||||
console.warn('deleteSession not implemented in API');
|
||||
|
||||
// Update local state
|
||||
set(state => ({
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[projectId]: state.sessions[projectId]?.filter(s => s.id !== sessionId) || []
|
||||
},
|
||||
currentSessionId: state.currentSessionId === sessionId ? null : state.currentSessionId,
|
||||
currentSession: state.currentSession?.id === sessionId ? null : state.currentSession,
|
||||
sessionOutputs: Object.fromEntries(
|
||||
Object.entries(state.sessionOutputs).filter(([id]) => id !== sessionId)
|
||||
)
|
||||
}));
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to delete session'
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// Clear error
|
||||
clearError: () => set({ error: null }),
|
||||
|
||||
// Handle session update
|
||||
handleSessionUpdate: (session: Session) => {
|
||||
set(state => {
|
||||
const projectId = session.project_id;
|
||||
const projectSessions = state.sessions[projectId] || [];
|
||||
const existingIndex = projectSessions.findIndex(s => s.id === session.id);
|
||||
|
||||
let updatedSessions;
|
||||
if (existingIndex >= 0) {
|
||||
updatedSessions = [...projectSessions];
|
||||
updatedSessions[existingIndex] = session;
|
||||
} else {
|
||||
updatedSessions = [session, ...projectSessions];
|
||||
}
|
||||
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[projectId]: updatedSessions
|
||||
},
|
||||
currentSession: state.currentSessionId === session.id ? session : state.currentSession
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
// Handle output update
|
||||
handleOutputUpdate: (sessionId: string, output: string) => {
|
||||
set(state => ({
|
||||
sessionOutputs: {
|
||||
...state.sessionOutputs,
|
||||
[sessionId]: output
|
||||
}
|
||||
}));
|
||||
}
|
||||
}))
|
||||
);
|
Reference in New Issue
Block a user