"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/agent:latest"; const DEFAULT_DOCKERFILE = "FROM ubuntu:24.04\n"; type BuildStatus = "idle" | "building" | "success" | "failed"; /** 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() { 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); setBuildStatus(hasError ? "failed" : "success"); } catch (err) { setBuildStatus("failed"); const msg = err instanceof Error ? err.message : "Unknown error"; setBuildLog((prev) => prev + `\nBuild failed: ${msg}`); } }, [appendLog], ); return { buildStatus, buildLog, runBuild }; } type DockerfileBuildCardProps = { dockerfile: string; onDockerfileChange: (v: string) => void; imageTag: string; onImageTagChange: (v: string) => void; }; export function DockerfileBuildCard({ dockerfile, onDockerfileChange, imageTag, onImageTagChange, }: DockerfileBuildCardProps) { const { buildStatus, buildLog, runBuild } = useBuildStream(); 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 running containers.

; } 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} {container.status}
{container.state === "running" && ( )}
); } export function DockerContainersCard({ profileId }: { profileId: string }) { const [containers, setContainers] = useState([]); const [loading, setLoading] = useState(true); const [actionLoading, setActionLoading] = useState(null); const refresh = useCallback(async () => { setLoading(true); try { const result = await listDockerContainers({ labels: { "kandev.profile_id": profileId } }); setContainers(result.containers ?? []); } catch { setContainers([]); } finally { setLoading(false); } }, [profileId]); useEffect(() => { void refresh(); }, [refresh]); const handleAction = useCallback( async (id: string, action: (id: string) => Promise) => { setActionLoading(id); try { await action(id); await refresh(); } finally { setActionLoading(null); } }, [refresh], ); return ( Running Containers Docker containers created by this profile. {containers.length === 0 ? ( ) : ( Name Image Status {containers.map((c) => ( handleAction(id, stopDockerContainer)} onRemove={(id) => handleAction(id, removeDockerContainer)} /> ))}
)}
); }