| function createAgentTab(type, initialMessage = null, autoSwitch = true, taskId = null, parentTabId = null) { |
| const tabId = tabCounter++; |
|
|
| |
| let title; |
| if (taskId) { |
| |
| title = taskId; |
| |
| taskIdToTabId[taskId] = tabId; |
| } else if (type !== 'command-center') { |
| agentCounters[type]++; |
| title = `New ${type} task`; |
| } else { |
| title = getTypeLabel(type); |
| } |
|
|
| |
| registerAgentInTimeline(tabId, type, title, parentTabId); |
|
|
| |
| const tab = document.createElement('div'); |
| tab.className = 'tab'; |
| tab.dataset.tabId = tabId; |
| tab.innerHTML = ` |
| <span class="tab-title">${title}</span> |
| <span class="tab-status" style="display: none;"><span></span><span></span><span></span></span> |
| <span class="tab-close">×</span> |
| `; |
|
|
| |
| const dynamicTabs = document.getElementById('dynamicTabs'); |
| dynamicTabs.appendChild(tab); |
|
|
| |
| if (!autoSwitch && parentTabId !== null) { |
| tab.style.display = 'none'; |
| } |
|
|
| |
| 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); |
|
|
| |
| if (autoSwitch) { |
| switchToTab(tabId); |
| } |
|
|
| |
| if (type !== 'command-center') { |
| setupInputListeners(content, tabId); |
| const input = content.querySelector('textarea'); |
|
|
| |
| if (type === 'code') { |
| startSandbox(tabId); |
| } |
|
|
| |
| if (initialMessage && input) { |
| input.value = initialMessage; |
| |
| setTimeout(() => { |
| sendMessage(tabId); |
| }, 100); |
| } |
| } |
|
|
| |
| saveWorkspaceDebounced(); |
|
|
| return tabId; |
| } |
|
|
| function createAgentContent(type, tabId, title = null) { |
| if (type === 'command-center') { |
| return document.querySelector('[data-content-id="0"]').innerHTML; |
| } |
|
|
| |
| const uniqueId = `${type}-${tabId}`; |
|
|
| |
| const displayTitle = title || `New ${type} task`; |
|
|
| return ` |
| <div class="agent-interface"> |
| <div class="agent-header"> |
| <div> |
| <div class="agent-type">${getTypeLabel(type)}</div> |
| <h2>${escapeHtml(displayTitle)}</h2> |
| </div> |
| </div> |
| <div class="agent-body"> |
| <div class="chat-container" id="messages-${uniqueId}" data-agent-type="${type}"> |
| </div> |
| </div> |
| <div class="input-area"> |
| <div class="input-container"> |
| <textarea placeholder="${getPlaceholder(type)}" id="input-${uniqueId}" rows="1"></textarea> |
| <button>SEND</button> |
| </div> |
| </div> |
| </div> |
| `; |
| } |
|
|
| function switchToTab(tabId) { |
| |
| document.querySelectorAll('.tab').forEach(tab => { |
| tab.classList.remove('active'); |
| }); |
| document.querySelectorAll('.tab-content').forEach(content => { |
| content.classList.remove('active'); |
| }); |
|
|
| |
| const tab = document.querySelector(`.tab[data-tab-id="${tabId}"]`); |
| const content = document.querySelector(`.tab-content[data-content-id="${tabId}"]`); |
|
|
| if (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; |
|
|
| |
| saveWorkspaceDebounced(); |
| } |
| } |
|
|
| |
| renderTimeline(); |
| } |
|
|
| function closeTab(tabId) { |
| if (tabId === 0) return; |
|
|
| const tab = document.querySelector(`[data-tab-id="${tabId}"]`); |
| const content = document.querySelector(`[data-content-id="${tabId}"]`); |
|
|
| if (tab && content) { |
| |
| const chatContainer = content.querySelector('.chat-container'); |
| const agentType = chatContainer?.dataset.agentType || 'chat'; |
|
|
| if (agentType === 'code') { |
| stopSandbox(tabId); |
| } |
|
|
| |
| if (timelineData[tabId]) { |
| timelineData[tabId].savedContent = content.innerHTML; |
| timelineData[tabId].savedTitle = tab.querySelector('.tab-title')?.textContent; |
| } |
|
|
| |
| for (const [taskId, mappedTabId] of Object.entries(taskIdToTabId)) { |
| if (mappedTabId === tabId) { |
| delete taskIdToTabId[taskId]; |
| break; |
| } |
| } |
|
|
| tab.remove(); |
| content.remove(); |
|
|
| |
| if (timelineData[tabId]) { |
| timelineData[tabId].isClosed = true; |
| renderTimeline(); |
| } |
|
|
| |
| if (activeTabId === tabId) { |
| switchToTab(0); |
| } |
|
|
| |
| saveWorkspaceDebounced(); |
| } |
| } |
|
|
| function showProgressWidget(chatContainer) { |
| |
| hideProgressWidget(chatContainer); |
|
|
| const widget = document.createElement('div'); |
| widget.className = 'progress-widget'; |
| widget.innerHTML = ` |
| <div class="progress-spinner"> |
| <span></span> |
| <span></span> |
| <span></span> |
| </div> |
| <span class="progress-text">Generating...</span> |
| `; |
| 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) { |
| |
| const agentBody = chatContainer.closest('.agent-body'); |
| if (!agentBody) return; |
|
|
| |
| 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; |
|
|
| |
| const content = document.querySelector(`[data-content-id="${tabId}"]`); |
| if (!content) return; |
| const chatContainer = content.querySelector('.chat-container'); |
| if (!chatContainer) return; |
|
|
| |
| const target = chatContainer.querySelector(`[data-timeline-index="${eventIndex}"]`); |
| if (!target) return; |
|
|
| |
| const agentBody = chatContainer.closest('.agent-body'); |
| if (!agentBody) return; |
|
|
| |
| requestAnimationFrame(() => { |
| target.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
| }); |
| } |
|
|
| async function abortAgent(tabId) { |
| |
| const controller = activeAbortControllers[tabId]; |
| if (controller) { |
| controller.abort(); |
| } |
|
|
| |
| if (tabId === 0) { |
| for (const [childId, td] of Object.entries(timelineData)) { |
| if (td.parentTabId === 0 && td.isGenerating) { |
| |
| const childController = activeAbortControllers[childId]; |
| if (childController) { |
| childController.abort(); |
| } |
| |
| try { |
| await apiFetch('/api/abort', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ agent_id: childId.toString() }) |
| }); |
| } catch (e) { } |
| } |
| } |
| |
| commandInputBlocked = false; |
| pendingAgentLaunches = 0; |
| setCommandCenterStopState(false); |
| const commandInput = document.getElementById('input-command'); |
| if (commandInput) { |
| commandInput.disabled = false; |
| } |
| } |
|
|
| |
| try { |
| await apiFetch('/api/abort', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ agent_id: tabId.toString() }) |
| }); |
| } catch (e) { |
| |
| } |
| } |
|
|
| async function sendMessage(tabId) { |
| |
| |
| if (timelineData[tabId]?.isGenerating || (tabId === 0 && commandInputBlocked)) { |
| abortAgent(tabId); |
| return; |
| } |
|
|
| const content = document.querySelector(`[data-content-id="${tabId}"]`); |
| if (!content) return; |
|
|
| |
| 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; |
|
|
| |
| const welcomeMsg = chatContainer.querySelector('.welcome-message'); |
| const isFirstMessage = welcomeMsg !== null; |
| if (welcomeMsg) { |
| welcomeMsg.remove(); |
| } |
|
|
| |
| const userMsg = document.createElement('div'); |
| userMsg.className = 'message user'; |
| userMsg.innerHTML = `<div class="message-content">${parseMarkdown(message.trim())}</div>`; |
| linkifyFilePaths(userMsg); |
| chatContainer.appendChild(userMsg); |
|
|
| |
| const evIdx = addTimelineEvent(tabId, 'user', message); |
| userMsg.dataset.timelineIndex = evIdx; |
|
|
| |
| const agentBody = chatContainer.closest('.agent-body'); |
| if (agentBody) { |
| agentBody.scrollTop = agentBody.scrollHeight; |
| } |
|
|
| |
| showProgressWidget(chatContainer); |
|
|
| |
| if (isFirstMessage && tabId !== 0) { |
| generateAgentTitle(tabId, message); |
| } |
|
|
| |
| input.value = ''; |
| input.style.height = 'auto'; |
| input.disabled = true; |
|
|
| |
| setTabGenerating(tabId, true); |
|
|
| |
| const agentType = getAgentTypeFromContainer(chatContainer); |
|
|
| |
| const messages = getConversationHistory(chatContainer); |
|
|
| |
| await streamChatResponse(messages, chatContainer, agentType, tabId); |
|
|
| |
| setTabGenerating(tabId, false); |
|
|
| if (tabId === 0) { |
| |
| const anyChildGenerating = Object.values(timelineData).some( |
| td => td.parentTabId === 0 && td.isGenerating |
| ); |
| if (anyChildGenerating || pendingAgentLaunches > 0) { |
| commandInputBlocked = true; |
| |
| input.disabled = true; |
| setCommandCenterStopState(true); |
| } else { |
| input.disabled = false; |
| input.focus(); |
| } |
| } else { |
| input.disabled = false; |
| input.focus(); |
| } |
|
|
| |
| 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() { |
| |
| 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); |
|
|
| |
| 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; |
|
|
| |
| const tab = document.querySelector(`[data-tab-id="${tabId}"]`); |
| if (tab) { |
| const titleEl = tab.querySelector('.tab-title'); |
| if (titleEl) { |
| titleEl.textContent = title.toUpperCase(); |
| |
| saveWorkspaceDebounced(); |
| |
| updateTimelineTitle(tabId, title.toUpperCase()); |
| } |
| } |
| } |
| } catch (error) { |
| console.error('Failed to generate title:', error); |
| |
| } |
| } |
|
|
| function getAgentTypeFromContainer(chatContainer) { |
| |
| const typeFromData = chatContainer.dataset.agentType; |
| if (typeFromData) { |
| return typeFromData; |
| } |
|
|
| |
| const containerId = chatContainer.id; |
| if (containerId && containerId.startsWith('messages-')) { |
| const type = containerId.replace('messages-', ''); |
| |
| 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'; |
| } |
|
|
| function getConversationHistory(chatContainer) { |
| const messages = []; |
| const messageElements = chatContainer.querySelectorAll('.message'); |
|
|
| messageElements.forEach(msg => { |
| if (msg.classList.contains('user')) { |
| |
| 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')) { |
| |
| const contentEl = msg.querySelector('.message-content'); |
| const content = contentEl?.textContent.trim(); |
|
|
| |
| const toolCallData = msg.getAttribute('data-tool-call'); |
| if (toolCallData) { |
| const toolCall = JSON.parse(toolCallData); |
| let funcName, funcArgs; |
|
|
| if (toolCall.function_name) { |
| |
| funcName = toolCall.function_name; |
| funcArgs = toolCall.arguments; |
| } else { |
| |
| 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')) { |
| |
| messages.push({ role: 'assistant', content: content }); |
| } |
| } else if (msg.classList.contains('tool')) { |
| |
| 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; |
| } |
|
|