diff --git a/dashboard/src/app/control/control-client.tsx b/dashboard/src/app/control/control-client.tsx index baabe6e..a84a0e3 100644 --- a/dashboard/src/app/control/control-client.tsx +++ b/dashboard/src/app/control/control-client.tsx @@ -16,9 +16,12 @@ import { getCurrentMission, uploadFile, getProgress, + getRunningMissions, + cancelMission, type ControlRunState, type Mission, type MissionStatus, + type RunningMissionInfo, } from "@/lib/api"; import { Send, @@ -40,6 +43,8 @@ import { Paperclip, ArrowDown, Cpu, + Layers, + RefreshCw, } from "lucide-react"; import { OptionList, @@ -363,6 +368,10 @@ export default function ControlClient() { const [showStatusMenu, setShowStatusMenu] = useState(false); const [missionLoading, setMissionLoading] = useState(false); + // Parallel missions state + const [runningMissions, setRunningMissions] = useState([]); + const [showParallelPanel, setShowParallelPanel] = useState(false); + // Attachment state const [attachments, setAttachments] = useState< { file: File; uploading: boolean }[] @@ -543,6 +552,41 @@ export default function ControlClient() { } }, [searchParams, router, missionHistoryToItems]); + // Poll for running parallel missions + useEffect(() => { + const pollRunning = async () => { + try { + const running = await getRunningMissions(); + setRunningMissions(running); + // Auto-show panel if there are parallel missions + if (running.length > 1) { + setShowParallelPanel(true); + } + } catch { + // Ignore errors + } + }; + + // Poll immediately and then every 3 seconds + pollRunning(); + const interval = setInterval(pollRunning, 3000); + return () => clearInterval(interval); + }, []); + + // Handle cancelling a parallel mission + const handleCancelMission = async (missionId: string) => { + try { + await cancelMission(missionId); + toast.success("Mission cancelled"); + // Refresh running list + const running = await getRunningMissions(); + setRunningMissions(running); + } catch (err) { + console.error("Failed to cancel mission:", err); + toast.error("Failed to cancel mission"); + } + }; + // Handle creating a new mission const handleNewMission = async () => { try { @@ -600,6 +644,20 @@ export default function ControlClient() { const handleEvent = (event: { type: string; data: unknown }) => { const data: unknown = event.data; + // Filter events by mission_id - only show events for current mission or events without mission_id + if (isRecord(data) && data["mission_id"]) { + const eventMissionId = String(data["mission_id"]); + // Get current mission ID from the URL or state + const urlMissionId = new URLSearchParams(window.location.search).get("mission"); + if (urlMissionId && eventMissionId !== urlMissionId) { + // Event is for a different mission, ignore it for UI updates + // But still process status events to update parallel missions list + if (event.type !== "status") { + return; + } + } + } + if (event.type === "status" && isRecord(data)) { reconnectAttempts = 0; const st = data["state"]; @@ -942,6 +1000,23 @@ export default function ControlClient() { New Mission + {/* Parallel missions indicator */} + {runningMissions.length > 0 && ( + + )} + {/* Status panel */}
{/* Run state indicator */} @@ -984,6 +1059,90 @@ export default function ControlClient() {
+ {/* Parallel Missions Panel */} + {showParallelPanel && runningMissions.length > 0 && ( +
+
+
+ +

Running Missions

+
+ +
+
+ {runningMissions.map((mission) => { + const isCurrentMission = currentMission?.id === mission.mission_id; + return ( +
+
+
+
+

+ {mission.model_override || "Default Model"} +

+

+ {mission.mission_id.slice(0, 8)}... • {mission.state} +

+
+
+
+ {!isCurrentMission && ( + + )} + +
+
+ ); + })} +
+
+ )} + {/* Chat container */}
{/* Messages */} diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index a2e12af..9de76cb 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -324,6 +324,48 @@ export async function loadMission(id: string): Promise { return res.json(); } +// ==================== Parallel Missions ==================== + +export interface RunningMissionInfo { + mission_id: string; + model_override: string | null; + state: "queued" | "running" | "waiting_for_tool" | "finished"; + queue_len: number; + history_len: number; +} + +// Get all running parallel missions +export async function getRunningMissions(): Promise { + const res = await apiFetch("/api/control/running"); + if (!res.ok) throw new Error("Failed to fetch running missions"); + return res.json(); +} + +// Start a mission in parallel +export async function startMissionParallel( + missionId: string, + content: string +): Promise<{ ok: boolean; mission_id: string }> { + const res = await apiFetch(`/api/control/missions/${missionId}/parallel`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`Failed to start parallel mission: ${text}`); + } + return res.json(); +} + +// Cancel a specific mission +export async function cancelMission(missionId: string): Promise { + const res = await apiFetch(`/api/control/missions/${missionId}/cancel`, { + method: "POST", + }); + if (!res.ok) throw new Error("Failed to cancel mission"); +} + // Set mission status export async function setMissionStatus( id: string, @@ -342,8 +384,8 @@ export async function setMissionStatus( export type ControlRunState = "idle" | "running" | "waiting_for_tool"; export type ControlAgentEvent = - | { type: "status"; state: ControlRunState; queue_len: number } - | { type: "user_message"; id: string; content: string } + | { type: "status"; state: ControlRunState; queue_len: number; mission_id?: string } + | { type: "user_message"; id: string; content: string; mission_id?: string } | { type: "assistant_message"; id: string; @@ -351,11 +393,12 @@ export type ControlAgentEvent = success: boolean; cost_cents: number; model: string | null; + mission_id?: string; } - | { type: "thinking"; content: string; done: boolean } - | { type: "tool_call"; tool_call_id: string; name: string; args: unknown } - | { type: "tool_result"; tool_call_id: string; name: string; result: unknown } - | { type: "error"; message: string }; + | { type: "thinking"; content: string; done: boolean; mission_id?: string } + | { type: "tool_call"; tool_call_id: string; name: string; args: unknown; mission_id?: string } + | { type: "tool_result"; tool_call_id: string; name: string; result: unknown; mission_id?: string } + | { type: "error"; message: string; mission_id?: string }; export async function postControlMessage( content: string