diff --git a/dashboard/src/components/new-mission-dialog.tsx b/dashboard/src/components/new-mission-dialog.tsx index 07b68c8..022ae4d 100644 --- a/dashboard/src/components/new-mission-dialog.tsx +++ b/dashboard/src/components/new-mission-dialog.tsx @@ -65,7 +65,7 @@ export function NewMissionDialog({ }); // SWR: fetch agents for selected backend - const { data: backendAgents } = useSWR( + const { data: backendAgents, isLoading: backendAgentsLoading } = useSWR( newMissionBackend ? `backend-${newMissionBackend}-agents` : null, () => listBackendAgents(newMissionBackend), { revalidateOnFocus: false, dedupingInterval: 30000 } @@ -81,8 +81,11 @@ export function NewMissionDialog({ dedupingInterval: 30000, }); - // Parse agents from either backend API or fallback - const agents = backendAgents?.map(a => a.name) || parseAgentNames(agentsPayload); + // Parse agents from backend API (only use fallback for opencode backend) + // For non-opencode backends, wait for backendAgents to load to avoid race condition + const agents = newMissionBackend === 'opencode' + ? (backendAgents?.map(a => a.name) || parseAgentNames(agentsPayload)) + : (backendAgents?.map(a => a.name) || []); const formatWorkspaceType = (type: Workspace['workspace_type']) => type === 'host' ? 'host' : 'isolated'; @@ -104,9 +107,13 @@ export function NewMissionDialog({ // Set default agent when dialog opens (only once per open) // Wait for both agents AND config to load before setting defaults useEffect(() => { - if (!open || defaultSet || agents.length === 0) return; + if (!open || defaultSet) return; // Wait for config to finish loading (undefined = still loading, null/object = loaded) if (config === undefined) return; + // Wait for backend agents to finish loading (avoid race condition when switching backends) + if (backendAgentsLoading) return; + // If no agents available yet, wait + if (agents.length === 0) return; if (config?.default_agent && agents.includes(config.default_agent)) { setNewMissionAgent(config.default_agent); @@ -114,7 +121,7 @@ export function NewMissionDialog({ setNewMissionAgent('Sisyphus'); } setDefaultSet(true); - }, [open, defaultSet, agents, config]); + }, [open, defaultSet, agents, config, backendAgentsLoading]); const resetForm = () => { setNewMissionWorkspace(''); diff --git a/src/api/mission_store/sqlite.rs b/src/api/mission_store/sqlite.rs index fef63a2..9305dd6 100644 --- a/src/api/mission_store/sqlite.rs +++ b/src/api/mission_store/sqlite.rs @@ -106,6 +106,9 @@ impl SqliteMissionStore { conn.execute_batch(SCHEMA) .map_err(|e| format!("Failed to run schema: {}", e))?; + // Run migrations for existing databases + Self::run_migrations(&conn)?; + Ok::<_, String>(conn) }) .await @@ -155,6 +158,29 @@ impl SqliteMissionStore { String::new() } } + + /// Run database migrations for existing databases. + /// CREATE TABLE IF NOT EXISTS doesn't add columns to existing tables, + /// so we need to handle schema changes manually. + fn run_migrations(conn: &Connection) -> Result<(), String> { + // Check if 'backend' column exists in missions table + let has_backend_column: bool = conn + .prepare("SELECT 1 FROM pragma_table_info('missions') WHERE name = 'backend'") + .map_err(|e| format!("Failed to check for backend column: {}", e))? + .exists([]) + .map_err(|e| format!("Failed to query table info: {}", e))?; + + if !has_backend_column { + tracing::info!("Running migration: adding 'backend' column to missions table"); + conn.execute( + "ALTER TABLE missions ADD COLUMN backend TEXT NOT NULL DEFAULT 'opencode'", + [], + ) + .map_err(|e| format!("Failed to add backend column: {}", e))?; + } + + Ok(()) + } } fn parse_status(s: &str) -> MissionStatus {