async function streamChatResponse(messages, chatContainer, agentType, tabId) { const currentSettings = getSettings(); const backendEndpoint = '/api'; // Resolve model configuration for this agent type let modelConfig = resolveModelConfig(agentType); if (!modelConfig) { modelConfig = getDefaultModelConfig(); } if (!modelConfig) { // No models configured - show error const errorDiv = document.createElement('div'); errorDiv.className = 'message assistant error'; errorDiv.innerHTML = `
`; chatContainer.appendChild(errorDiv); return; } // Resolve research sub-agent model if configured let researchSubAgentConfig = null; if (currentSettings.researchSubAgentModel) { const subAgentModel = currentSettings.models?.[currentSettings.researchSubAgentModel]; if (subAgentModel) { const subAgentProvider = currentSettings.providers?.[subAgentModel.providerId]; if (subAgentProvider) { researchSubAgentConfig = { endpoint: subAgentProvider.endpoint, token: subAgentProvider.token, model: subAgentModel.modelId }; } } } // Resolve image model selections to HF model ID strings const imageGenModelId = currentSettings.imageGenModel ? currentSettings.models?.[currentSettings.imageGenModel]?.modelId || null : null; const imageEditModelId = currentSettings.imageEditModel ? currentSettings.models?.[currentSettings.imageEditModel]?.modelId || null : null; // Set up AbortController for this request const abortController = new AbortController(); if (tabId !== undefined) { activeAbortControllers[tabId] = abortController; } try { // Determine parent agent ID for abort propagation const parentAgentId = timelineData[tabId]?.parentTabId != null ? timelineData[tabId].parentTabId.toString() : null; const response = await apiFetch(`${backendEndpoint}/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: abortController.signal, body: JSON.stringify({ messages: messages, agent_type: agentType, stream: true, endpoint: modelConfig.endpoint, token: modelConfig.token || null, model: modelConfig.model, extra_params: modelConfig.extraParams || null, multimodal: modelConfig.multimodal || false, e2b_key: currentSettings.e2bKey || null, serper_key: currentSettings.serperKey || null, hf_token: currentSettings.hfToken || null, image_gen_model: imageGenModelId, image_edit_model: imageEditModelId, research_sub_agent_model: researchSubAgentConfig?.model || null, research_sub_agent_endpoint: researchSubAgentConfig?.endpoint || null, research_sub_agent_token: researchSubAgentConfig?.token || null, research_sub_agent_extra_params: researchSubAgentConfig?.extraParams || null, research_parallel_workers: currentSettings.researchParallelWorkers || null, research_max_websites: currentSettings.researchMaxWebsites || null, agent_id: tabId.toString(), // Send unique tab ID for sandbox sessions parent_agent_id: parentAgentId, // Parent agent ID for abort propagation frontend_context: getFrontendContext() // Dynamic context for system prompts }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let fullResponse = ''; let currentMessageEl = null; let progressHidden = false; // Flush any accumulated assistant text to the timeline // Call before adding tool/code/report timeline events to preserve ordering function flushResponseToTimeline() { if (fullResponse && tabId !== undefined) { const evIdx = addTimelineEvent(tabId, 'assistant', fullResponse); if (currentMessageEl) currentMessageEl.dataset.timelineIndex = evIdx; fullResponse = ''; } } while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const data = JSON.parse(line.slice(6)); // Hide progress widget on first meaningful response if (!progressHidden && data.type !== 'generating' && data.type !== 'retry' && !data.type.startsWith('debug_')) { hideProgressWidget(chatContainer); progressHidden = true; } // Handle different message types from backend if (data.type === 'thinking') { // Assistant thinking - create message if not exists if (!currentMessageEl) { currentMessageEl = createAssistantMessage(chatContainer); } fullResponse += data.content; appendToMessage(currentMessageEl, resolveGlobalFigureRefs(parseMarkdown(fullResponse))); scrollChatToBottom(chatContainer); } else if (data.type === 'code') { // Code execution result - update the last code cell updateLastCodeCell(chatContainer, data.output, data.error, data.images); currentMessageEl = null; // Reset for next thinking scrollChatToBottom(chatContainer); } else if (data.type === 'code_start') { // Code cell starting execution - show with spinner flushResponseToTimeline(); createCodeCell(chatContainer, data.code, null, false, true); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline - show code snippet preview const codePreview = data.code ? data.code.split('\n')[0] : 'code execution'; const codeEvIdx = addTimelineEvent(tabId, 'assistant', codePreview, null, { tag: 'CODE' }); chatContainer.lastElementChild.dataset.timelineIndex = codeEvIdx; } else if (data.type === 'upload') { // File upload notification flushResponseToTimeline(); createUploadMessage(chatContainer, data.paths, data.output); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline const upEvIdx = addTimelineEvent(tabId, 'assistant', data.paths?.join(', ') || 'files', null, { tag: 'UPLOAD' }); chatContainer.lastElementChild.dataset.timelineIndex = upEvIdx; } else if (data.type === 'download') { // File download notification flushResponseToTimeline(); createDownloadMessage(chatContainer, data.paths, data.output); currentMessageEl = null; scrollChatToBottom(chatContainer); // Add to timeline const dlEvIdx = addTimelineEvent(tabId, 'assistant', data.paths?.join(', ') || 'files', null, { tag: 'DOWNLOAD' }); chatContainer.lastElementChild.dataset.timelineIndex = dlEvIdx; } else if (data.type === 'generating') { // Still generating - no action needed } else if (data.type === 'result') { // References are globally namespaced by the backend (e.g., figure_T3_1) const resultText = data.content || ''; // Populate global registry if (data.figures) { for (const [name, figData] of Object.entries(data.figures)) { if (new RegExp(`?${name}>`, 'i').test(resultText)) { globalFigureRegistry[name] = figData; } } } // Agent result - update command center widget updateActionWidgetWithResult(tabId, resultText, data.figures || {}); } else if (data.type === 'result_preview') { // Show result preview flushResponseToTimeline(); // Replace${placeholderId}
`, 'g'), imageHtml); html = html.replace(new RegExp(placeholderId, 'g'), imageHtml); } // Store raw result as hidden assistant message for history reconstruction const resultMsg = document.createElement('div'); resultMsg.className = 'message assistant'; resultMsg.style.display = 'none'; resultMsg.innerHTML = ``; chatContainer.appendChild(resultMsg); // Create result block const resultDiv = document.createElement('div'); resultDiv.className = 'agent-result'; resultDiv.innerHTML = `${abortResultText}
${escapeHtml(code)}${placeholderId}
`, 'g'), imageHtml); html = html.replace(new RegExp(placeholderId, 'g'), imageHtml); } // Add result section inside the body const body = widget.querySelector('.action-widget-body'); if (body) { const resultSection = document.createElement('div'); resultSection.className = 'action-widget-result-section'; resultSection.innerHTML = `${highlighted}`;
}
return `${escapeHtml(code)}`;
};
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: false, // Don't convert \n to ${escapeHtml(text).replace(/\n\n/g, '
').replace(/\n/g, '
')}