function createAgentTab(type, initialMessage = null, autoSwitch = true, taskId = null, parentTabId = null) { const tabId = tabCounter++; // Use task_id if provided, otherwise generate default title let title; if (taskId) { // Convert dashes to spaces and title case for display title = taskId; // Register this agent for task_id reuse taskIdToTabId[taskId] = tabId; } else if (type !== 'command-center') { agentCounters[type]++; title = `New ${type} task`; } else { title = getTypeLabel(type); } // Register in timeline registerAgentInTimeline(tabId, type, title, parentTabId); // Create tab const tab = document.createElement('div'); tab.className = 'tab'; tab.dataset.tabId = tabId; tab.innerHTML = ` ${title} × `; // Insert into the dynamic tabs container const dynamicTabs = document.getElementById('dynamicTabs'); dynamicTabs.appendChild(tab); // Hide tab for subagents until the user clicks on them if (!autoSwitch && parentTabId !== null) { tab.style.display = 'none'; } // Create content const content = document.createElement('div'); content.className = 'tab-content'; content.dataset.contentId = tabId; content.innerHTML = createAgentContent(type, tabId, title); document.querySelector('.main-content').appendChild(content); // Only switch to new tab if autoSwitch is true if (autoSwitch) { switchToTab(tabId); } // Add event listeners for the new content if (type !== 'command-center') { setupInputListeners(content, tabId); const input = content.querySelector('textarea'); // If this is a code agent, start the sandbox proactively if (type === 'code') { startSandbox(tabId); } // If there's an initial message, automatically send it if (initialMessage && input) { input.value = initialMessage; // Small delay to ensure everything is set up setTimeout(() => { sendMessage(tabId); }, 100); } } // Save workspace state after creating new tab saveWorkspaceDebounced(); return tabId; // Return the tabId so we can reference it } function createAgentContent(type, tabId, title = null) { if (type === 'command-center') { return document.querySelector('[data-content-id="0"]').innerHTML; } // Use unique ID combining type and tabId to ensure unique container IDs const uniqueId = `${type}-${tabId}`; // Display title or default const displayTitle = title || `New ${type} task`; return `
${getTypeLabel(type)}

${escapeHtml(displayTitle)}

`; } function switchToTab(tabId) { // Deactivate all tabs document.querySelectorAll('.tab').forEach(tab => { tab.classList.remove('active'); }); document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); // Activate selected tab (use .tab selector to avoid matching timeline elements) const tab = document.querySelector(`.tab[data-tab-id="${tabId}"]`); const content = document.querySelector(`.tab-content[data-content-id="${tabId}"]`); if (content) { // For settings, there's no tab, just show the content if (tabId === 'settings') { content.classList.add('active'); activeTabId = tabId; } else if (tab) { tab.style.display = ''; tab.classList.add('active'); content.classList.add('active'); activeTabId = tabId; // Save workspace state after switching tabs saveWorkspaceDebounced(); } } // Update timeline to reflect active tab renderTimeline(); } function closeTab(tabId) { if (tabId === 0) return; // Can't close command center const tab = document.querySelector(`[data-tab-id="${tabId}"]`); const content = document.querySelector(`[data-content-id="${tabId}"]`); if (tab && content) { // Check if this is a code agent and stop its sandbox const chatContainer = content.querySelector('.chat-container'); const agentType = chatContainer?.dataset.agentType || 'chat'; if (agentType === 'code') { stopSandbox(tabId); } // Save the tab's content BEFORE removing from DOM so we can restore it later if (timelineData[tabId]) { timelineData[tabId].savedContent = content.innerHTML; timelineData[tabId].savedTitle = tab.querySelector('.tab-title')?.textContent; } // Clean up task_id mapping for (const [taskId, mappedTabId] of Object.entries(taskIdToTabId)) { if (mappedTabId === tabId) { delete taskIdToTabId[taskId]; break; } } tab.remove(); content.remove(); // Mark as closed in timeline (don't remove - allow reopening) if (timelineData[tabId]) { timelineData[tabId].isClosed = true; renderTimeline(); } // Switch to command center if closing active tab if (activeTabId === tabId) { switchToTab(0); } // Save workspace state after closing tab saveWorkspaceDebounced(); } } function showProgressWidget(chatContainer) { // Remove any existing progress widget hideProgressWidget(chatContainer); const widget = document.createElement('div'); widget.className = 'progress-widget'; widget.innerHTML = `
Generating... `; chatContainer.appendChild(widget); scrollChatToBottom(chatContainer, true); return widget; } function hideProgressWidget(chatContainer) { const widget = chatContainer.querySelector('.progress-widget'); if (widget) { widget.remove(); } } function scrollChatToBottom(chatContainer, force = false) { // The actual scrolling container is .agent-body const agentBody = chatContainer.closest('.agent-body'); if (!agentBody) return; // If not forced, only scroll if user is already near the bottom if (!force) { const distanceFromBottom = agentBody.scrollHeight - agentBody.scrollTop - agentBody.clientHeight; if (distanceFromBottom > 150) return; } agentBody.scrollTop = agentBody.scrollHeight; } function scrollToTimelineEvent(tabId, eventIndex) { if (eventIndex === undefined || eventIndex === null) return; // Find the tab's content area and chat container const content = document.querySelector(`[data-content-id="${tabId}"]`); if (!content) return; const chatContainer = content.querySelector('.chat-container'); if (!chatContainer) return; // Find the chat element tagged with this timeline index const target = chatContainer.querySelector(`[data-timeline-index="${eventIndex}"]`); if (!target) return; // Scroll the agent-body so the target is near the top const agentBody = chatContainer.closest('.agent-body'); if (!agentBody) return; // Use a small delay to ensure tab switch has rendered requestAnimationFrame(() => { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); } async function abortAgent(tabId) { // Abort the frontend fetch const controller = activeAbortControllers[tabId]; if (controller) { controller.abort(); } // For command center (tab 0): also abort all generating child tabs if (tabId === 0) { for (const [childId, td] of Object.entries(timelineData)) { if (td.parentTabId === 0 && td.isGenerating) { // Abort frontend fetch const childController = activeAbortControllers[childId]; if (childController) { childController.abort(); } // Abort backend agent try { await apiFetch('/api/abort', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agent_id: childId.toString() }) }); } catch (e) { /* ignore */ } } } // Unblock command center input and restore SEND button commandInputBlocked = false; pendingAgentLaunches = 0; setCommandCenterStopState(false); const commandInput = document.getElementById('input-command'); if (commandInput) { commandInput.disabled = false; } } // Tell the backend to set the abort flag for this agent try { await apiFetch('/api/abort', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agent_id: tabId.toString() }) }); } catch (e) { // Ignore abort endpoint errors } } async function sendMessage(tabId) { // If tab is currently generating, abort instead of sending // For command center: also abort if input is blocked (sub-agents still running) if (timelineData[tabId]?.isGenerating || (tabId === 0 && commandInputBlocked)) { abortAgent(tabId); return; } const content = document.querySelector(`[data-content-id="${tabId}"]`); if (!content) return; // Support both textarea and input const input = content.querySelector('textarea') || content.querySelector('input[type="text"]'); const chatContainer = content.querySelector('.chat-container'); if (!input || !chatContainer) return; const message = input.value.trim(); if (!message) return; // Remove welcome message if it exists (only on first user message) const welcomeMsg = chatContainer.querySelector('.welcome-message'); const isFirstMessage = welcomeMsg !== null; if (welcomeMsg) { welcomeMsg.remove(); } // Add user message const userMsg = document.createElement('div'); userMsg.className = 'message user'; userMsg.innerHTML = `
${parseMarkdown(message.trim())}
`; linkifyFilePaths(userMsg); chatContainer.appendChild(userMsg); // Add to timeline and tag DOM element for scroll-to-turn const evIdx = addTimelineEvent(tabId, 'user', message); userMsg.dataset.timelineIndex = evIdx; // Scroll the agent body (the actual scrolling container) to bottom const agentBody = chatContainer.closest('.agent-body'); if (agentBody) { agentBody.scrollTop = agentBody.scrollHeight; } // Show progress widget while waiting for response showProgressWidget(chatContainer); // Generate a title for the agent if this is the first message and not command center if (isFirstMessage && tabId !== 0) { generateAgentTitle(tabId, message); } // Clear input and disable it during processing input.value = ''; input.style.height = 'auto'; // Reset textarea height input.disabled = true; // Set tab to generating state setTabGenerating(tabId, true); // Determine agent type from chat container ID const agentType = getAgentTypeFromContainer(chatContainer); // Send full conversation history for all agent types (stateless backend) const messages = getConversationHistory(chatContainer); // Stream response from backend await streamChatResponse(messages, chatContainer, agentType, tabId); // Re-enable input and mark generation as complete setTabGenerating(tabId, false); if (tabId === 0) { // Command center: keep input blocked if launched agents are still running or pending const anyChildGenerating = Object.values(timelineData).some( td => td.parentTabId === 0 && td.isGenerating ); if (anyChildGenerating || pendingAgentLaunches > 0) { commandInputBlocked = true; // Keep STOP button visible and input disabled while children run input.disabled = true; setCommandCenterStopState(true); } else { input.disabled = false; input.focus(); } } else { input.disabled = false; input.focus(); } // Save workspace state after message exchange completes saveWorkspaceDebounced(); } function setCommandCenterStopState(showStop) { const content = document.querySelector('[data-content-id="0"]'); if (!content) return; const sendBtn = content.querySelector('.input-container button'); if (!sendBtn) return; if (showStop) { sendBtn.textContent = 'STOP'; sendBtn.classList.add('stop-btn'); sendBtn.disabled = false; } else { sendBtn.textContent = 'SEND'; sendBtn.classList.remove('stop-btn'); } } async function continueCommandCenter() { // Called when all launched agents finish — re-run command center with actual results in history const chatContainer = document.getElementById('messages-command'); const commandInput = document.getElementById('input-command'); if (!chatContainer) return; setTabGenerating(0, true); showProgressWidget(chatContainer); const messages = getConversationHistory(chatContainer); await streamChatResponse(messages, chatContainer, 'command', 0); setTabGenerating(0, false); // Check if new agents were launched (recursive blocking) const anyChildGenerating = Object.values(timelineData).some( td => td.parentTabId === 0 && td.isGenerating ); if (anyChildGenerating || pendingAgentLaunches > 0) { commandInputBlocked = true; if (commandInput) commandInput.disabled = true; setCommandCenterStopState(true); } else if (commandInput) { commandInput.disabled = false; commandInput.focus(); } saveWorkspaceDebounced(); } async function generateAgentTitle(tabId, query) { const currentSettings = getSettings(); const backendEndpoint = '/api'; const llmEndpoint = currentSettings.endpoint || 'https://api.openai.com/v1'; const modelToUse = currentSettings.model || 'gpt-4'; try { const response = await apiFetch(`${backendEndpoint}/generate-title`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query, endpoint: llmEndpoint, token: currentSettings.token || null, model: modelToUse }) }); if (response.ok) { const result = await response.json(); const title = result.title; // Update the tab title const tab = document.querySelector(`[data-tab-id="${tabId}"]`); if (tab) { const titleEl = tab.querySelector('.tab-title'); if (titleEl) { titleEl.textContent = title.toUpperCase(); // Save workspace after title update saveWorkspaceDebounced(); // Update timeline to reflect new title updateTimelineTitle(tabId, title.toUpperCase()); } } } } catch (error) { console.error('Failed to generate title:', error); // Don't show error to user, just keep the default title } } function getAgentTypeFromContainer(chatContainer) { // Try to get type from data attribute first (for dynamically created agents) const typeFromData = chatContainer.dataset.agentType; if (typeFromData) { return typeFromData; } // Fallback: Extract agent type from the container ID (e.g., "messages-command" -> "command") const containerId = chatContainer.id; if (containerId && containerId.startsWith('messages-')) { const type = containerId.replace('messages-', ''); // Map to agent type if (type === 'command') return 'command'; if (type.startsWith('agent')) return 'agent'; if (type.startsWith('code')) return 'code'; if (type.startsWith('research')) return 'research'; if (type.startsWith('chat')) return 'chat'; } return 'command'; // Default fallback } function getConversationHistory(chatContainer) { const messages = []; const messageElements = chatContainer.querySelectorAll('.message'); messageElements.forEach(msg => { if (msg.classList.contains('user')) { // User messages use .message-content const contentEl = msg.querySelector('.message-content'); const content = contentEl?.textContent.trim() || msg.textContent.trim(); if (content) { messages.push({ role: 'user', content: content }); } } else if (msg.classList.contains('assistant')) { // Assistant messages use .message-content const contentEl = msg.querySelector('.message-content'); const content = contentEl?.textContent.trim(); // Check if this message has a tool call const toolCallData = msg.getAttribute('data-tool-call'); if (toolCallData) { const toolCall = JSON.parse(toolCallData); let funcName, funcArgs; if (toolCall.function_name) { // Agent-style tool call (web_search, read_url, etc.) funcName = toolCall.function_name; funcArgs = toolCall.arguments; } else { // Command center-style tool call (launch_*_agent) funcName = `launch_${toolCall.agent_type}_agent`; funcArgs = JSON.stringify({ task: toolCall.message, topic: toolCall.message, message: toolCall.message }); } messages.push({ role: 'assistant', content: toolCall.thinking || content || '', tool_calls: [{ id: toolCall.tool_call_id || 'tool_' + Date.now(), type: 'function', function: { name: funcName, arguments: funcArgs } }] }); } else if (content && !content.includes('msg-') && !content.includes('→ Launched')) { // Regular assistant message (exclude launch notifications) messages.push({ role: 'assistant', content: content }); } } else if (msg.classList.contains('tool')) { // Tool response message const toolResponseData = msg.getAttribute('data-tool-response'); if (toolResponseData) { const toolResponse = JSON.parse(toolResponseData); messages.push({ role: 'tool', tool_call_id: toolResponse.tool_call_id, content: toolResponse.content }); } } }); return messages; }