2026-04-20 15:10:54 +02:00
|
|
|
"use client";
|
|
|
|
|
|
2026-07-08 10:41:02 +02:00
|
|
|
import { useRef, useState } from "react";
|
2026-04-20 15:10:54 +02:00
|
|
|
import { useSpawnAgent } from "@/hooks/use-agents";
|
2026-07-08 10:41:02 +02:00
|
|
|
import { getErrorMessage } from "@/lib/api/client";
|
2026-04-20 15:10:54 +02:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogTrigger,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
2026-07-15 18:04:00 +02:00
|
|
|
import { HelpTip } from "@/components/ui/help-tip";
|
2026-07-20 20:38:36 +02:00
|
|
|
import { TaskSelector } from "@/components/tasks/task-selector";
|
2026-04-20 15:10:54 +02:00
|
|
|
import { Play } from "lucide-react";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
|
|
|
|
|
interface SpawnAgentDialogProps {
|
|
|
|
|
agentId: string;
|
|
|
|
|
agentName: string;
|
|
|
|
|
trigger?: React.ReactNode;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-29 05:38:21 +02:00
|
|
|
export function SpawnAgentDialog({
|
|
|
|
|
agentId,
|
|
|
|
|
agentName,
|
|
|
|
|
trigger,
|
|
|
|
|
}: SpawnAgentDialogProps) {
|
2026-04-20 15:10:54 +02:00
|
|
|
const [open, setOpen] = useState(false);
|
2026-07-20 20:38:36 +02:00
|
|
|
const [taskId, setTaskId] = useState<string | null>(null);
|
2026-04-20 15:10:54 +02:00
|
|
|
const [initialPrompt, setInitialPrompt] = useState("");
|
|
|
|
|
const spawnAgent = useSpawnAgent();
|
2026-07-08 10:41:02 +02:00
|
|
|
// Synchronous re-entrancy guard: `spawnAgent.isPending` only flips on a
|
|
|
|
|
// re-render, which lags a fast double-click/double-fire by a tick or two —
|
|
|
|
|
// the guard below blocks a second call within the same synchronous burst
|
|
|
|
|
// regardless of render timing.
|
|
|
|
|
const submittingRef = useRef(false);
|
2026-04-20 15:10:54 +02:00
|
|
|
|
|
|
|
|
const handleSpawn = async () => {
|
2026-07-08 10:41:02 +02:00
|
|
|
if (submittingRef.current) return;
|
|
|
|
|
submittingRef.current = true;
|
2026-04-20 15:10:54 +02:00
|
|
|
try {
|
2026-07-08 10:41:02 +02:00
|
|
|
const result = await spawnAgent.mutateAsync({
|
2026-04-20 15:10:54 +02:00
|
|
|
agentId,
|
|
|
|
|
request: {
|
|
|
|
|
task_id: taskId || undefined,
|
|
|
|
|
initial_prompt: initialPrompt || undefined,
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-07-08 10:41:02 +02:00
|
|
|
if (result.already_running) {
|
|
|
|
|
toast.info(`Agent ${agentName} already running — spawn skipped`);
|
|
|
|
|
} else {
|
|
|
|
|
toast.success(`Agent ${agentName} spawned successfully`);
|
|
|
|
|
}
|
2026-04-20 15:10:54 +02:00
|
|
|
setOpen(false);
|
|
|
|
|
resetForm();
|
2026-07-08 10:41:02 +02:00
|
|
|
} catch (error) {
|
|
|
|
|
toast.error(getErrorMessage(error));
|
|
|
|
|
} finally {
|
|
|
|
|
submittingRef.current = false;
|
2026-04-20 15:10:54 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
2026-07-20 20:38:36 +02:00
|
|
|
setTaskId(null);
|
2026-04-20 15:10:54 +02:00
|
|
|
setInitialPrompt("");
|
|
|
|
|
};
|
|
|
|
|
|
2026-07-16 15:47:08 +02:00
|
|
|
// The tooltip must wrap the DialogTrigger, never sit inside it: with
|
|
|
|
|
// HelpTip as DialogTrigger's asChild child, the dialog's click handler is
|
|
|
|
|
// cloned onto the Tooltip root (not a DOM element) and silently dropped.
|
2026-04-20 15:10:54 +02:00
|
|
|
const defaultTrigger = (
|
2026-07-16 15:47:08 +02:00
|
|
|
<DropdownMenuItem onSelect={(e) => e.preventDefault()}>
|
|
|
|
|
<Play className="h-4 w-4 mr-2" />
|
|
|
|
|
Spawn
|
|
|
|
|
</DropdownMenuItem>
|
2026-04-20 15:10:54 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={setOpen}>
|
2026-07-16 15:47:08 +02:00
|
|
|
{trigger ? (
|
|
|
|
|
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
|
|
|
) : (
|
|
|
|
|
<HelpTip
|
|
|
|
|
label="Start this agent's container, optionally pre-claiming a task"
|
|
|
|
|
side="left"
|
|
|
|
|
>
|
|
|
|
|
<DialogTrigger asChild>{defaultTrigger}</DialogTrigger>
|
|
|
|
|
</HelpTip>
|
|
|
|
|
)}
|
2026-04-20 15:10:54 +02:00
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Spawn {agentName}</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Start this agent with optional task assignment and initial prompt.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
2026-07-15 18:04:00 +02:00
|
|
|
<HelpTip label="Pre-claims this task on spawn instead of pulling from the pool">
|
2026-07-20 20:38:36 +02:00
|
|
|
<Label className="w-fit">Task (optional)</Label>
|
2026-07-15 18:04:00 +02:00
|
|
|
</HelpTip>
|
2026-07-20 20:38:36 +02:00
|
|
|
<TaskSelector
|
2026-04-20 15:10:54 +02:00
|
|
|
value={taskId}
|
2026-07-20 20:38:36 +02:00
|
|
|
onChange={setTaskId}
|
|
|
|
|
placeholder="Select task to assign (optional)..."
|
2026-04-20 15:10:54 +02:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
2026-07-15 18:04:00 +02:00
|
|
|
<HelpTip label="Extra instructions passed to the agent's first turn">
|
2026-07-20 20:38:36 +02:00
|
|
|
<Label htmlFor="initialPrompt" className="w-fit">
|
|
|
|
|
Initial Prompt (optional)
|
|
|
|
|
</Label>
|
2026-07-15 18:04:00 +02:00
|
|
|
</HelpTip>
|
2026-04-20 15:10:54 +02:00
|
|
|
<Input
|
|
|
|
|
id="initialPrompt"
|
|
|
|
|
value={initialPrompt}
|
|
|
|
|
onChange={(e) => setInitialPrompt(e.target.value)}
|
|
|
|
|
placeholder="Initial instructions for the agent"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex justify-end gap-2">
|
2026-07-15 18:04:00 +02:00
|
|
|
<HelpTip label="Closes without spawning">
|
|
|
|
|
<Button variant="outline" onClick={() => setOpen(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
</HelpTip>
|
|
|
|
|
<HelpTip label="Already-running agents are skipped — no duplicate container is started">
|
|
|
|
|
<span>
|
|
|
|
|
<Button onClick={handleSpawn} disabled={spawnAgent.isPending}>
|
|
|
|
|
{spawnAgent.isPending ? "Spawning..." : "Spawn Agent"}
|
|
|
|
|
</Button>
|
|
|
|
|
</span>
|
|
|
|
|
</HelpTip>
|
2026-04-20 15:10:54 +02:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|