import * as React from "react"; import Markdown from "react-markdown"; import type { AgentConfigInfo, AgentEvent, AgentInfo, AgentStatusValue, } from "../api/agents"; import { agentsApi, subscribeAgentStream } from "../api/agents"; import type { TestResultsResponse, TokenCostResponse, } from "../api/client"; import { api } from "../api/client"; import { AgentLogsSection } from "./AgentLogsSection"; import { TestResultsSection } from "./TestResultsSection"; import { TokenCostSection } from "./TokenCostSection"; import { WorkItemDetailPanelHeader } from "./WorkItemDetailPanelHeader"; import { stripDisplayContent } from "./workItemDetailPanelUtils"; const { useCallback, useEffect, useRef, useState } = React; interface WorkItemDetailPanelProps { storyId: string; pipelineVersion: number; onClose: () => void; /** True when the item is in QA and awaiting human review. */ reviewHold?: boolean; } export function WorkItemDetailPanel({ storyId, pipelineVersion, onClose, reviewHold: _reviewHold, }: WorkItemDetailPanelProps) { const [content, setContent] = useState(null); const [stage, setStage] = useState(""); const [name, setName] = useState(null); const [assignedAgent, setAssignedAgent] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [agentInfo, setAgentInfo] = useState(null); const [agentLog, setAgentLog] = useState([]); const [agentStatus, setAgentStatus] = useState(null); const [testResults, setTestResults] = useState( null, ); const [tokenCost, setTokenCost] = useState(null); const [agentConfig, setAgentConfig] = useState([]); const [assigning, setAssigning] = useState(false); const [assignError, setAssignError] = useState(null); const panelRef = useRef(null); const cleanupRef = useRef<(() => void) | null>(null); useEffect(() => { setLoading(true); setError(null); api .getWorkItemContent(storyId) .then((data) => { setContent(data.content); setStage(data.stage); setName(data.name); setAssignedAgent(data.agent); }) .catch((err: unknown) => { setError(err instanceof Error ? err.message : "Failed to load content"); }) .finally(() => { setLoading(false); }); }, [storyId]); // Fetch test results on mount and when pipeline updates arrive. useEffect(() => { api .getTestResults(storyId) .then((data) => { setTestResults(data); }) .catch(() => { // Silently ignore — test results may not exist yet. }); }, [storyId, pipelineVersion]); // Fetch token cost on mount and when pipeline updates arrive. useEffect(() => { api .getTokenCost(storyId) .then((data) => { setTokenCost(data); }) .catch(() => { // Silently ignore — token cost may not exist yet. }); }, [storyId, pipelineVersion]); useEffect(() => { cleanupRef.current?.(); cleanupRef.current = null; setAgentInfo(null); setAgentLog([]); setAgentStatus(null); agentsApi .listAgents() .then((agents) => { const agent = agents.find((a) => a.story_id === storyId); if (!agent) return; setAgentInfo(agent); setAgentStatus(agent.status); if (agent.status === "running" || agent.status === "pending") { const cleanup = subscribeAgentStream( storyId, agent.agent_name, (event: AgentEvent) => { switch (event.type) { case "status": setAgentStatus((event.status as AgentStatusValue) ?? null); break; case "output": setAgentLog((prev) => [...prev, event.text ?? ""]); break; case "done": setAgentStatus("completed"); break; case "error": setAgentStatus("failed"); setAgentLog((prev) => [ ...prev, `[ERROR] ${event.message ?? "Unknown error"}`, ]); break; default: break; } }, ); cleanupRef.current = cleanup; } }) .catch((err: unknown) => { console.error("Failed to load agents:", err); }); return () => { cleanupRef.current?.(); cleanupRef.current = null; }; }, [storyId]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [onClose]); // Load agent config roster for the dropdown. useEffect(() => { agentsApi .getAgentConfig() .then((config) => { setAgentConfig(config); }) .catch((err: unknown) => { console.error("Failed to load agent config:", err); }); }, []); // The currently active agent name for this story (running or pending). const activeAgentName = agentInfo && (agentStatus === "running" || agentStatus === "pending") ? agentInfo.agent_name : null; const handleAgentAssign = useCallback( async (selectedAgentName: string) => { setAssigning(true); setAssignError(null); try { // Stop current running agent if there is one. if (activeAgentName) { await agentsApi.stopAgent(storyId, activeAgentName); } // Start the new agent (or skip if "none" selected). if (selectedAgentName) { await agentsApi.startAgent(storyId, selectedAgentName); } } catch (err: unknown) { setAssignError( err instanceof Error ? err.message : "Failed to assign agent", ); } finally { setAssigning(false); } }, [storyId, activeAgentName], ); return (
{/* Scrollable content area */}
{loading && (
Loading...
)} {error && (
{error}
)} {!loading && !error && content !== null && (
(

{children}

), // biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props h2: ({ children }: any) => (

{children}

), // biome-ignore lint/suspicious/noExplicitAny: react-markdown requires any for component props h3: ({ children }: any) => (

{children}

), }} > {stripDisplayContent(content)}
)}
{/* Placeholder sections for future content */} {( [{ id: "coverage", label: "Coverage" }] as { id: string; label: string; }[] ).map(({ id, label }) => (
{label}
Coming soon
))}
); }