import { useRef, useEffect, useState, useCallback } from "react"; import { SimpleMarkdown } from "../SimpleMarkdown"; import type { TaskOutputEvent } from "../../hooks/useTaskSubscription"; import { sendTaskMessage } from "../../lib/api"; import { PhaseConfirmationInline, type PhaseConfirmationData, } from "../contracts/PhaseConfirmationModal"; import type { ContractPhase } from "../../lib/api"; interface TaskOutputProps { /** Array of parsed output events from the backend */ entries: TaskOutputEvent[]; isStreaming: boolean; /** Name of subtask whose output is being viewed (null = parent task) */ viewingSubtaskName?: string | null; /** Callback to return to parent task output */ onClearSubtaskView?: () => void; onClear?: () => void; /** Task ID for sending input (if provided, shows input bar when streaming) */ taskId?: string | null; /** Callback when user sends input (to show it immediately in output) */ onUserInput?: (message: string) => void; /** Set of pending question IDs (for supervisor questions) */ pendingQuestionIds?: Set; /** Callback to answer a supervisor question */ onAnswerQuestion?: (questionId: string, response: string) => Promise; } export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSubtaskView, onClear, taskId, onUserInput, pendingQuestionIds, onAnswerQuestion, }: TaskOutputProps) { const containerRef = useRef(null); const [autoScroll, setAutoScroll] = useState(true); const [inputValue, setInputValue] = useState(""); const [sendingInput, setSendingInput] = useState(false); const [inputError, setInputError] = useState(null); const inputRef = useRef(null); // Handle scroll to check if user has scrolled up const handleScroll = useCallback(() => { if (!containerRef.current) return; const { scrollTop, scrollHeight, clientHeight } = containerRef.current; const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; setAutoScroll(isAtBottom); }, []); // Auto-scroll when entries change useEffect(() => { if (autoScroll && containerRef.current) { containerRef.current.scrollTop = containerRef.current.scrollHeight; } }, [entries, autoScroll]); // Handle sending input to the task const handleSendInput = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!taskId || !inputValue.trim() || sendingInput) return; const message = inputValue.trim(); setSendingInput(true); setInputError(null); // Show user input immediately in the output window onUserInput?.(message); try { await sendTaskMessage(taskId, message); setInputValue(""); inputRef.current?.focus(); } catch (err) { setInputError(err instanceof Error ? err.message : "Failed to send input"); } finally { setSendingInput(false); } }, [taskId, inputValue, sendingInput, onUserInput]); // Show input bar when task is running and has a valid taskId const showInputBar = isStreaming && taskId; return (
{/* Header */}
{viewingSubtaskName ? ( <> Subtask: {viewingSubtaskName} ) : ( Output )} {isStreaming && ( Live )}
{!autoScroll && ( )} {onClear && entries.length > 0 && ( )}
{/* Output area */}
{entries.length === 0 ? (
{isStreaming ? "Waiting for output..." : "No output yet"}
) : (
{entries.map((entry, idx) => ( ))} {isStreaming && ( )}
)}
{/* Input bar for sending messages to running tasks */} {showInputBar && (
{inputError && (
{inputError}
)}
> setInputValue(e.target.value)} placeholder={sendingInput ? "Sending..." : "Send input to Claude..."} disabled={sendingInput} className="flex-1 bg-transparent border-none outline-none font-mono text-sm text-white placeholder-[#555]" />
)}
); } interface OutputEntryRendererProps { entry: TaskOutputEvent; pendingQuestionIds?: Set; onAnswerQuestion?: (questionId: string, response: string) => Promise; } function OutputEntryRenderer({ entry, pendingQuestionIds, onAnswerQuestion }: OutputEntryRendererProps) { const [expanded, setExpanded] = useState(false); switch (entry.messageType) { case "user_input": return (
You:
{entry.content}
); case "system": return (
{entry.content}
); case "assistant": return (
); case "tool_use": return (
* {entry.toolName || "unknown"} {entry.toolInput && Object.keys(entry.toolInput).length > 0 && ( )}
{expanded && entry.toolInput && (
              {JSON.stringify(entry.toolInput, null, 2)}
            
)}
); case "tool_result": if (!entry.content) return null; return (
{entry.isError ? "x" : "+"} {" "} {entry.content.split("\n")[0]} {entry.content.includes("\n") && "..."}
); case "result": return (
Result:
{(entry.costUsd !== undefined || entry.durationMs !== undefined) && (
{entry.durationMs !== undefined && ( Duration: {(entry.durationMs / 1000).toFixed(1)}s )} {entry.costUsd !== undefined && entry.durationMs !== undefined && " | "} {entry.costUsd !== undefined && ( Cost: ${entry.costUsd.toFixed(4)} )}
)}
); case "error": return (
{entry.content}
); case "raw": return (
{entry.content}
); case "auth_required": return ; case "supervisor_question": return ( ); case "phase_confirmation": return ( ); default: return null; } } function SupervisorQuestionEntry({ entry, pendingQuestionIds, onAnswerQuestion, }: { entry: TaskOutputEvent; pendingQuestionIds?: Set; onAnswerQuestion?: (questionId: string, response: string) => Promise; }) { const questionId = entry.toolInput?.question_id as string; const choices = (entry.toolInput?.choices as string[]) || []; const context = entry.toolInput?.context as string | null; const multiSelect = (entry.toolInput?.multi_select as boolean) ?? false; const [customInput, setCustomInput] = useState(""); const [showOther, setShowOther] = useState(false); const [submitting, setSubmitting] = useState(false); const [selectedChoices, setSelectedChoices] = useState>(new Set()); const isPending = pendingQuestionIds?.has(questionId) ?? false; const handleChoiceSelect = async (choice: string) => { if (!onAnswerQuestion || submitting) return; if (multiSelect) { // Toggle selection for multi-select mode setSelectedChoices(prev => { const newSet = new Set(prev); if (newSet.has(choice)) { newSet.delete(choice); } else { newSet.add(choice); } return newSet; }); } else { // Single select - submit immediately setSubmitting(true); try { await onAnswerQuestion(questionId, choice); } finally { setSubmitting(false); } } }; const handleMultiSelectSubmit = async () => { if (!onAnswerQuestion || submitting || selectedChoices.size === 0) return; setSubmitting(true); try { // Join selected choices with comma const response = Array.from(selectedChoices).join(", "); await onAnswerQuestion(questionId, response); setSelectedChoices(new Set()); } finally { setSubmitting(false); } }; const handleOtherSubmit = async () => { if (!onAnswerQuestion || !customInput.trim() || submitting) return; setSubmitting(true); try { await onAnswerQuestion(questionId, customInput.trim()); setCustomInput(""); } finally { setSubmitting(false); } }; return (
? Question {multiSelect && isPending && ( (select multiple) )} {!isPending && ( (Answered) )}
{context && (

{context}

)}

{entry.content}

{isPending && (
{choices.length > 0 && (
{choices.map((choice, idx) => { const isSelected = selectedChoices.has(choice); return ( ); })}
)} {/* Submit button for multi-select mode */} {multiSelect && selectedChoices.size > 0 && ( )} {/* Other option */} {!showOther ? ( ) : (
setCustomInput(e.target.value)} placeholder="Type custom response..." disabled={submitting} className="flex-1 px-2 py-1 bg-[#0a1525] border border-amber-500/30 text-amber-100 text-sm rounded focus:outline-none focus:border-amber-400" onKeyDown={(e) => { if (e.key === "Enter" && customInput.trim()) { handleOtherSubmit(); } }} />
)}
)}
); } function AuthRequiredEntry({ entry }: { entry: TaskOutputEvent }) { const [authCode, setAuthCode] = useState(""); const [submitting, setSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(null); const loginUrl = entry.toolInput?.loginUrl as string | undefined; const hostname = entry.toolInput?.hostname as string | undefined; // Get taskId from entry or fallback to toolInput (for robustness) const taskId = entry.taskId || (entry.toolInput?.taskId as string | undefined); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!authCode.trim() || !taskId) return; setSubmitting(true); setError(null); try { // Send the auth code to the task via the message endpoint await sendTaskMessage(taskId, `AUTH_CODE:${authCode.trim()}`); setSubmitted(true); } catch (err) { setError(err instanceof Error ? err.message : "Failed to submit code"); } finally { setSubmitting(false); } }; if (submitted) { return (
Authentication code submitted

Waiting for authentication to complete...

); } return (
🔐 Authentication Required{hostname ? ` (${hostname})` : ""}

The daemon's OAuth token has expired. Click the button to login, then paste the code below:

{loginUrl ? ( 1. Login to Claude ) : (

Login URL not available

)}
setAuthCode(e.target.value)} placeholder="2. Paste authentication code here" className="flex-1 bg-[#0a1525] border border-amber-500/30 rounded px-3 py-2 text-amber-100 placeholder-amber-500/50 focus:outline-none focus:border-amber-400" disabled={submitting} />
{error && (

{error}

)}
); } /** Entry for phase transition confirmations */ function PhaseConfirmationEntry({ entry, pendingQuestionIds, onAnswerQuestion, }: { entry: TaskOutputEvent; pendingQuestionIds?: Set; onAnswerQuestion?: (questionId: string, response: string) => Promise; }) { const questionId = entry.toolInput?.question_id as string; const currentPhase = entry.toolInput?.current_phase as ContractPhase; const nextPhase = entry.toolInput?.next_phase as ContractPhase; const contractId = entry.toolInput?.contract_id as string; const contractName = entry.toolInput?.contract_name as string | undefined; const summary = entry.toolInput?.summary as string | undefined; const deliverables = entry.toolInput?.deliverables as | Array<{ name: string; completed: boolean }> | undefined; const isPending = pendingQuestionIds?.has(questionId) ?? false; const data: PhaseConfirmationData = { questionId, contractId, contractName, currentPhase, nextPhase, summary, deliverables, }; const handleApprove = async (qId: string) => { if (!onAnswerQuestion) return; await onAnswerQuestion(qId, "APPROVE"); }; const handleRequestChanges = async (qId: string, feedback: string) => { if (!onAnswerQuestion) return; await onAnswerQuestion(qId, `CHANGES_REQUESTED: ${feedback}`); }; return ( ); }