import { createSignal, Show, onMount, For, createMemo, createEffect } from "solid-js"; import { sessions, withSession, setActiveSession } from "@/stores/session-state"; import { instances } from "@/stores/instances"; import { sendMessage } from "@/stores/session-actions"; import { addTask, setActiveTask } from "@/stores/task-actions"; import { messageStoreBus } from "@/stores/message-v2/bus"; import MessageBlockList from "@/components/message-block-list"; import { formatTokenTotal } from "@/lib/formatters"; import { addToTaskQueue, getSoloState, setActiveTaskId, toggleAutonomous, toggleAutoApproval } from "@/stores/solo-store"; import { getLogger } from "@/lib/logger"; import { Command, Plus, CheckCircle2, PanelRight, ListTodo, AtSign, Hash, Mic, ArrowUp, Terminal, ChevronRight, Loader2, AlertCircle, X, Zap, Layers, Shield, Activity, } from "lucide-solid"; import type { InstanceMessageStore } from "@/stores/message-v2/instance-store"; import type { Task } from "@/types/session"; const log = getLogger("multix-chat"); interface MultiTaskChatProps { instanceId: string; sessionId: string; } export default function MultiTaskChat(props: MultiTaskChatProps) { const selectedTaskId = () => session()?.activeTaskId || null; const setSelectedTaskId = (id: string | null) => setActiveTask(props.instanceId, props.sessionId, id || undefined); const [isSending, setIsSending] = createSignal(false); const [chatInput, setChatInput] = createSignal(""); let scrollContainer: HTMLDivElement | undefined; const [bottomSentinel, setBottomSentinel] = createSignal(null); // Scroll to bottom helper const scrollToBottom = () => { if (scrollContainer) { scrollContainer.scrollTop = scrollContainer.scrollHeight; } }; // Get current session and tasks const session = () => { const instanceSessions = sessions().get(props.instanceId); return instanceSessions?.get(props.sessionId); }; const tasks = () => session()?.tasks || []; const selectedTask = () => tasks().find(t => t.id === selectedTaskId()); // Message store integration const messageStore = () => messageStoreBus.getOrCreate(props.instanceId); const lastAssistantIndex = () => { const ids = filteredMessageIds(); const store = messageStore(); for (let i = ids.length - 1; i >= 0; i--) { const msg = store.getMessage(ids[i]); if (msg?.role === "assistant") return i; } return -1; }; // Filter messages based on selected task - use store's session messages for the task session const filteredMessageIds = () => { const task = selectedTask(); if (!task) return []; // Show no messages in Pipeline view // If task has a dedicated session, get messages from the store for that session if (task.taskSessionId) { const store = messageStore(); return store.getSessionMessageIds(task.taskSessionId); } // Fallback to task.messageIds for backward compatibility return task.messageIds || []; }; // Note: Auto-scroll is handled in two places: // 1. After sending a message (in handleSendMessage) // 2. During streaming (in the isAgentThinking effect below) // We intentionally don't scroll on message count change to let users scroll freely // Token and status tracking const sessionUsage = createMemo(() => { const store = messageStore(); return store.getSessionUsage(props.sessionId); }); const tokenStats = createMemo(() => { const usage = sessionUsage(); return { used: usage?.actualUsageTokens ?? 0, total: usage?.totalCost ?? 0, }; }); const activeTaskSessionId = createMemo(() => { const task = selectedTask(); return task?.taskSessionId || props.sessionId; }); const solo = () => getSoloState(props.instanceId); const isAgentThinking = createMemo(() => { // Show thinking while we're actively sending if (isSending()) return true; const store = messageStore(); // Check for streaming in the specific task session const taskSessionId = activeTaskSessionId(); const sessionRecord = store.state.sessions[taskSessionId]; const sessionMessages = sessionRecord ? sessionRecord.messageIds : []; const isAnyStreaming = sessionMessages.some((id: string) => { const m = store.getMessage(id); return m?.role === "assistant" && (m.status === "streaming" || m.status === "sending"); }); if (isAnyStreaming) return true; // Also check the filtered message IDs (for tasks) const ids = filteredMessageIds(); if (ids.length === 0) return false; const lastMsg = store.getMessage(ids[ids.length - 1]); return lastMsg?.role === "assistant" && (lastMsg.status === "streaming" || lastMsg.status === "sending"); }); // Auto-scroll during streaming - must be after isAgentThinking is defined createEffect(() => { const streaming = isAgentThinking(); if (!streaming) return; // During streaming, scroll periodically to keep up with content const interval = setInterval(scrollToBottom, 300); return () => clearInterval(interval); }); const handleSendMessage = async () => { const message = chatInput().trim(); if (!message || isSending()) return; setIsSending(true); log.info("handleSendMessage started", { instanceId: props.instanceId, sessionId: props.sessionId, selectedTaskId: selectedTaskId(), messageLength: message.length }); try { let taskId = selectedTaskId(); let targetSessionId = props.sessionId; // If no task selected, create one automatically if (!taskId) { log.info("No task selected, creating new task"); const title = message.length > 30 ? message.substring(0, 27) + "..." : message; const result = await addTask(props.instanceId, props.sessionId, title); taskId = result.id; targetSessionId = result.taskSessionId || props.sessionId; log.info("New task created", { taskId, targetSessionId }); setSelectedTaskId(taskId); // If autonomous mode is on, we might want to queue it or set it as active const s = solo(); if (s.isAutonomous) { log.info("Autonomous mode active, setting active task or queuing"); if (!s.activeTaskId) { setActiveTaskId(props.instanceId, taskId); } else { addToTaskQueue(props.instanceId, taskId); } } } else { const task = selectedTask(); targetSessionId = task?.taskSessionId || props.sessionId; } log.info("Target session identified", { targetSessionId, taskId }); const store = messageStore(); log.info("Message store check before sending", { instanceId: props.instanceId, storeExists: !!store, messageCount: store?.getSessionMessageIds(targetSessionId).length }); await sendMessage( props.instanceId, targetSessionId, message, [], taskId || undefined ); log.info("sendMessage call completed"); setChatInput(""); // Auto-scroll to bottom after sending setTimeout(scrollToBottom, 100); } catch (error) { log.error("handleSendMessage failed", error); console.error("[MultiTaskChat] Send failed:", error); } finally { setIsSending(false); log.info("handleSendMessage finished"); } }; const handleKeyDown = (e: KeyboardEvent) => { // Enter to submit, Shift+Enter for new line if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; return (
{/* Header */}
MULTIX
{selectedTask()?.title || "Active Task"}
{/* Task Tabs (Horizontal Scroll) */} 0}>
{(task) => ( )}
{/* Main Content Area - min-h-0 is critical for flex containers with overflow */}
true} thinkingDefaultExpanded={() => true} showUsageMetrics={() => true} scrollContainer={() => scrollContainer} setBottomSentinel={setBottomSentinel} />
}> {/* Pipeline View */}

Pipeline

Agentic Orchestration

Active Threads
{tasks().length}

No active tasks

Send a message below to start a new thread

}> {(task) => ( )}
{/* Chat Input Area - Fixed at bottom */}
{/* Header Row */}
{selectedTaskId() ? "Task Context" : "Global Pipeline"} {selectedTaskId() ? "MultiX Threaded" : "Auto-Task"}
0}>
{formatTokenTotal(tokenStats().used)}
{isAgentThinking() ? "Thinking" : "Sending"}
{/* Text Input */}