diff --git a/dashboard/src/components/new-mission-dialog.tsx b/dashboard/src/components/new-mission-dialog.tsx index 022ae4d..ccaf9d9 100644 --- a/dashboard/src/components/new-mission-dialog.tsx +++ b/dashboard/src/components/new-mission-dialog.tsx @@ -1,9 +1,9 @@ 'use client'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useMemo } from 'react'; import { Plus } from 'lucide-react'; import useSWR from 'swr'; -import { getVisibleAgents, getOpenAgentConfig, listBackends, listBackendAgents, type Backend, type BackendAgent } from '@/lib/api'; +import { getVisibleAgents, getOpenAgentConfig, listBackends, listBackendAgents, getBackendConfig, type Backend, type BackendAgent } from '@/lib/api'; import type { Provider, Workspace } from '@/lib/api'; interface NewMissionDialogProps { @@ -18,6 +18,14 @@ interface NewMissionDialogProps { }) => Promise | void; } +// Combined agent with backend info +interface CombinedAgent { + backend: string; + backendName: string; + agent: string; + value: string; // "backend:agent" format +} + // Parse agent names from API response const parseAgentNames = (payload: unknown): string[] => { const normalizeEntry = (entry: unknown): string | null => { @@ -50,9 +58,9 @@ export function NewMissionDialog({ }: NewMissionDialogProps) { const [open, setOpen] = useState(false); const [newMissionWorkspace, setNewMissionWorkspace] = useState(''); - const [newMissionAgent, setNewMissionAgent] = useState(''); + // Combined value: "backend:agent" or empty for default + const [selectedAgentValue, setSelectedAgentValue] = useState(''); const [newMissionModelOverride, setNewMissionModelOverride] = useState(''); - const [newMissionBackend, setNewMissionBackend] = useState('opencode'); const [submitting, setSubmitting] = useState(false); const [defaultSet, setDefaultSet] = useState(false); const dialogRef = useRef(null); @@ -64,14 +72,38 @@ export function NewMissionDialog({ fallbackData: [{ id: 'opencode', name: 'OpenCode' }, { id: 'claudecode', name: 'Claude Code' }], }); - // SWR: fetch agents for selected backend - const { data: backendAgents, isLoading: backendAgentsLoading } = useSWR( - newMissionBackend ? `backend-${newMissionBackend}-agents` : null, - () => listBackendAgents(newMissionBackend), + // SWR: fetch backend configs to check enabled status + const { data: opencodeConfig } = useSWR('backend-opencode-config', () => getBackendConfig('opencode'), { + revalidateOnFocus: false, + dedupingInterval: 30000, + }); + const { data: claudecodeConfig } = useSWR('backend-claudecode-config', () => getBackendConfig('claudecode'), { + revalidateOnFocus: false, + dedupingInterval: 30000, + }); + + // Filter to only enabled backends + const enabledBackends = useMemo(() => { + return backends?.filter((b) => { + if (b.id === 'opencode') return opencodeConfig?.enabled !== false; + if (b.id === 'claudecode') return claudecodeConfig?.enabled !== false; + return true; + }) || []; + }, [backends, opencodeConfig, claudecodeConfig]); + + // SWR: fetch agents for each enabled backend + const { data: opencodeAgents } = useSWR( + enabledBackends.some(b => b.id === 'opencode') ? 'backend-opencode-agents' : null, + () => listBackendAgents('opencode'), + { revalidateOnFocus: false, dedupingInterval: 30000 } + ); + const { data: claudecodeAgents } = useSWR( + enabledBackends.some(b => b.id === 'claudecode') ? 'backend-claudecode-agents' : null, + () => listBackendAgents('claudecode'), { revalidateOnFocus: false, dedupingInterval: 30000 } ); - // SWR: fetch once, cache globally, revalidate in background (fallback for agent list) + // SWR: fallback for opencode agents const { data: agentsPayload } = useSWR('opencode-agents', getVisibleAgents, { revalidateOnFocus: false, dedupingInterval: 30000, @@ -81,11 +113,51 @@ export function NewMissionDialog({ dedupingInterval: 30000, }); - // 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) || []); + // Combine all agents from enabled backends + const allAgents = useMemo((): CombinedAgent[] => { + const result: CombinedAgent[] = []; + + for (const backend of enabledBackends) { + let agentNames: string[] = []; + + if (backend.id === 'opencode') { + agentNames = opencodeAgents?.map(a => a.name) || parseAgentNames(agentsPayload); + } else if (backend.id === 'claudecode') { + agentNames = claudecodeAgents?.map(a => a.name) || []; + } + + for (const agent of agentNames) { + result.push({ + backend: backend.id, + backendName: backend.name, + agent, + value: `${backend.id}:${agent}`, + }); + } + } + + return result; + }, [enabledBackends, opencodeAgents, claudecodeAgents, agentsPayload]); + + // Group agents by backend for display + const agentsByBackend = useMemo(() => { + const groups: Record = {}; + for (const agent of allAgents) { + if (!groups[agent.backend]) { + groups[agent.backend] = []; + } + groups[agent.backend].push(agent); + } + return groups; + }, [allAgents]); + + // Parse selected value to get backend and agent + const parseSelectedValue = (value: string): { backend: string; agent: string } | null => { + if (!value) return null; + const [backend, ...agentParts] = value.split(':'); + const agent = agentParts.join(':'); // Handle agent names with colons + return backend && agent ? { backend, agent } : null; + }; const formatWorkspaceType = (type: Workspace['workspace_type']) => type === 'host' ? 'host' : 'isolated'; @@ -105,29 +177,42 @@ export function NewMissionDialog({ }, [open]); // 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) return; - // Wait for config to finish loading (undefined = still loading, null/object = loaded) + // Wait for config to finish loading 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; + // Wait for agents to load + if (allAgents.length === 0) return; - if (config?.default_agent && agents.includes(config.default_agent)) { - setNewMissionAgent(config.default_agent); - } else if (agents.includes('Sisyphus')) { - setNewMissionAgent('Sisyphus'); + // Try to find the default agent from config + if (config?.default_agent) { + const defaultAgent = allAgents.find(a => a.agent === config.default_agent); + if (defaultAgent) { + setSelectedAgentValue(defaultAgent.value); + setDefaultSet(true); + return; + } + } + + // Fallback: try Sisyphus in OpenCode + const sisyphus = allAgents.find(a => a.backend === 'opencode' && a.agent === 'Sisyphus'); + if (sisyphus) { + setSelectedAgentValue(sisyphus.value); + setDefaultSet(true); + return; + } + + // Fallback: use first available agent + if (allAgents.length > 0) { + setSelectedAgentValue(allAgents[0].value); } setDefaultSet(true); - }, [open, defaultSet, agents, config, backendAgentsLoading]); + }, [open, defaultSet, allAgents, config]); const resetForm = () => { setNewMissionWorkspace(''); - setNewMissionAgent(''); + setSelectedAgentValue(''); setNewMissionModelOverride(''); - setNewMissionBackend('opencode'); setDefaultSet(false); }; @@ -140,11 +225,12 @@ export function NewMissionDialog({ if (disabled || submitting) return; setSubmitting(true); try { + const parsed = parseSelectedValue(selectedAgentValue); await onCreate({ workspaceId: newMissionWorkspace || undefined, - agent: newMissionAgent || undefined, + agent: parsed?.agent || undefined, modelOverride: newMissionModelOverride || undefined, - backend: newMissionBackend || undefined, + backend: parsed?.backend || 'opencode', }); setOpen(false); resetForm(); @@ -154,7 +240,9 @@ export function NewMissionDialog({ }; const isBusy = disabled || submitting; - const defaultAgentLabel = 'Default (OpenCode default)'; + + // Determine default label based on enabled backends + const defaultBackendName = enabledBackends[0]?.name || 'OpenCode'; return (
@@ -209,17 +297,12 @@ export function NewMissionDialog({

Where the mission will run

- {/* Backend selection */} + {/* Agent selection (includes backend) */}
- + -

AI coding backend to power this mission

-
+ {enabledBackends.map((backend) => { + const backendAgentsList = agentsByBackend[backend.id] || []; + if (backendAgentsList.length === 0) return null; - {/* Agent selection */} -
- -

- Agents are provided by plugins; defaults are recommended + Select an agent and backend to power this mission