From 8b17a175c3e7e27b789812eba4e3cd760beadb10 Mon Sep 17 00:00:00 2001 From: soryu Date: Tue, 6 Jan 2026 04:08:11 +0000 Subject: Initial Control system --- makima/frontend/src/components/mesh/TaskOutput.tsx | 281 +++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 makima/frontend/src/components/mesh/TaskOutput.tsx (limited to 'makima/frontend/src/components/mesh/TaskOutput.tsx') diff --git a/makima/frontend/src/components/mesh/TaskOutput.tsx b/makima/frontend/src/components/mesh/TaskOutput.tsx new file mode 100644 index 0000000..10de225 --- /dev/null +++ b/makima/frontend/src/components/mesh/TaskOutput.tsx @@ -0,0 +1,281 @@ +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(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]" + /> + +
+
+ )} +
+ ); +} + +function OutputEntryRenderer({ entry }: { entry: TaskOutputEvent }) { + 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} +
+ ); + + default: + return null; + } +} -- cgit v1.2.3