// --------------------------------------------------------------------------- // Agent Bridge -- Frontend Application (v2.1) // --------------------------------------------------------------------------- // Handles WebSocket, settings panel, honest CLI status, real chat relay, // tool rail, suggestion chips, and output rendering. // --------------------------------------------------------------------------- (function () { 'use strict'; // ----------------------------------------------------------------------- // DOM // ----------------------------------------------------------------------- const $ = (id) => document.getElementById(id); const $toolRail = $('tool-rail'); const $centerCard = $('center-card'); const $greeting = $('greeting'); const $promptInput = $('prompt-input'); const $sendBtn = $('send-btn'); const $iconBar = $('icon-bar'); const $chips = $('suggestion-chips'); const $connInd = $('connection-indicator'); const $statusText = $('status-text'); const $cardClose = $('card-close'); const $settingsToggle = $('settings-toggle'); // Settings panel const $settingsPanel = $('settings-panel'); const $settingsClose = $('settings-close'); const $cliPathInput = $('cli-path-input'); const $savePathBtn = $('save-path-btn'); const $detectIndicator = $('detect-indicator'); const $detectText = $('detect-text'); const $detectDetails = $('detect-details'); const $redetectBtn = $('redetect-btn'); const $connectBtn = $('connect-btn'); const $disconnectBtn = $('disconnect-btn'); const $sessionStatus = $('session-status'); // Conversation const $conversation = $('conversation'); const $messages = $('messages'); const $convInput = $('conv-input'); const $convSendBtn = $('conv-send-btn'); const $outputArea = $('output-area'); const $outputContent = $('output-content'); const $closeOutput = $('close-output'); const $linkTools = $('link-tools'); const $linkBridge = $('link-bridge'); // ----------------------------------------------------------------------- // State // ----------------------------------------------------------------------- let socket = null; let tools = []; let msgCount = 0; let inConversation = false; let cliInfo = { detected: false, version: null, path: null, method: null }; let sessionActive = false; let savedSettings = { cliPath: '', autoConnect: false }; // ----------------------------------------------------------------------- // Connect // ----------------------------------------------------------------------- function connect() { socket = io({ transports: ['websocket', 'polling'] }); socket.on('connect', () => { $connInd.classList.add('connected'); updateStatusText(); }); socket.on('disconnect', () => { $connInd.classList.remove('connected'); $statusText.textContent = 'Disconnected'; }); socket.on('connect_error', () => { $connInd.classList.remove('connected'); $statusText.textContent = 'Connection failed'; }); // Initial state from server socket.on('bridge:init', (data) => { tools = data.tools || []; cliInfo = data.cli || {}; savedSettings = data.settings || {}; sessionActive = data.sessionActive || false; renderToolRail(tools); renderIconBar(tools); renderChips(tools); updateStatusText(); updateSettingsPanel(); }); // Chat messages socket.on('agent:message', (data) => { if (data.from === 'system') { addMsg('system', '', data.message); } else if (data.from !== 'human') { addMsg('agent', 'Agent', data.message); } }); // Tool events socket.on('tool:started', (data) => { addMsg('tool', data.toolName, 'Running...'); showOutput(); appendOutput(`[${data.toolName}] Started\n`); }); socket.on('tool:progress', (data) => { appendProgressLine(data.progress); }); socket.on('tool:completed', (data) => { addMsg('tool', data.toolName, 'Completed.'); renderToolResult(data.result); }); socket.on('tool:error', (data) => { addMsg('error', data.toolName, data.error); appendOutput(`[Error] ${data.error}\n`); }); // CLI stdout/stderr -- these come from the real Antigravity process socket.on('cli:stdout', (data) => { showOutput(); appendOutput(data.data); // Also show in chat as agent messages (strip ANSI codes) const clean = stripAnsi(data.data).trim(); if (clean) { addMsg('agent', 'Antigravity', clean); } }); socket.on('cli:stderr', (data) => { showOutput(); appendOutput(`${data.data}`); }); socket.on('cli:started', () => { sessionActive = true; addMsg('system', '', 'Antigravity CLI session started.'); updateStatusText(); updateSessionButtons(); }); socket.on('cli:starting', () => { addMsg('system', '', 'Starting Antigravity CLI...'); }); socket.on('cli:ended', (data) => { sessionActive = false; addMsg('system', '', `Antigravity CLI session ended${data.exitCode !== null ? ` (exit code ${data.exitCode})` : ''}.`); updateStatusText(); updateSessionButtons(); }); socket.on('cli:error', (data) => { addMsg('error', 'CLI', data.message); sessionActive = false; updateStatusText(); updateSessionButtons(); }); // Settings changes from other clients socket.on('settings:changed', (data) => { savedSettings = data.settings; cliInfo = data.cli; updateSettingsPanel(); updateStatusText(); }); } // ----------------------------------------------------------------------- // Status // ----------------------------------------------------------------------- function updateStatusText() { const parts = []; if (socket?.connected) { parts.push('Server: connected'); } else { parts.push('Server: disconnected'); } if (sessionActive) { parts.push('CLI: active session'); } else if (cliInfo.detected) { parts.push('CLI: detected (not running)'); } else { parts.push('CLI: not detected'); } $statusText.textContent = parts.join(' | '); } // ----------------------------------------------------------------------- // Settings Panel // ----------------------------------------------------------------------- function openSettings() { $settingsPanel.classList.remove('hidden'); $cliPathInput.value = savedSettings.cliPath || ''; updateSettingsPanel(); } function closeSettings() { $settingsPanel.classList.add('hidden'); } function updateSettingsPanel() { // Detection status if (cliInfo.detected) { $detectIndicator.className = 'detect-dot found'; $detectText.textContent = `Detected: ${cliInfo.path || 'unknown'}`; $detectDetails.textContent = `Version: ${cliInfo.version || 'unknown'}\nMethod: ${cliInfo.method || 'unknown'}`; } else { $detectIndicator.className = 'detect-dot missing'; $detectText.textContent = 'Not detected'; $detectDetails.textContent = 'Configure the path above and click Save, or install Antigravity CLI and click Re-detect.'; } // Path input if (savedSettings.cliPath && !$cliPathInput.value) { $cliPathInput.value = savedSettings.cliPath; } updateSessionButtons(); } function updateSessionButtons() { $connectBtn.disabled = sessionActive; $disconnectBtn.disabled = !sessionActive; $sessionStatus.textContent = sessionActive ? 'CLI session is active. Messages will be sent to the Antigravity agent.' : 'No active session. Click Connect to start.'; } // Save CLI path $savePathBtn.addEventListener('click', () => { const path = $cliPathInput.value.trim(); socket.emit('settings:update', { cliPath: path }, (response) => { savedSettings = response.settings; cliInfo = response.cli; updateSettingsPanel(); updateStatusText(); addMsg('system', '', cliInfo.detected ? `CLI detected at: ${cliInfo.path}` : `CLI not found at the specified path. Check the path and try again.`); }); }); // Re-detect $redetectBtn.addEventListener('click', () => { $detectText.textContent = 'Detecting...'; socket.emit('settings:update', { cliPath: $cliPathInput.value.trim() }, (response) => { savedSettings = response.settings; cliInfo = response.cli; updateSettingsPanel(); updateStatusText(); }); }); // Connect CLI session $connectBtn.addEventListener('click', () => { $connectBtn.disabled = true; $sessionStatus.textContent = 'Starting...'; socket.emit('cli:start', { cliPath: savedSettings.cliPath || cliInfo.path }); }); // Disconnect CLI session $disconnectBtn.addEventListener('click', () => { socket.emit('cli:stop'); }); $settingsToggle.addEventListener('click', openSettings); $settingsClose.addEventListener('click', closeSettings); // Close settings on backdrop click $settingsPanel.addEventListener('click', (e) => { if (e.target === $settingsPanel) closeSettings(); }); // ----------------------------------------------------------------------- // Tool Rail // ----------------------------------------------------------------------- function renderToolRail(toolList) { $toolRail.innerHTML = ''; toolList.forEach((tool, i) => { const btn = document.createElement('button'); btn.className = 'rail-btn' + (tool.mock ? '' : ' active'); btn.textContent = String(i + 1); btn.title = tool.name.replace(/_/g, ' '); btn.addEventListener('click', () => insertSyntax(tool.syntax || `use <${tool.name}> `)); $toolRail.appendChild(btn); }); } // ----------------------------------------------------------------------- // Icon Bar // ----------------------------------------------------------------------- function renderIconBar(toolList) { $iconBar.innerHTML = ''; addIconButton($iconBar, iconSvg('globe'), 'Browse tools', () => openSettings()); toolList.slice(0, 6).forEach((tool) => { addIconButton($iconBar, tool.mock ? iconSvg('box') : iconSvg('zap'), tool.name.replace(/_/g, ' '), () => insertSyntax(tool.syntax || `use <${tool.name}> `) ); }); } function addIconButton(parent, svgHtml, title, onClick) { const btn = document.createElement('button'); btn.className = 'icon-btn'; btn.innerHTML = svgHtml; btn.title = title; btn.addEventListener('click', onClick); parent.appendChild(btn); } // ----------------------------------------------------------------------- // Chips // ----------------------------------------------------------------------- function renderChips(toolList) { $chips.innerHTML = ''; const suggestions = [ { label: 'Transcribe YouTube', syntax: 'use ' }, ...toolList.filter(t => t.mock).slice(0, 3).map(t => ({ label: t.name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()), syntax: t.syntax, })), ]; suggestions.forEach((s) => { const chip = document.createElement('button'); chip.className = 'chip'; chip.textContent = s.label; chip.addEventListener('click', () => insertSyntax(s.syntax)); $chips.appendChild(chip); }); } // ----------------------------------------------------------------------- // Input helpers // ----------------------------------------------------------------------- function insertSyntax(syntax) { const input = inConversation ? $convInput : $promptInput; input.value = syntax; input.focus(); input.setSelectionRange(input.value.length, input.value.length); } function send(inputEl) { const text = inputEl.value.trim(); if (!text) return; const id = `msg-${++msgCount}-${Date.now()}`; if (!inConversation) enterConversation(); addMsg('user', 'You', text); socket.emit('prompt:send', { message: text, id }); inputEl.value = ''; inputEl.focus(); } // ----------------------------------------------------------------------- // View Switching // ----------------------------------------------------------------------- function enterConversation() { inConversation = true; $centerCard.style.display = 'none'; $conversation.classList.remove('hidden'); $convInput.focus(); } // ----------------------------------------------------------------------- // Messages // ----------------------------------------------------------------------- function addMsg(type, label, body) { if (!inConversation) enterConversation(); const div = document.createElement('div'); if (type === 'system') { div.className = 'msg msg-system'; div.textContent = body; } else { div.className = 'msg msg-' + type; const labelEl = document.createElement('div'); labelEl.className = 'msg-label'; labelEl.textContent = label; div.appendChild(labelEl); const bodyEl = document.createElement('div'); bodyEl.className = 'msg-body'; bodyEl.textContent = body; div.appendChild(bodyEl); } $messages.appendChild(div); $messages.scrollTop = $messages.scrollHeight; } // ----------------------------------------------------------------------- // Output // ----------------------------------------------------------------------- function showOutput() { $outputArea.classList.remove('hidden'); } function appendOutput(text) { const span = document.createElement('span'); span.textContent = text; $outputContent.appendChild(span); $outputContent.scrollTop = $outputContent.scrollHeight; } function appendProgressLine(text) { showOutput(); const line = document.createElement('div'); line.className = 'progress-line'; line.textContent = text; $outputContent.appendChild(line); $outputContent.scrollTop = $outputContent.scrollHeight; } function renderToolResult(result) { if (!result) return; if (result.transcript) { const preview = document.createElement('div'); preview.className = 'transcript-preview'; preview.textContent = result.transcript.length > 1500 ? result.transcript.substring(0, 1500) + '\n\n[truncated]' : result.transcript; $outputContent.appendChild(preview); if (result.downloadUrl) { const link = document.createElement('a'); link.className = 'download-link'; link.href = result.downloadUrl; link.download = result.filename || 'transcript.txt'; link.textContent = 'Download Transcript'; $outputContent.appendChild(link); } } else if (result.message) { appendOutput('\n' + result.message + '\n'); } $outputContent.scrollTop = $outputContent.scrollHeight; } // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- function stripAnsi(str) { return str.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1B\][^\x07]*\x07/g, ''); } function iconSvg(name) { const icons = { globe: '', zap: '', box: '', }; return icons[name] || ''; } // ----------------------------------------------------------------------- // Events // ----------------------------------------------------------------------- $promptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send($promptInput); } }); $sendBtn.addEventListener('click', () => send($promptInput)); $convInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); send($convInput); } }); $convSendBtn.addEventListener('click', () => send($convInput)); $cardClose.addEventListener('click', () => { $centerCard.style.display = 'none'; }); $closeOutput.addEventListener('click', () => { $outputArea.classList.add('hidden'); }); $linkTools.addEventListener('click', (e) => { e.preventDefault(); showOutput(); $outputContent.innerHTML = ''; appendOutput('Registered Tools:\n'); tools.forEach((t, i) => { appendOutput(` ${i + 1}. ${t.name} ${t.mock ? '[mock]' : '[live]'}\n`); appendOutput(` ${t.description}\n`); appendOutput(` Syntax: ${t.syntax}\n\n`); }); }); $linkBridge.addEventListener('click', (e) => { e.preventDefault(); openSettings(); }); // ----------------------------------------------------------------------- // Init // ----------------------------------------------------------------------- connect(); })();