// WebSocket connection and chat functionality let socket = null; let sessionId = getSessionId(); let isConnected = false; // DOM elements const messagesContainer = document.getElementById('messages'); const messageInput = document.getElementById('message-input'); const sendButton = document.getElementById('send-button'); const connectionStatus = document.getElementById('connection-status'); // Track if we're currently processing let isProcessing = false; // Get session ID - either from URL parameter, injected session name, or generate new one function getSessionId() { // Check if session name was injected by server (for /session/:name routes) if (window.GOOSE_SESSION_NAME) { return window.GOOSE_SESSION_NAME; } // Check URL parameters const urlParams = new URLSearchParams(window.location.search); const sessionParam = urlParams.get('session') || urlParams.get('name'); if (sessionParam) { return sessionParam; } // Generate new session ID using CLI format return generateSessionId(); } // Generate a session ID using timestamp format (yyyymmdd_hhmmss) like CLI function generateSessionId() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); const second = String(now.getSeconds()).padStart(2, '0'); return `${year}${month}${day}_${hour}${minute}${second}`; } // Format timestamp function formatTimestamp(date) { return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); } // Create message element function createMessageElement(content, role, timestamp) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}`; // Create content div const contentDiv = document.createElement('div'); contentDiv.className = 'message-content'; contentDiv.innerHTML = formatMessageContent(content); messageDiv.appendChild(contentDiv); // Add timestamp const timestampDiv = document.createElement('div'); timestampDiv.className = 'timestamp'; timestampDiv.textContent = formatTimestamp(new Date(timestamp || Date.now())); messageDiv.appendChild(timestampDiv); return messageDiv; } // Format message content (handle markdown-like formatting) function formatMessageContent(content) { // Escape HTML let formatted = content .replace(/&/g, '&') .replace(//g, '>'); // Handle code blocks formatted = formatted.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { return `
${code.trim()}
`; }); // Handle inline code formatted = formatted.replace(/`([^`]+)`/g, '$1'); // Handle line breaks formatted = formatted.replace(/\n/g, '
'); return formatted; } // Add message to chat function addMessage(content, role, timestamp) { // Remove welcome message if it exists const welcomeMessage = messagesContainer.querySelector('.welcome-message'); if (welcomeMessage) { welcomeMessage.remove(); } const messageElement = createMessageElement(content, role, timestamp); messagesContainer.appendChild(messageElement); // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Add thinking indicator function addThinkingIndicator() { removeThinkingIndicator(); // Remove any existing one first const thinkingDiv = document.createElement('div'); thinkingDiv.id = 'thinking-indicator'; thinkingDiv.className = 'message thinking-message'; thinkingDiv.innerHTML = `
Goose is thinking... `; messagesContainer.appendChild(thinkingDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Remove thinking indicator function removeThinkingIndicator() { const thinking = document.getElementById('thinking-indicator'); if (thinking) { thinking.remove(); } } // Connect to WebSocket function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; socket = new WebSocket(wsUrl); socket.onopen = () => { console.log('WebSocket connected'); isConnected = true; connectionStatus.textContent = 'Connected'; connectionStatus.className = 'status connected'; sendButton.disabled = false; // Check if this session exists and load history if it does loadSessionIfExists(); }; socket.onmessage = (event) => { try { const data = JSON.parse(event.data); handleServerMessage(data); } catch (e) { console.error('Failed to parse message:', e); } }; socket.onclose = () => { console.log('WebSocket disconnected'); isConnected = false; connectionStatus.textContent = 'Disconnected'; connectionStatus.className = 'status disconnected'; sendButton.disabled = true; // Attempt to reconnect after 3 seconds setTimeout(connectWebSocket, 3000); }; socket.onerror = (error) => { console.error('WebSocket error:', error); }; } // Handle messages from server function handleServerMessage(data) { switch (data.type) { case 'response': // For streaming responses, we need to handle partial messages handleStreamingResponse(data); break; case 'tool_request': handleToolRequest(data); break; case 'tool_response': handleToolResponse(data); break; case 'tool_confirmation': handleToolConfirmation(data); break; case 'thinking': handleThinking(data); break; case 'context_exceeded': handleContextExceeded(data); break; case 'cancelled': handleCancelled(data); break; case 'complete': handleComplete(data); break; case 'error': removeThinkingIndicator(); resetSendButton(); addMessage(`Error: ${data.message}`, 'assistant', Date.now()); break; default: console.log('Unknown message type:', data.type); } } // Track current streaming message let currentStreamingMessage = null; // Handle streaming responses function handleStreamingResponse(data) { removeThinkingIndicator(); // If this is the first chunk of a new message, or we don't have a current streaming message if (!currentStreamingMessage) { // Create a new message element const messageElement = createMessageElement(data.content, data.role || 'assistant', data.timestamp); messageElement.setAttribute('data-streaming', 'true'); messagesContainer.appendChild(messageElement); currentStreamingMessage = { element: messageElement, content: data.content, role: data.role || 'assistant', timestamp: data.timestamp }; } else { // Append to existing streaming message currentStreamingMessage.content += data.content; // Update the message content using the proper content div const contentDiv = currentStreamingMessage.element.querySelector('.message-content'); if (contentDiv) { contentDiv.innerHTML = formatMessageContent(currentStreamingMessage.content); } } // Scroll to bottom messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Handle tool requests function handleToolRequest(data) { removeThinkingIndicator(); // Remove thinking when tool starts // Reset streaming message so tool doesn't interfere with message flow currentStreamingMessage = null; const toolDiv = document.createElement('div'); toolDiv.className = 'message assistant tool-message'; const headerDiv = document.createElement('div'); headerDiv.className = 'tool-header'; headerDiv.innerHTML = `🔧 ${data.tool_name}`; const contentDiv = document.createElement('div'); contentDiv.className = 'tool-content'; // Format the arguments if (data.tool_name === 'developer__shell' && data.arguments.command) { contentDiv.innerHTML = `
${escapeHtml(data.arguments.command)}
`; } else if (data.tool_name === 'developer__text_editor') { const action = data.arguments.command || 'unknown'; const path = data.arguments.path || 'unknown'; contentDiv.innerHTML = `
action: ${action}
`; contentDiv.innerHTML += `
path: ${escapeHtml(path)}
`; if (data.arguments.file_text) { contentDiv.innerHTML += `
content:
${escapeHtml(data.arguments.file_text.substring(0, 200))}${data.arguments.file_text.length > 200 ? '...' : ''}
`; } } else { contentDiv.innerHTML = `
${JSON.stringify(data.arguments, null, 2)}
`; } toolDiv.appendChild(headerDiv); toolDiv.appendChild(contentDiv); // Add a "running" indicator const runningDiv = document.createElement('div'); runningDiv.className = 'tool-running'; runningDiv.innerHTML = '⏳ Running...'; toolDiv.appendChild(runningDiv); messagesContainer.appendChild(toolDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Handle tool responses function handleToolResponse(data) { // Remove the "running" indicator from the last tool message const toolMessages = messagesContainer.querySelectorAll('.tool-message'); if (toolMessages.length > 0) { const lastToolMessage = toolMessages[toolMessages.length - 1]; const runningIndicator = lastToolMessage.querySelector('.tool-running'); if (runningIndicator) { runningIndicator.remove(); } } if (data.is_error) { const errorDiv = document.createElement('div'); errorDiv.className = 'message tool-error'; errorDiv.innerHTML = `Tool Error: ${escapeHtml(data.result.error || 'Unknown error')}`; messagesContainer.appendChild(errorDiv); } else { // Handle successful tool response if (Array.isArray(data.result)) { data.result.forEach(content => { if (content.type === 'text' && content.text) { const responseDiv = document.createElement('div'); responseDiv.className = 'message tool-result'; responseDiv.innerHTML = `
${escapeHtml(content.text)}
`; messagesContainer.appendChild(responseDiv); } }); } } messagesContainer.scrollTop = messagesContainer.scrollHeight; // Reset streaming message so next assistant response creates a new message currentStreamingMessage = null; // Show thinking indicator because assistant will likely follow up with explanation // Only show if we're still processing (cancel button is active) if (isProcessing) { addThinkingIndicator(); } } // Handle tool confirmations function handleToolConfirmation(data) { const confirmDiv = document.createElement('div'); confirmDiv.className = 'message tool-confirmation'; confirmDiv.innerHTML = `
⚠️ Tool Confirmation Required
${data.tool_name} wants to execute with:
${JSON.stringify(data.arguments, null, 2)}
Auto-approved in web mode (UI coming soon)
`; messagesContainer.appendChild(confirmDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Handle thinking messages function handleThinking(data) { // For now, just log thinking messages console.log('Thinking:', data.message); } // Handle context exceeded function handleContextExceeded(data) { const contextDiv = document.createElement('div'); contextDiv.className = 'message context-warning'; contextDiv.innerHTML = `
⚠️ Context Length Exceeded
${escapeHtml(data.message)}
Auto-summarizing conversation...
`; messagesContainer.appendChild(contextDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Handle cancelled operation function handleCancelled(data) { removeThinkingIndicator(); resetSendButton(); const cancelDiv = document.createElement('div'); cancelDiv.className = 'message system-message cancelled'; cancelDiv.innerHTML = `${escapeHtml(data.message)}`; messagesContainer.appendChild(cancelDiv); messagesContainer.scrollTop = messagesContainer.scrollHeight; } // Handle completion of response function handleComplete(data) { removeThinkingIndicator(); resetSendButton(); // Finalize any streaming message if (currentStreamingMessage) { currentStreamingMessage = null; } } // Reset send button to normal state function resetSendButton() { isProcessing = false; sendButton.textContent = 'Send'; sendButton.classList.remove('cancel-mode'); } // Escape HTML to prevent XSS function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // Send message or cancel function sendMessage() { if (isProcessing) { // Cancel the current operation socket.send(JSON.stringify({ type: 'cancel', session_id: sessionId })); return; } const message = messageInput.value.trim(); if (!message || !isConnected) return; // Add user message to chat addMessage(message, 'user', Date.now()); // Clear input messageInput.value = ''; messageInput.style.height = 'auto'; // Add thinking indicator addThinkingIndicator(); // Update button to show cancel isProcessing = true; sendButton.textContent = 'Cancel'; sendButton.classList.add('cancel-mode'); // Send message through WebSocket socket.send(JSON.stringify({ type: 'message', content: message, session_id: sessionId, timestamp: Date.now() })); } // Handle suggestion pill clicks function sendSuggestion(text) { if (!isConnected || isProcessing) return; messageInput.value = text; sendMessage(); } // Load session history if the session exists (like --resume in CLI) async function loadSessionIfExists() { try { const response = await fetch(`/api/sessions/${sessionId}`); if (response.ok) { const sessionData = await response.json(); if (sessionData.messages && sessionData.messages.length > 0) { // Remove welcome message since we're resuming const welcomeMessage = messagesContainer.querySelector('.welcome-message'); if (welcomeMessage) { welcomeMessage.remove(); } // Display session resumed message const resumeDiv = document.createElement('div'); resumeDiv.className = 'message system-message'; resumeDiv.innerHTML = `Session resumed: ${sessionData.messages.length} messages loaded`; messagesContainer.appendChild(resumeDiv); // Update page title with session description if available if (sessionData.metadata && sessionData.metadata.description) { document.title = `Goose Chat - ${sessionData.metadata.description}`; } messagesContainer.scrollTop = messagesContainer.scrollHeight; } } } catch (error) { console.log('No existing session found or error loading:', error); // This is fine - just means it's a new session } } // Event listeners sendButton.addEventListener('click', sendMessage); messageInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // Auto-resize textarea messageInput.addEventListener('input', () => { messageInput.style.height = 'auto'; messageInput.style.height = messageInput.scrollHeight + 'px'; }); // Initialize WebSocket connection connectWebSocket(); // Focus on input messageInput.focus(); // Update session title function updateSessionTitle() { const titleElement = document.getElementById('session-title'); // Just show "Goose Chat" - no need to show session ID titleElement.textContent = 'Goose Chat'; } // Update title on load updateSessionTitle();