diff --git a/web/src/stores/agents.svelte.ts b/web/src/stores/agents.svelte.ts new file mode 100644 index 0000000..34983fe --- /dev/null +++ b/web/src/stores/agents.svelte.ts @@ -0,0 +1,88 @@ +/** + * Reactive store for agents and event feed using Svelte 5 runes. + * Manages active agents and WebSocket event stream. + */ + +import { createApiClient } from '../api/client'; +import type { ActiveAgent, WsEvent } from '../types'; + +/** + * Agents state object. + * Managed as Svelte 5 $state for reactivity. + * Event feed is a ring buffer (max 200 items, newest at index 0). + */ +export const agentsState = $state<{ + activeAgents: Array; + eventFeed: Array; + loading: boolean; +}>({ + activeAgents: [], + eventFeed: [], + loading: false, +}); + +const apiClient = createApiClient(); +const MAX_FEED_SIZE = 200; + +/** + * Load active agents for a goal. + */ +export async function loadAgents(goalId: string): Promise { + agentsState.loading = true; + try { + agentsState.activeAgents = await apiClient.listAgents(goalId); + } finally { + agentsState.loading = false; + } +} + +/** + * Add an event to the feed. + * Maintains a ring buffer by trimming to MAX_FEED_SIZE. + * Newest events appear at index 0. + */ +export function addEvent(event: WsEvent): void { + agentsState.eventFeed.unshift(event); + if (agentsState.eventFeed.length > MAX_FEED_SIZE) { + agentsState.eventFeed.pop(); + } +} + +/** + * Clear the event feed. + */ +export function clearFeed(): void { + agentsState.eventFeed = []; +} + +/** + * Handle WebSocket events. + * - agent_spawned: add placeholder to activeAgents + * - agent_progress: add to feed + * - agent_completed: remove from activeAgents and add to feed + * - tool_execution: add to feed + * Agent-related events are added to the event feed. + */ +export function handleWsEvent(event: WsEvent): void { + if (event.type === 'agent_spawned') { + // Add placeholder to activeAgents + const placeholder: ActiveAgent = { + agent_id: event.agent_id, + task_id: '', // Will be updated when agent takes a task + task_title: 'Initializing...', + task_status: 'pending', + }; + agentsState.activeAgents.push(placeholder); + addEvent(event); + } else if (event.type === 'agent_progress') { + addEvent(event); + } else if (event.type === 'agent_completed') { + // Remove from activeAgents + agentsState.activeAgents = agentsState.activeAgents.filter( + (a) => a.agent_id !== event.agent_id + ); + addEvent(event); + } else if (event.type === 'tool_execution') { + addEvent(event); + } +} diff --git a/web/src/stores/search.svelte.ts b/web/src/stores/search.svelte.ts new file mode 100644 index 0000000..098f6ba --- /dev/null +++ b/web/src/stores/search.svelte.ts @@ -0,0 +1,76 @@ +/** + * Reactive store for search state using Svelte 5 runes. + * Manages search query, results, and filters. + */ + +import { createApiClient } from '../api/client'; +import type { GraphNode, NodeType } from '../types'; + +/** + * Search state object. + * Managed as Svelte 5 $state for reactivity. + */ +export const searchState = $state<{ + query: string; + results: Array; + nodeTypeFilter: NodeType | null; + loading: boolean; + error: string | null; +}>({ + query: '', + results: [], + nodeTypeFilter: null, + loading: false, + error: null, +}); + +const apiClient = createApiClient(); + +/** + * Execute a search for nodes. + */ +export async function executeSearch(projectId: string): Promise { + if (!searchState.query) { + searchState.results = []; + return; + } + + searchState.loading = true; + searchState.error = null; + try { + searchState.results = await apiClient.searchNodes(projectId, { + query: searchState.query, + node_type: searchState.nodeTypeFilter || undefined, + limit: 50, + }); + } catch (error) { + searchState.error = error instanceof Error ? error.message : 'Unknown error'; + searchState.results = []; + } finally { + searchState.loading = false; + } +} + +/** + * Update the search query. + */ +export function setQuery(q: string): void { + searchState.query = q; +} + +/** + * Update the node type filter. + */ +export function setNodeTypeFilter(type: NodeType | null): void { + searchState.nodeTypeFilter = type; +} + +/** + * Clear all search state. + */ +export function clearSearch(): void { + searchState.query = ''; + searchState.results = []; + searchState.nodeTypeFilter = null; + searchState.error = null; +}