Files
roboco/panel/src/components/agents/spawn-agent-dialog.tsx
T

146 lines
4.7 KiB
TypeScript
Raw Normal View History

2026-04-20 15:10:54 +02:00
"use client";
import { useRef, useState } from "react";
2026-04-20 15:10:54 +02:00
import { useSpawnAgent } from "@/hooks/use-agents";
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";
import { HelpTip } from "@/components/ui/help-tip";
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);
const [taskId, setTaskId] = useState<string | null>(null);
2026-04-20 15:10:54 +02:00
const [initialPrompt, setInitialPrompt] = useState("");
const spawnAgent = useSpawnAgent();
// 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 () => {
if (submittingRef.current) return;
submittingRef.current = true;
2026-04-20 15:10:54 +02:00
try {
const result = await spawnAgent.mutateAsync({
2026-04-20 15:10:54 +02:00
agentId,
request: {
task_id: taskId || undefined,
initial_prompt: initialPrompt || undefined,
},
});
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();
} catch (error) {
toast.error(getErrorMessage(error));
} finally {
submittingRef.current = false;
2026-04-20 15:10:54 +02:00
}
};
const resetForm = () => {
setTaskId(null);
2026-04-20 15:10:54 +02:00
setInitialPrompt("");
};
// 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 = (
<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}>
{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">
<HelpTip label="Pre-claims this task on spawn instead of pulling from the pool">
<Label className="w-fit">Task (optional)</Label>
</HelpTip>
<TaskSelector
2026-04-20 15:10:54 +02:00
value={taskId}
onChange={setTaskId}
placeholder="Select task to assign (optional)..."
2026-04-20 15:10:54 +02:00
/>
</div>
<div className="space-y-2">
<HelpTip label="Extra instructions passed to the agent's first turn">
<Label htmlFor="initialPrompt" className="w-fit">
Initial Prompt (optional)
</Label>
</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">
<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>
);
}