"use client"; import { useState, useCallback, useRef, useEffect } from "react"; import { IconPlayerPlay, IconLoader2, IconCheck, IconX, IconTrash } from "@tabler/icons-react"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@kandev/ui/card"; import { Input } from "@kandev/ui/input"; import { Label } from "@kandev/ui/label"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@kandev/ui/table"; import { ScriptEditor } from "@/components/settings/profile-edit/script-editor"; import { buildDockerImage, listDockerContainers, stopDockerContainer, removeDockerContainer, } from "@/lib/api/domains/settings-api"; import type { DockerContainer } from "@/lib/api/domains/settings-api"; const DEFAULT_IMAGE_TAG = "kandev/multi-agent:latest"; // Self-contained default that produces a working image: // - node + npm/npx so ACP agents (Claude, Codex, OpenCode, Auggie) can be // fetched on demand at runtime // - git so the prepare script can clone the workspace into the container // - ca-certificates + curl as a baseline for any extra tooling users add // // The kandev backend mounts the agentctl binary into /usr/local/bin/agentctl // at container creation time, so users do NOT need to bake it in here. const DEFAULT_DOCKERFILE = `FROM node:22-slim RUN apt-get update \\ && apt-get install -y --no-install-recommends \\ git ca-certificates curl \\ && rm -rf /var/lib/apt/lists/* WORKDIR /workspace `; export type BuildStatus = "idle" | "building" | "success" | "failed"; export type DockerBuildSuccess = { dockerfile: string; imageTag: string }; /** Parse a Docker JSON stream line into a human-readable string. */ function parseDockerLine(raw: string): string | null { const trimmed = raw.trim(); if (!trimmed) return null; try { const obj = JSON.parse(trimmed) as Record; if (obj.error) return `ERROR: ${obj.error}\n`; if (typeof obj.stream === "string") return obj.stream; if (typeof obj.status === "string") { const id = obj.id ? ` ${obj.id}` : ""; return `${obj.status}${id}\n`; } // Skip metadata-only messages (aux, progressDetail, etc.) return null; } catch { return trimmed; } } function BuildStatusBadge({ status }: { status: BuildStatus }) { if (status === "idle") return null; if (status === "building") return Building...; if (status === "success") { return ( Success ); } return ( Failed ); } async function readDockerStream( reader: ReadableStreamDefaultReader, appendLog: (text: string) => void, ): Promise { const decoder = new TextDecoder(); let buffer = ""; let hasError = false; let done = false; while (!done) { const result = await reader.read(); done = result.done; if (!result.value) continue; buffer += decoder.decode(result.value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { const parsed = parseDockerLine(line); if (!parsed) continue; if (parsed.startsWith("ERROR:")) hasError = true; appendLog(parsed); } } const tail = parseDockerLine(buffer); if (tail) appendLog(tail); return hasError; } function useBuildStream(onBuildSuccess?: (result: DockerBuildSuccess) => void) { const [buildStatus, setBuildStatus] = useState("idle"); const [buildLog, setBuildLog] = useState(""); const appendLog = useCallback((text: string) => { setBuildLog((prev) => prev + text); }, []); const runBuild = useCallback( async (dockerfile: string, tag: string) => { setBuildStatus("building"); setBuildLog(""); try { const response = await buildDockerImage({ dockerfile, tag }); if (!response.ok) { const text = await response.text(); setBuildStatus("failed"); setBuildLog(`Build failed (${response.status}): ${text}`); return; } const reader = response.body?.getReader(); if (!reader) { setBuildStatus("failed"); setBuildLog("No response body"); return; } const hasError = await readDockerStream(reader, appendLog); const nextStatus = hasError ? "failed" : "success"; setBuildStatus(nextStatus); if (nextStatus === "success") { onBuildSuccess?.({ dockerfile, imageTag: tag.trim() }); } } catch (err) { setBuildStatus("failed"); const msg = err instanceof Error ? err.message : "Unknown error"; setBuildLog((prev) => prev + `\nBuild failed: ${msg}`); } }, [appendLog, onBuildSuccess], ); return { buildStatus, buildLog, runBuild }; } type DockerfileBuildCardProps = { dockerfile: string; onDockerfileChange: (v: string) => void; imageTag: string; onImageTagChange: (v: string) => void; onBuildSuccess?: (result: DockerBuildSuccess) => void; }; export function DockerfileBuildCard({ dockerfile, onDockerfileChange, imageTag, onImageTagChange, onBuildSuccess, }: DockerfileBuildCardProps) { const { buildStatus, buildLog, runBuild } = useBuildStream(onBuildSuccess); const logRef = useRef(null); useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [buildLog]); const handleBuild = () => { if (dockerfile.trim() && imageTag.trim()) void runBuild(dockerfile, imageTag); }; const canFillDefaults = !dockerfile.trim() || !imageTag.trim(); const fillDefaults = () => { if (!imageTag.trim()) onImageTagChange(DEFAULT_IMAGE_TAG); if (!dockerfile.trim()) onDockerfileChange(DEFAULT_DOCKERFILE); }; return (
Dockerfile Define the Docker image. Build and test it here.
{canFillDefaults && ( )}
onImageTagChange(e.target.value)} placeholder={DEFAULT_IMAGE_TAG} className="font-mono text-sm" />
{buildLog && (
            {buildLog}
          
)}
); } function ContainersEmptyState({ loading }: { loading: boolean }) { if (loading) { return (
Loading...
); } return

No Docker containers.

; } export function useDockerProfileContainers(profileId: string, enabled = true) { const [containers, setContainers] = useState([]); const [loading, setLoading] = useState(enabled); const refresh = useCallback(async () => { if (!enabled) { setContainers([]); setLoading(false); return; } setLoading(true); try { const result = await listDockerContainers({ labels: { "kandev.executor_profile_id": profileId }, }); setContainers(result.containers ?? []); } catch { setContainers([]); } finally { setLoading(false); } }, [enabled, profileId]); useEffect(() => { void refresh(); }, [refresh]); return { containers, loading, refresh }; } export function containerTaskLabel(container: DockerContainer) { const title = container.labels?.["kandev.task_title"]?.trim(); return title || container.labels?.["kandev.task_id"] || "Untracked"; } function ContainerRow({ container, actionLoading, onStop, onRemove, }: { container: DockerContainer; actionLoading: string | null; onStop: (id: string) => void; onRemove: (id: string) => void; }) { const isLoading = actionLoading === container.id; return ( {container.name} {container.image} {containerTaskLabel(container)} {container.status}
{container.state === "running" && ( )}
); } export function DockerContainersCard({ profileId }: { profileId: string }) { const { containers, loading, refresh } = useDockerProfileContainers(profileId); const [actionLoading, setActionLoading] = useState(null); const handleAction = useCallback( async (id: string, action: (id: string) => Promise) => { setActionLoading(id); try { await action(id); await refresh(); } finally { setActionLoading(null); } }, [refresh], ); return ( Docker Containers Docker containers created by this profile. {containers.length === 0 ? ( ) : ( Name Image Task Status {containers.map((c) => ( handleAction(id, stopDockerContainer)} onRemove={(id) => handleAction(id, removeDockerContainer)} /> ))}
)}
); }