import { useRef, useEffect, useState, useCallback } from "react";
import { SimpleMarkdown } from "../SimpleMarkdown";
import type { TaskOutputEvent } from "../../hooks/useTaskSubscription";
import { sendTaskMessage } 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;
}
export function TaskOutput({ entries, isStreaming, viewingSubtaskName, onClearSubtaskView, onClear, taskId, onUserInput }: TaskOutputProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const [inputValue, setInputValue] = useState("");
const [sendingInput, setSendingInput] = useState(false);
const [inputError, setInputError] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(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 (
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2 border-b border-dashed border-[rgba(117,170,252,0.35)] shrink-0">
<div className="flex items-center gap-2">
{viewingSubtaskName ? (
<>
<button
onClick={onClearSubtaskView}
className="font-mono text-xs text-[#75aafc] hover:text-[#9bc3ff] transition-colors"
>
<
</button>
<span className="font-mono text-xs text-green-400 tracking-wide uppercase">
Subtask: {viewingSubtaskName}
</span>
</>
) : (
<span className="font-mono text-xs text-[#9bc3ff] tracking-wide uppercase">
Output
</span>
)}
{isStreaming && (
<span className="flex items-center gap-1.5 px-2 py-0.5 bg-green-400/10 border border-green-400/30 text-green-400 font-mono text-[10px] uppercase">
<span className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse" />
Live
</span>
)}
</div>
<div className="flex items-center gap-2">
{!autoScroll && (
<button
onClick={() => {
setAutoScroll(true);
if (containerRef.current) {
containerRef.current.scrollTop =
containerRef.current.scrollHeight;
}
}}
className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
>
Resume Scroll
</button>
)}
{onClear && entries.length > 0 && (
<button
onClick={onClear}
className="px-2 py-1 font-mono text-[10px] text-[#9bc3ff] border border-[rgba(117,170,252,0.25)] hover:border-[#3f6fb3] transition-colors uppercase"
>
Clear
</button>
)}
</div>
</div>
{/* Output area */}
<div
ref={containerRef}
onScroll={handleScroll}
className="flex-1 overflow-auto bg-[#0a0f18] p-3 font-mono text-xs min-h-0"
>
{entries.length === 0 ? (
<div className="text-[#555] italic">
{isStreaming ? "Waiting for output..." : "No output yet"}
</div>
) : (
<div className="space-y-3">
{entries.map((entry, idx) => (
<OutputEntryRenderer key={idx} entry={entry} />
))}
{isStreaming && (
<span className="inline-block w-2 h-4 bg-[#9bc3ff] animate-pulse" />
)}
</div>
)}
</div>
{/* Input bar for sending messages to running tasks */}
{showInputBar && (
<div className="shrink-0 border-t border-[rgba(117,170,252,0.35)] bg-[#0d1b2d]">
{inputError && (
<div className="px-3 py-1 bg-red-900/20 text-red-400 text-xs font-mono">
{inputError}
</div>
)}
<form onSubmit={handleSendInput} className="flex items-center gap-2 px-3 py-2">
<span className="text-green-400 font-mono text-sm">></span>
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => 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]"
/>
<button
type="submit"
disabled={sendingInput || !inputValue.trim()}
className="px-3 py-1 font-mono text-xs text-green-400 border border-green-400/30 hover:border-green-400/50 hover:bg-green-400/10 disabled:opacity-50 disabled:cursor-not-allowed transition-colors uppercase"
>
{sendingInput ? "..." : "Send"}
</button>
</form>
</div>
)}
</div>
);
}
function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) {
const [expanded, setExpanded] = useState(false);
switch (entry.messageType) {
case "user_input":
return (
<div className="pl-2 border-l-2 border-cyan-400/50">
<div className="flex items-center gap-2">
<span className="text-cyan-400 text-[10px] uppercase tracking-wide">You:</span>
</div>
<div className="text-cyan-300 mt-1">{entry.content}</div>
</div>
);
case "system":
return (
<div className="text-[#555] text-[10px] uppercase tracking-wide">
{entry.content}
</div>
);
case "assistant":
return (
<div className="pl-2 border-l-2 border-[#3f6fb3]">
<SimpleMarkdown content={entry.content} className="text-[#9bc3ff]" />
</div>
);
case "tool_use":
return (
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className="text-yellow-500">*</span>
<span className="text-[#75aafc]">{entry.toolName || "unknown"}</span>
{entry.toolInput && Object.keys(entry.toolInput).length > 0 && (
<button
onClick={() => setExpanded(!expanded)}
className="text-[#555] hover:text-[#9bc3ff] text-[10px]"
>
{expanded ? "[-]" : "[+]"}
</button>
)}
</div>
{expanded && entry.toolInput && (
<pre className="ml-4 text-[10px] text-[#555] bg-[#0a1525] p-2 overflow-x-auto">
{JSON.stringify(entry.toolInput, null, 2)}
</pre>
)}
</div>
);
case "tool_result":
if (!entry.content) return null;
return (
<div className="ml-4 text-[10px]">
<span className={entry.isError ? "text-red-400" : "text-green-500"}>
{entry.isError ? "x" : "+"}
</span>{" "}
<span className="text-[#555]">
{entry.content.split("\n")[0]}
{entry.content.includes("\n") && "..."}
</span>
</div>
);
case "result":
return (
<div className="border-t border-[rgba(117,170,252,0.2)] pt-2 mt-2">
<div className="text-green-500 font-semibold mb-1">Result:</div>
<SimpleMarkdown content={entry.content} className="text-[#9bc3ff]" />
{(entry.costUsd !== undefined || entry.durationMs !== undefined) && (
<div className="text-[10px] text-[#555] mt-2">
{entry.durationMs !== undefined && (
<span>Duration: {(entry.durationMs / 1000).toFixed(1)}s</span>
)}
{entry.costUsd !== undefined && entry.durationMs !== undefined && " | "}
{entry.costUsd !== undefined && (
<span>Cost: ${entry.costUsd.toFixed(4)}</span>
)}
</div>
)}
</div>
);
case "error":
return (
<div className="text-red-400 pl-2 border-l-2 border-red-400/50">
{entry.content}
</div>
);
case "raw":
return (
<div className="text-[#555] text-[10px]">
{entry.content}
</div>
);
default:
return null;
}
}