import { Ban, Send } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { MessageBubble } from "./MessageBubble"; import { ThinkingIndicator } from "./ThinkingIndicator"; import { useAcpCancel, useAcpHistory, useAcpPrompt, useAcpStatus, } from "@/lib/api/acp"; import { cn } from "@/lib/utils"; interface AcpPanelProps { workspaceId: string | null; } interface LocalError { id: number; message: string; } function StatusPill({ status }: { status: ReturnType }) { let dotClass = "bg-muted-foreground"; let label = "not started"; if (status.isPending) { dotClass = "bg-muted-foreground animate-pulse"; label = "initializing…"; } else if (status.isError) { dotClass = "bg-destructive"; label = "error"; } else if (status.data) { if (status.data.error) { dotClass = "bg-destructive"; label = `error: ${status.data.error}`; } else if (status.data.ready) { dotClass = "bg-green-500"; label = "ready"; } else if (status.data.running) { dotClass = "bg-muted-foreground animate-pulse"; label = "initializing…"; } else { dotClass = "bg-muted-foreground"; label = "not started"; } } return (
{label}
); } export function AcpPanel({ workspaceId }: AcpPanelProps) { const acpStatus = useAcpStatus(workspaceId); const acpHistory = useAcpHistory(workspaceId); const prompt = useAcpPrompt(); const cancel = useAcpCancel(); const [input, setInput] = useState(""); const [localErrors, setLocalErrors] = useState([]); const scrollRootRef = useRef(null); const sentinelRef = useRef(null); const messages = useMemo( () => acpHistory.data?.messages ?? [], [acpHistory.data], ); useEffect(() => { if (prompt.error) { const id = Date.now(); setLocalErrors((prev) => [ ...prev, { id, message: prompt.error?.message ?? "Failed to send prompt" }, ]); const timeout = window.setTimeout(() => { setLocalErrors((prev) => prev.filter((e) => e.id !== id)); }, 5000); return () => window.clearTimeout(timeout); } }, [prompt.error]); useEffect(() => { const root = scrollRootRef.current; const sentinel = sentinelRef.current; if (!root || !sentinel) return; const viewport = root.querySelector( "[data-radix-scroll-area-viewport]", ) as HTMLDivElement | null; if (!viewport) return; const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight; if (distanceFromBottom <= 80) { sentinel.scrollIntoView({ behavior: "smooth", block: "end" }); } }, [messages, prompt.isPending]); const handleSend = () => { const content = input.trim(); if (!content || !workspaceId || prompt.isPending) return; prompt.mutate({ workspaceId, content }); setInput(""); }; const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const isReady = acpStatus.data?.ready === true; const sessionId = acpStatus.data?.sessionId; const trimmedSession = sessionId && sessionId.length > 12 ? `${sessionId.slice(0, 12)}…` : sessionId; if (!workspaceId) { return (
Select a workspace to use the agent.
); } return (
Agent {sessionId && ( sess:{trimmedSession} )}
{messages.length === 0 && isReady && (
Send a message to start the conversation.
)} {messages.map((message, index) => ( ))} {prompt.isPending && } {localErrors.map((err) => (
{err.message}
))}