From 9500bbe519bdb4dec44b7c71e3ba747d9b57f6f1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 21:33:10 +0000 Subject: [PATCH 1/4] feat: add bridge mode navigation and TypeScript infrastructure - Add "Open Dashboard" navigation for projects in bridge command palette - Add project dashboard route (/project/:id) with context detection - Add dashboard buttons to project cards and sidebar for quick navigation - Add "Back to Bridge" link in project dashboard view - Add "Message Lead" button on project cards - Add "Go to Bridge" command in main dashboard command palette - Create TypeScript infrastructure for bridge frontend (types, state, app) - Update build scripts to compile both dashboard and bridge frontend --- package.json | 4 +- src/dashboard/frontend/app.ts | 74 ++- src/dashboard/frontend/bridge/app.ts | 649 +++++++++++++++++++++++++ src/dashboard/frontend/bridge/state.ts | 140 ++++++ src/dashboard/frontend/bridge/types.ts | 70 +++ src/dashboard/public/bridge.html | 202 +++++++- src/dashboard/public/index.html | 43 +- src/dashboard/public/js/app.js | 86 ++-- src/dashboard/public/js/app.js.map | 6 +- src/dashboard/public/js/bridge.js | 117 +++++ src/dashboard/public/js/bridge.js.map | 7 + src/dashboard/server.ts | 51 ++ 12 files changed, 1389 insertions(+), 60 deletions(-) create mode 100644 src/dashboard/frontend/bridge/app.ts create mode 100644 src/dashboard/frontend/bridge/state.ts create mode 100644 src/dashboard/frontend/bridge/types.ts create mode 100644 src/dashboard/public/js/bridge.js create mode 100644 src/dashboard/public/js/bridge.js.map diff --git a/package.json b/package.json index 58fc328e9..0f0fea223 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,9 @@ "scripts": { "postinstall": "npm rebuild better-sqlite3", "build": "npm run clean && tsc && npm run build:frontend", - "build:frontend": "esbuild src/dashboard/frontend/app.ts --bundle --outfile=src/dashboard/public/js/app.js --format=esm --target=es2022 --minify --sourcemap", + "build:frontend": "npm run build:frontend:dashboard && npm run build:frontend:bridge", + "build:frontend:dashboard": "esbuild src/dashboard/frontend/app.ts --bundle --outfile=src/dashboard/public/js/app.js --format=esm --target=es2022 --minify --sourcemap", + "build:frontend:bridge": "esbuild src/dashboard/frontend/bridge/app.ts --bundle --outfile=src/dashboard/public/js/bridge.js --format=esm --target=es2022 --minify --sourcemap", "postbuild": "cp -r src/dashboard/public dist/dashboard/ && chmod +x dist/cli/index.js", "dev": "tsc -w", "dev:local": "npm run build && npm link && echo '✓ agent-relay linked globally'", diff --git a/src/dashboard/frontend/app.ts b/src/dashboard/frontend/app.ts index 01c7943d6..e434a9f9c 100644 --- a/src/dashboard/frontend/app.ts +++ b/src/dashboard/frontend/app.ts @@ -28,12 +28,73 @@ import { } from './components.js'; import { state } from './state.js'; +/** + * Detect if we're viewing a project dashboard from bridge context + */ +function detectProjectContext(): { projectId: string | null; fromBridge: boolean } { + const pathname = window.location.pathname; + const match = pathname.match(/^\/project\/([^/]+)$/); + + if (match) { + return { projectId: decodeURIComponent(match[1]), fromBridge: true }; + } + + return { projectId: null, fromBridge: false }; +} + +/** + * Update the UI for project context (when accessed from bridge) + */ +async function setupProjectContext(projectId: string): Promise { + // Update workspace name to show project + const workspaceName = document.querySelector('.workspace-name'); + if (workspaceName) { + // Fetch project info + try { + const response = await fetch(`/api/project/${encodeURIComponent(projectId)}`); + if (response.ok) { + const project = await response.json(); + const nameSpan = workspaceName.querySelector(':not(.status-dot)'); + if (nameSpan && nameSpan.nodeType === Node.TEXT_NODE) { + nameSpan.textContent = project.name || projectId; + } else { + // Replace text content after status-dot + const textNodes = Array.from(workspaceName.childNodes).filter(n => n.nodeType === Node.TEXT_NODE); + textNodes.forEach(n => n.textContent = ''); + workspaceName.appendChild(document.createTextNode(' ' + (project.name || projectId))); + } + } + } catch { + // Fallback - just show project ID + } + } + + // Update bridge nav link to show "Back to Bridge" with back arrow + const bridgeLinkText = document.getElementById('bridge-link-text'); + const bridgeNavLink = document.getElementById('bridge-nav-link'); + if (bridgeLinkText) { + bridgeLinkText.textContent = '← Back to Bridge'; + } + if (bridgeNavLink) { + bridgeNavLink.classList.add('back-to-bridge'); + } + + // Add a subtle indicator that we're in project view + document.body.classList.add('project-view'); +} + /** * Initialize the dashboard application */ export function initApp(): void { const elements = initElements(); + // Check if we're in project context (from bridge) + const { projectId, fromBridge } = detectProjectContext(); + if (fromBridge && projectId) { + setupProjectContext(projectId); + } + // Subscribe to state changes subscribe(() => { updateConnectionStatus(); @@ -196,7 +257,10 @@ function setupEventListeners(elements: ReturnType): void { item.addEventListener('click', () => { const command = item.dataset.command; - if (command === 'broadcast') { + if (command === 'bridge') { + // Navigate to bridge view + window.location.href = '/bridge'; + } else if (command === 'broadcast') { // Pre-fill message input with @* for broadcast elements.messageInput.value = '@* '; elements.messageInput.focus(); @@ -208,6 +272,14 @@ function setupEventListeners(elements: ReturnType): void { }); }); + // Add Cmd/Ctrl+B shortcut for bridge navigation + document.addEventListener('keydown', (e: KeyboardEvent) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'b') { + e.preventDefault(); + window.location.href = '/bridge'; + } + }); + // Initialize palette channel click handlers initPaletteChannels(); diff --git a/src/dashboard/frontend/bridge/app.ts b/src/dashboard/frontend/bridge/app.ts new file mode 100644 index 000000000..9a52c3ab0 --- /dev/null +++ b/src/dashboard/frontend/bridge/app.ts @@ -0,0 +1,649 @@ +/** + * Bridge Dashboard Application Entry Point + */ + +import { subscribe, state, setProjects, setMessages, setConnected, setWebSocket, setSelectedProject, getUptimeString, getConnectedProjects, getAllAgents, getProject } from './state.js'; +import type { BridgeDOMElements, Project, BridgeMessage } from './types.js'; +import { escapeHtml, formatTime, getAvatarColor, getInitials } from '../utils.js'; + +let elements: BridgeDOMElements; + +/** + * Initialize DOM element references + */ +function initElements(): BridgeDOMElements { + return { + statusDot: document.getElementById('status-dot')!, + projectList: document.getElementById('project-list')!, + cardsGrid: document.getElementById('cards-grid')!, + emptyState: document.getElementById('empty-state')!, + messagesList: document.getElementById('messages-list')!, + searchBar: document.getElementById('search-bar')!, + paletteOverlay: document.getElementById('command-palette-overlay')!, + paletteSearch: document.getElementById('palette-search') as HTMLInputElement, + paletteResults: document.getElementById('palette-results')!, + paletteProjectsSection: document.getElementById('palette-projects-section')!, + paletteAgentsSection: document.getElementById('palette-agents-section')!, + channelName: document.getElementById('channel-name')!, + statAgents: document.getElementById('stat-agents')!, + statMessages: document.getElementById('stat-messages')!, + composerProject: document.getElementById('composer-project') as HTMLSelectElement, + composerAgent: document.getElementById('composer-agent') as HTMLSelectElement, + composerMessage: document.getElementById('composer-message') as HTMLInputElement, + composerSend: document.getElementById('composer-send') as HTMLButtonElement, + composerStatus: document.getElementById('composer-status')!, + uptime: document.getElementById('uptime')!, + }; +} + +/** + * Update connection status indicator + */ +function updateConnectionStatus(): void { + elements.statusDot.classList.toggle('offline', !state.isConnected); +} + +/** + * Render sidebar projects list + */ +function renderSidebarProjects(): void { + const { projects, selectedProjectId } = state; + + if (!projects || projects.length === 0) { + elements.projectList.innerHTML = '
  • No projects
  • '; + document.getElementById('project-count')!.textContent = '0'; + return; + } + + document.getElementById('project-count')!.textContent = String(projects.length); + + elements.projectList.innerHTML = projects.map((p) => ` +
  • + + ${escapeHtml(p.name || p.id)} + +
  • + `).join(''); +} + +/** + * Render project cards grid + */ +function renderProjectCards(): void { + const { projects, selectedProjectId } = state; + + if (!projects || projects.length === 0) { + elements.cardsGrid.innerHTML = ''; + elements.cardsGrid.appendChild(elements.emptyState); + elements.emptyState.style.display = 'flex'; + return; + } + + elements.emptyState.style.display = 'none'; + + elements.cardsGrid.innerHTML = projects.map((p) => { + const agents = p.agents || []; + const agentsHtml = agents.length > 0 + ? agents.map((a) => ` +
    + + ${escapeHtml(a.name)} + ${escapeHtml(a.cli || '')} +
    + `).join('') + : '
    No agents connected
    '; + + const isSelected = selectedProjectId === p.id; + return ` +
    +
    +
    +
    + + + +
    +
    +
    ${escapeHtml(p.name || p.id)}
    +
    ${escapeHtml(p.path || '')}
    +
    +
    +
    + + ${p.connected ? 'Online' : p.reconnecting ? 'Reconnecting...' : 'Offline'} +
    +
    + +
    +
    + Agents + ${agents.length} active +
    +
    + ${agentsHtml} +
    +
    + +
    + + +
    +
    + `; + }).join(''); +} + +/** + * Render messages list + */ +function renderMessages(): void { + const { messages } = state; + + if (!messages || messages.length === 0) { + elements.messagesList.innerHTML = '

    No messages yet

    '; + return; + } + + elements.messagesList.innerHTML = messages.slice(-50).reverse().map((m) => ` +
    +
    + ${escapeHtml(m.sourceProject || 'local')} + ${escapeHtml(m.from)} + + ${escapeHtml(m.to || '*')} + ${formatTime(m.timestamp)} +
    +
    ${escapeHtml(m.body || m.content || '')}
    +
    + `).join(''); +} + +/** + * Update stats display + */ +function updateStats(): void { + const allAgents = getAllAgents(); + elements.statAgents.textContent = String(allAgents.length); + elements.statMessages.textContent = String(state.messages.length); +} + +/** + * Update composer project options + */ +function updateComposerProjects(): void { + const connectedProjects = getConnectedProjects(); + const currentValue = elements.composerProject.value; + + elements.composerProject.innerHTML = '' + + connectedProjects.map((p) => + `` + ).join(''); + + // Restore selection if still valid + if (currentValue && connectedProjects.some((p) => p.id === currentValue)) { + elements.composerProject.value = currentValue; + } else if (state.selectedProjectId && connectedProjects.some((p) => p.id === state.selectedProjectId)) { + elements.composerProject.value = state.selectedProjectId; + updateComposerAgents(); + } +} + +/** + * Update composer agent options + */ +function updateComposerAgents(): void { + const projectId = elements.composerProject.value; + if (!projectId) { + elements.composerAgent.innerHTML = ''; + elements.composerAgent.disabled = true; + elements.composerMessage.disabled = true; + elements.composerSend.disabled = true; + return; + } + + const currentAgent = elements.composerAgent.value; + const project = getProject(projectId); + const agents = project?.agents || []; + + elements.composerAgent.innerHTML = '' + + '' + + '' + + agents.map((a) => + `` + ).join(''); + + elements.composerAgent.disabled = false; + + // Restore agent selection if still valid + if (currentAgent) { + const validAgents = ['*', 'lead', ...agents.map((a) => a.name)]; + if (validAgents.includes(currentAgent)) { + elements.composerAgent.value = currentAgent; + } + } +} + +/** + * Update composer state based on selections + */ +function updateComposerState(): void { + const hasProject = !!elements.composerProject.value; + const hasAgent = !!elements.composerAgent.value; + const hasMessage = elements.composerMessage.value.trim().length > 0; + + elements.composerMessage.disabled = !hasProject || !hasAgent; + elements.composerSend.disabled = !hasProject || !hasAgent || !hasMessage; +} + +/** + * Send message via bridge API + */ +async function sendBridgeMessage(): Promise { + const projectId = elements.composerProject.value; + const to = elements.composerAgent.value; + const message = elements.composerMessage.value.trim(); + + if (!projectId || !to || !message) return; + + elements.composerSend.disabled = true; + elements.composerStatus.textContent = 'Sending...'; + elements.composerStatus.className = 'composer-status'; + + try { + const response = await fetch('/api/bridge/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ projectId, to, message }), + }); + + const result = await response.json(); + + if (response.ok && result.success) { + elements.composerStatus.textContent = 'Message sent!'; + elements.composerStatus.className = 'composer-status success'; + elements.composerMessage.value = ''; + setTimeout(() => { + elements.composerStatus.textContent = ''; + elements.composerStatus.className = 'composer-status'; + }, 2000); + } else { + throw new Error(result.error || 'Failed to send'); + } + } catch (err) { + elements.composerStatus.textContent = (err as Error).message || 'Failed to send message'; + elements.composerStatus.className = 'composer-status error'; + } + + updateComposerState(); +} + +/** + * Update header for project selection + */ +function updateHeader(): void { + const { selectedProjectId } = state; + + if (selectedProjectId) { + const project = getProject(selectedProjectId); + if (project) { + elements.channelName.innerHTML = ` + ← All Projects + ${escapeHtml(project.name || project.id)} + `; + } + } else { + elements.channelName.textContent = 'All Projects'; + } +} + +/** + * Select a project + */ +function selectProject(projectId: string | null): void { + setSelectedProject(projectId); + + if (projectId) { + elements.composerProject.value = projectId; + updateComposerAgents(); + updateComposerState(); + } + + // Update card selection visually + document.querySelectorAll('.project-card').forEach((card) => { + card.classList.toggle('selected', (card as HTMLElement).dataset.projectId === projectId); + }); +} + +/** + * Open command palette + */ +function openPalette(): void { + elements.paletteOverlay.classList.add('visible'); + elements.paletteSearch.value = ''; + elements.paletteSearch.focus(); + updatePaletteResults(); +} + +/** + * Close command palette + */ +function closePalette(): void { + elements.paletteOverlay.classList.remove('visible'); +} + +/** + * Update palette search results + */ +function updatePaletteResults(): void { + const query = elements.paletteSearch.value.toLowerCase(); + const { projects } = state; + + // Update projects section + const filteredProjects = query + ? projects.filter((p) => (p.name || p.id).toLowerCase().includes(query)) + : projects; + + if (filteredProjects.length > 0) { + elements.paletteProjectsSection.innerHTML = ` +
    Open Project Dashboard
    + ${filteredProjects.map((p) => ` +
    +
    + + + + + +
    +
    +
    ${escapeHtml(p.name || p.id)}
    +
    ${p.connected ? 'Online' : 'Offline'} · ${(p.agents || []).length} agents · Click to open dashboard
    +
    +
    + +
    +
    + `).join('')} + `; + } else { + elements.paletteProjectsSection.innerHTML = '
    Open Project Dashboard
    '; + } + + // Update agents section + const allAgents = getAllAgents(); + const filteredAgents = query + ? allAgents.filter((a) => a.name.toLowerCase().includes(query)) + : allAgents; + + if (filteredAgents.length > 0) { + elements.paletteAgentsSection.innerHTML = ` +
    Message Agent
    + ${filteredAgents.map((a) => ` +
    +
    + + + + +
    +
    +
    ${escapeHtml(a.name)}
    +
    ${escapeHtml(a.projectName)} · ${escapeHtml(a.cli || 'unknown')}
    +
    +
    + `).join('')} + `; + } else { + elements.paletteAgentsSection.innerHTML = '
    Message Agent
    '; + } +} + +/** + * Set up event listeners + */ +function setupEventListeners(): void { + // Search bar opens palette + elements.searchBar.addEventListener('click', openPalette); + + // Palette overlay click closes + elements.paletteOverlay.addEventListener('click', (e) => { + if (e.target === elements.paletteOverlay) closePalette(); + }); + + // Palette search filtering + elements.paletteSearch.addEventListener('input', updatePaletteResults); + + // Keyboard shortcuts + document.addEventListener('keydown', (e) => { + // Cmd/Ctrl + K to open palette + if ((e.metaKey || e.ctrlKey) && e.key === 'k') { + e.preventDefault(); + if (elements.paletteOverlay.classList.contains('visible')) { + closePalette(); + } else { + openPalette(); + } + } + // Escape to close + if (e.key === 'Escape' && elements.paletteOverlay.classList.contains('visible')) { + closePalette(); + } + }); + + // Palette item clicks + elements.paletteResults.addEventListener('click', (e) => { + const item = (e.target as HTMLElement).closest('.palette-item') as HTMLElement | null; + if (!item) return; + + const command = item.dataset.command; + const projectId = item.dataset.project; + const agentName = item.dataset.agent; + const action = item.dataset.action; + + if (command === 'broadcast') { + closePalette(); + elements.composerMessage.focus(); + elements.composerStatus.textContent = 'Select a project and agent to send a message'; + } else if (command === 'refresh') { + closePalette(); + location.reload(); + } else if (command === 'go-dashboard') { + closePalette(); + window.location.href = '/'; + } else if (action === 'open-dashboard' && projectId) { + closePalette(); + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } else if (agentName && projectId) { + closePalette(); + elements.composerProject.value = projectId; + updateComposerAgents(); + setTimeout(() => { + elements.composerAgent.value = agentName; + updateComposerState(); + elements.composerMessage.focus(); + }, 50); + } else if (projectId) { + closePalette(); + selectProject(projectId); + const card = document.querySelector(`.project-card[data-project-id="${projectId}"]`); + if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }); + + // Project card clicks + elements.cardsGrid.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + + // Handle "Open Dashboard" button + const dashboardBtn = target.closest('[data-open-dashboard]') as HTMLElement | null; + if (dashboardBtn) { + e.stopPropagation(); + const projectId = dashboardBtn.dataset.openDashboard; + if (projectId) { + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } + return; + } + + // Handle "Message Lead" button + const messageLeadBtn = target.closest('[data-message-lead]') as HTMLButtonElement | null; + if (messageLeadBtn && !messageLeadBtn.disabled) { + e.stopPropagation(); + const projectId = messageLeadBtn.dataset.messageLead; + if (projectId) { + elements.composerProject.value = projectId; + updateComposerAgents(); + setTimeout(() => { + elements.composerAgent.value = 'lead'; + updateComposerState(); + elements.composerMessage.focus(); + }, 50); + } + return; + } + + const card = target.closest('.project-card') as HTMLElement | null; + if (card) { + selectProject(card.dataset.projectId || null); + } + }); + + // Sidebar project clicks + elements.projectList.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + + // Check if dashboard button was clicked + const dashboardBtn = target.closest('.project-dashboard-btn') as HTMLElement | null; + if (dashboardBtn) { + e.stopPropagation(); + const projectId = dashboardBtn.dataset.dashboardProject; + if (projectId) { + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } + return; + } + + const item = target.closest('.project-item') as HTMLElement | null; + if (item) { + selectProject(item.dataset.projectId || null); + } + }); + + // Header back link + elements.channelName.addEventListener('click', (e) => { + const target = e.target as HTMLElement; + if (target.id === 'back-to-all' || target.classList.contains('back-link')) { + selectProject(null); + } + }); + + // Composer events + elements.composerProject.addEventListener('change', () => { + updateComposerAgents(); + updateComposerState(); + }); + + elements.composerAgent.addEventListener('change', updateComposerState); + elements.composerMessage.addEventListener('input', updateComposerState); + + elements.composerSend.addEventListener('click', sendBridgeMessage); + elements.composerMessage.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey && !elements.composerSend.disabled) { + e.preventDefault(); + sendBridgeMessage(); + } + }); +} + +/** + * Connect to WebSocket + */ +function connect(): void { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const ws = new WebSocket(`${protocol}//${window.location.host}/ws/bridge`); + + ws.onopen = () => { + setConnected(true); + setWebSocket(ws); + }; + + ws.onclose = () => { + setConnected(false); + setWebSocket(null); + setTimeout(connect, 3000); + }; + + ws.onerror = () => { + setConnected(false); + }; + + ws.onmessage = (e) => { + try { + const data = JSON.parse(e.data); + setProjects(data.projects || []); + setMessages(data.messages || []); + } catch (err) { + console.error('[bridge] Parse error:', err); + } + }; +} + +/** + * Initialize the bridge application + */ +export function initBridgeApp(): void { + elements = initElements(); + + // Subscribe to state changes + subscribe(() => { + updateConnectionStatus(); + renderSidebarProjects(); + renderProjectCards(); + renderMessages(); + updateStats(); + updateComposerProjects(); + updateHeader(); + if (elements.composerProject.value) { + updateComposerAgents(); + updateComposerState(); + } + }); + + // Set up event listeners + setupEventListeners(); + + // Connect to WebSocket + connect(); + + // Update uptime periodically + setInterval(() => { + elements.uptime.textContent = `Uptime: ${getUptimeString()}`; + }, 1000); +} + +// Auto-initialize when DOM is ready +if (typeof document !== 'undefined') { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initBridgeApp); + } else { + initBridgeApp(); + } +} diff --git a/src/dashboard/frontend/bridge/state.ts b/src/dashboard/frontend/bridge/state.ts new file mode 100644 index 000000000..1385fb010 --- /dev/null +++ b/src/dashboard/frontend/bridge/state.ts @@ -0,0 +1,140 @@ +/** + * Bridge State Management + * Centralized state for the bridge dashboard + */ + +import type { BridgeState, Project, BridgeMessage } from './types.js'; + +type StateListener = () => void; + +// Bridge state +export const state: BridgeState = { + projects: [], + messages: [], + selectedProjectId: null, + isConnected: false, + ws: null, + connectionStart: null, +}; + +// State subscribers +const listeners: StateListener[] = []; + +/** + * Subscribe to state changes + */ +export function subscribe(listener: StateListener): () => void { + listeners.push(listener); + return () => { + const index = listeners.indexOf(listener); + if (index > -1) { + listeners.splice(index, 1); + } + }; +} + +/** + * Notify all listeners of state change + */ +function notifyListeners(): void { + listeners.forEach((listener) => { + try { + listener(); + } catch (err) { + console.error('[bridge-state] Listener error:', err); + } + }); +} + +/** + * Update projects + */ +export function setProjects(projects: Project[]): void { + state.projects = projects; + notifyListeners(); +} + +/** + * Update messages + */ +export function setMessages(messages: BridgeMessage[]): void { + state.messages = messages; + notifyListeners(); +} + +/** + * Set selected project + */ +export function setSelectedProject(projectId: string | null): void { + state.selectedProjectId = projectId; + notifyListeners(); +} + +/** + * Update connection status + */ +export function setConnected(connected: boolean): void { + state.isConnected = connected; + if (connected && !state.connectionStart) { + state.connectionStart = Date.now(); + } + notifyListeners(); +} + +/** + * Set WebSocket instance + */ +export function setWebSocket(ws: WebSocket | null): void { + state.ws = ws; +} + +/** + * Get all agents across all projects + */ +export function getAllAgents(): { name: string; projectId: string; projectName: string; cli?: string }[] { + const agents: { name: string; projectId: string; projectName: string; cli?: string }[] = []; + + state.projects.forEach((project) => { + (project.agents || []).forEach((agent) => { + agents.push({ + name: agent.name, + projectId: project.id, + projectName: project.name || project.id, + cli: agent.cli, + }); + }); + }); + + return agents; +} + +/** + * Get connected projects + */ +export function getConnectedProjects(): Project[] { + return state.projects.filter((p) => p.connected); +} + +/** + * Get project by ID + */ +export function getProject(projectId: string): Project | undefined { + return state.projects.find((p) => p.id === projectId); +} + +/** + * Get uptime formatted string + */ +export function getUptimeString(): string { + if (!state.connectionStart) return '--'; + + const ms = Date.now() - state.connectionStart; + const seconds = Math.floor(ms / 1000); + if (seconds < 60) return `${seconds}s`; + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m`; + + const hours = Math.floor(minutes / 60); + return `${hours}h ${minutes % 60}m`; +} diff --git a/src/dashboard/frontend/bridge/types.ts b/src/dashboard/frontend/bridge/types.ts new file mode 100644 index 000000000..478ed2051 --- /dev/null +++ b/src/dashboard/frontend/bridge/types.ts @@ -0,0 +1,70 @@ +/** + * Bridge Frontend Types + * Extends shared types from the main dashboard + */ + +import type { Agent, Message } from '../types.js'; + +export interface Project { + id: string; + name?: string; + path: string; + connected: boolean; + reconnecting?: boolean; + lead?: LeadInfo; + agents?: ProjectAgent[]; +} + +export interface LeadInfo { + name: string; + connected: boolean; +} + +export interface ProjectAgent extends Agent { + projectId: string; + projectName: string; +} + +export interface BridgeMessage extends Message { + sourceProject?: string; + targetProject?: string; + body?: string; // Alternative to content for bridge messages +} + +export interface BridgeData { + projects: Project[]; + messages: BridgeMessage[]; + connected: boolean; +} + +export interface BridgeState { + projects: Project[]; + messages: BridgeMessage[]; + selectedProjectId: string | null; + isConnected: boolean; + ws: WebSocket | null; + connectionStart: number | null; +} + +export interface BridgeDOMElements { + statusDot: HTMLElement; + projectList: HTMLElement; + cardsGrid: HTMLElement; + emptyState: HTMLElement; + messagesList: HTMLElement; + searchBar: HTMLElement; + paletteOverlay: HTMLElement; + paletteSearch: HTMLInputElement; + paletteResults: HTMLElement; + paletteProjectsSection: HTMLElement; + paletteAgentsSection: HTMLElement; + channelName: HTMLElement; + statAgents: HTMLElement; + statMessages: HTMLElement; + composerProject: HTMLSelectElement; + composerAgent: HTMLSelectElement; + composerMessage: HTMLInputElement; + composerSend: HTMLButtonElement; + composerStatus: HTMLElement; + uptime: HTMLElement; +} diff --git a/src/dashboard/public/bridge.html b/src/dashboard/public/bridge.html index 5ab943e91..fea4a2f86 100644 --- a/src/dashboard/public/bridge.html +++ b/src/dashboard/public/bridge.html @@ -267,6 +267,36 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + flex: 1; + } + + .project-dashboard-btn { + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all var(--transition-fast); + } + + .project-item:hover .project-dashboard-btn { + opacity: 1; + } + + .project-dashboard-btn:hover { + background: rgba(255, 255, 255, 0.15); + color: var(--text-primary); + } + + .project-dashboard-btn svg { + width: 14px; + height: 14px; } .sidebar-footer { @@ -551,6 +581,52 @@ border-radius: 6px; } + .card-actions { + display: flex; + gap: 8px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid var(--border-divider); + } + + .card-action-btn { + flex: 1; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all var(--transition-fast); + } + + .card-action-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); + border-color: var(--accent-primary); + } + + .card-action-btn.primary { + background: var(--accent-primary); + border-color: var(--accent-primary); + color: white; + } + + .card-action-btn.primary:hover { + background: #0d4f7a; + } + + .card-action-btn svg { + width: 14px; + height: 14px; + } + /* Empty State */ .empty-state { display: flex; @@ -1166,12 +1242,27 @@

    All Projects

    +
    +
    Navigation
    +
    +
    + + + + +
    +
    +
    Go to Main Dashboard
    +
    View single-project dashboard
    +
    +
    +
    -
    Jump to Project
    +
    Open Project Dashboard
    -
    Jump to Agent
    +
    Message Agent
    @@ -1226,23 +1317,28 @@

    All Projects

    if (filteredProjects.length > 0) { paletteProjectsSection.innerHTML = ` -
    Jump to Project
    +
    Open Project Dashboard
    ${filteredProjects.map(p => ` -
    +
    - + + +
    ${escapeHtml(p.name || p.id)}
    -
    ${p.connected ? 'Online' : 'Offline'} · ${(p.agents || []).length} agents
    +
    ${p.connected ? 'Online' : 'Offline'} · ${(p.agents || []).length} agents · Click to open dashboard
    +
    +
    +
    `).join('')} `; } else { - paletteProjectsSection.innerHTML = '
    Jump to Project
    '; + paletteProjectsSection.innerHTML = '
    Open Project Dashboard
    '; } // Update agents section @@ -1312,16 +1408,37 @@

    All Projects

    const command = item.dataset.command; const projectId = item.dataset.project; const agentName = item.dataset.agent; + const action = item.dataset.action; if (command === 'broadcast') { closePalette(); - alert('Broadcast: Use the bridge CLI to send messages to all leads'); + // Focus on message composer + composerMessage.focus(); + composerStatus.textContent = 'Select a project and agent to send a message'; } else if (command === 'refresh') { closePalette(); location.reload(); + } else if (command === 'go-dashboard') { + closePalette(); + window.location.href = '/'; + } else if (action === 'open-dashboard' && projectId) { + closePalette(); + // Navigate to project-specific dashboard + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } else if (agentName && projectId) { + closePalette(); + // Pre-select project and agent in composer for messaging + composerProject.value = projectId; + updateComposerAgents(); + setTimeout(() => { + composerAgent.value = agentName; + updateComposerState(); + composerMessage.focus(); + }, 50); } else if (projectId) { closePalette(); - // Scroll to project card + // Select project and scroll to card + selectProject(projectId); const card = document.querySelector(`.project-card[data-project-id="${projectId}"]`); if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' }); } @@ -1494,6 +1611,35 @@

    All Projects

    // Handle project card clicks cardsGrid.addEventListener('click', (e) => { + // Handle "Open Dashboard" button + const dashboardBtn = e.target.closest('[data-open-dashboard]'); + if (dashboardBtn) { + e.stopPropagation(); + const projectId = dashboardBtn.dataset.openDashboard; + if (projectId) { + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } + return; + } + + // Handle "Message Lead" button + const messageLeadBtn = e.target.closest('[data-message-lead]'); + if (messageLeadBtn && !messageLeadBtn.disabled) { + e.stopPropagation(); + const projectId = messageLeadBtn.dataset.messageLead; + if (projectId) { + // Pre-select project and lead in composer + composerProject.value = projectId; + updateComposerAgents(); + setTimeout(() => { + composerAgent.value = 'lead'; + updateComposerState(); + composerMessage.focus(); + }, 50); + } + return; + } + const card = e.target.closest('.project-card'); if (card) { selectProject(card.dataset.projectId); @@ -1509,6 +1655,17 @@

    All Projects

    // Handle sidebar project clicks projectList.addEventListener('click', (e) => { + // Check if dashboard button was clicked + const dashboardBtn = e.target.closest('.project-dashboard-btn'); + if (dashboardBtn) { + e.stopPropagation(); + const projectId = dashboardBtn.dataset.dashboardProject; + if (projectId) { + window.location.href = `/project/${encodeURIComponent(projectId)}`; + } + return; + } + const item = e.target.closest('.project-item'); if (item) { selectProject(item.dataset.projectId); @@ -1543,6 +1700,13 @@

    All Projects

  • ${escapeHtml(p.name || p.id)} +
  • `).join(''); } @@ -1600,6 +1764,23 @@

    All Projects

    ${agentsHtml}
    + +
    + + +
    `; }).join(''); @@ -1693,5 +1874,8 @@

    All Projects

    // Start connect(); + + + diff --git a/src/dashboard/public/index.html b/src/dashboard/public/index.html index c8db8ecf0..f9e2804c1 100644 --- a/src/dashboard/public/index.html +++ b/src/dashboard/public/index.html @@ -307,6 +307,23 @@ height: 16px; } + /* Back to Bridge link styling */ + .nav-link.back-to-bridge { + background: rgba(18, 100, 163, 0.15); + color: var(--text-link); + border: 1px solid rgba(18, 100, 163, 0.3); + } + + .nav-link.back-to-bridge:hover { + background: rgba(18, 100, 163, 0.25); + color: var(--text-link); + } + + /* Project view mode indicator */ + body.project-view .workspace-name { + color: var(--text-link); + } + .channel-item { display: flex; align-items: center; @@ -1465,15 +1482,15 @@ -
    +
    +
    Navigation
    +
    +
    + + + + + + +
    +
    +
    Go to Bridge
    +
    Multi-project orchestration view
    +
    +
    + B +
    +
    +
    Commands
    diff --git a/src/dashboard/public/js/app.js b/src/dashboard/public/js/app.js index 4d689de6f..03138d7dc 100644 --- a/src/dashboard/public/js/app.js +++ b/src/dashboard/public/js/app.js @@ -1,13 +1,13 @@ -var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConnected:!1,ws:null,reconnectAttempts:0},y=[];function O(t){return y.push(t),()=>{let e=y.indexOf(t);e>-1&&y.splice(e,1)}}function E(){y.forEach(t=>t())}function q(t){r.agents=t,E()}function N(t){r.messages=t,E()}function R(t){r.currentChannel=t,E()}function b(t){r.isConnected=t,t&&(r.reconnectAttempts=0),E()}function K(){r.reconnectAttempts++}function V(t){r.ws=t}function z(){let{messages:t,currentChannel:e}=r;return e==="general"?t:t.filter(s=>s.from===e||s.to===e)}function x(t){r.currentThread=t}function W(t){return r.messages.filter(e=>e.thread===t)}function F(t){return r.messages.filter(e=>e.thread===t).length}var U=null;function S(){let t=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${t}//${window.location.host}/ws`);e.onopen=()=>{b(!0)},e.onclose=()=>{b(!1);let s=Math.min(1e3*Math.pow(2,r.reconnectAttempts),3e4);K(),setTimeout(S,s)},e.onerror=s=>{console.error("WebSocket error:",s)},e.onmessage=s=>{try{let n=JSON.parse(s.data);ce(n)}catch(n){console.error("Failed to parse message:",n)}},V(e)}function ce(t){console.log("[WS] Received data:",{agentCount:t.agents?.length,messageCount:t.messages?.length}),t.agents&&(console.log("[WS] Setting agents:",t.agents.map(e=>e.name)),q(t.agents)),t.messages&&N(t.messages),U&&U(t)}async function A(t,e,s){try{let n={to:t,message:e};s&&(n.thread=s);let o=await fetch("/api/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(n)}),i=await o.json();return o.ok&&i.success?{success:!0}:{success:!1,error:i.error||"Failed to send message"}}catch{return{success:!1,error:"Network error - could not send message"}}}function L(t){if(!t)return!1;let e=Date.parse(t);return Number.isNaN(e)?!1:Date.now()-e<3e4}function l(t){if(!t)return"";let e=document.createElement("div");return e.textContent=t,e.innerHTML}function T(t){return new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}function _(t){let e=new Date(t),s=new Date,n=new Date(s);return n.setDate(n.getDate()-1),e.toDateString()===s.toDateString()?"Today":e.toDateString()===n.toDateString()?"Yesterday":e.toLocaleDateString([],{weekday:"long",month:"long",day:"numeric"})}function g(t){let e=["#e01e5a","#2bac76","#e8a427","#1264a3","#7c3aed","#0d9488","#dc2626","#9333ea","#ea580c","#0891b2"],s=0;for(let n=0;n$1"),e=e.replace(/`([^`]+)`/g,"$1"),e=e.replace(/\n/g,"
    "),e}var a,c=-1;function Q(){return a={connectionDot:document.getElementById("connection-dot"),channelsList:document.getElementById("channels-list"),agentsList:document.getElementById("agents-list"),messagesList:document.getElementById("messages-list"),currentChannelName:document.getElementById("current-channel-name"),channelTopic:document.getElementById("channel-topic"),onlineCount:document.getElementById("online-count"),messageInput:document.getElementById("message-input"),sendBtn:document.getElementById("send-btn"),boldBtn:document.getElementById("bold-btn"),emojiBtn:document.getElementById("emoji-btn"),searchTrigger:document.getElementById("search-trigger"),commandPaletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteChannelsSection:document.getElementById("palette-channels-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),paletteMessagesSection:document.getElementById("palette-messages-section"),typingIndicator:document.getElementById("typing-indicator"),threadPanelOverlay:document.getElementById("thread-panel-overlay"),threadPanelId:document.getElementById("thread-panel-id"),threadPanelClose:document.getElementById("thread-panel-close"),threadMessages:document.getElementById("thread-messages"),threadMessageInput:document.getElementById("thread-message-input"),threadSendBtn:document.getElementById("thread-send-btn"),mentionAutocomplete:document.getElementById("mention-autocomplete"),mentionAutocompleteList:document.getElementById("mention-autocomplete-list")},a}function I(){return a}function Y(){r.isConnected?a.connectionDot.classList.remove("offline"):a.connectionDot.classList.add("offline")}function G(){console.log("[UI] renderAgents called, agents:",r.agents.length,r.agents.map(e=>e.name));let t=r.agents.map(e=>{let n=L(e.lastSeen||e.lastActive)?"online":"",o=r.currentChannel===e.name,i=e.needsAttention?"needs-attention":"";return` +var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConnected:!1,ws:null,reconnectAttempts:0},E=[];function N(t){return E.push(t),()=>{let e=E.indexOf(t);e>-1&&E.splice(e,1)}}function L(){E.forEach(t=>t())}function q(t){r.agents=t,L()}function R(t){r.messages=t,L()}function K(t){r.currentChannel=t,L()}function x(t){r.isConnected=t,t&&(r.reconnectAttempts=0),L()}function V(){r.reconnectAttempts++}function z(t){r.ws=t}function W(){let{messages:t,currentChannel:e}=r;return e==="general"?t:t.filter(n=>n.from===e||n.to===e)}function S(t){r.currentThread=t}function U(t){return r.messages.filter(e=>e.thread===t)}function _(t){return r.messages.filter(e=>e.thread===t).length}var F=null;function T(){let t=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${t}//${window.location.host}/ws`);e.onopen=()=>{x(!0)},e.onclose=()=>{x(!1);let n=Math.min(1e3*Math.pow(2,r.reconnectAttempts),3e4);V(),setTimeout(T,n)},e.onerror=n=>{console.error("WebSocket error:",n)},e.onmessage=n=>{try{let s=JSON.parse(n.data);ce(s)}catch(s){console.error("Failed to parse message:",s)}},z(e)}function ce(t){console.log("[WS] Received data:",{agentCount:t.agents?.length,messageCount:t.messages?.length}),t.agents&&(console.log("[WS] Setting agents:",t.agents.map(e=>e.name)),q(t.agents)),t.messages&&R(t.messages),F&&F(t)}async function w(t,e,n){try{let s={to:t,message:e};n&&(s.thread=n);let o=await fetch("/api/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),i=await o.json();return o.ok&&i.success?{success:!0}:{success:!1,error:i.error||"Failed to send message"}}catch{return{success:!1,error:"Network error - could not send message"}}}function b(t){if(!t)return!1;let e=Date.parse(t);return Number.isNaN(e)?!1:Date.now()-e<3e4}function l(t){if(!t)return"";let e=document.createElement("div");return e.textContent=t,e.innerHTML}function C(t){return new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}function J(t){let e=new Date(t),n=new Date,s=new Date(n);return s.setDate(s.getDate()-1),e.toDateString()===n.toDateString()?"Today":e.toDateString()===s.toDateString()?"Yesterday":e.toLocaleDateString([],{weekday:"long",month:"long",day:"numeric"})}function h(t){let e=["#e01e5a","#2bac76","#e8a427","#1264a3","#7c3aed","#0d9488","#dc2626","#9333ea","#ea580c","#0891b2"],n=0;for(let s=0;s$1"),e=e.replace(/`([^`]+)`/g,"$1"),e=e.replace(/\n/g,"
    "),e}var a,c=-1;function X(){return a={connectionDot:document.getElementById("connection-dot"),channelsList:document.getElementById("channels-list"),agentsList:document.getElementById("agents-list"),messagesList:document.getElementById("messages-list"),currentChannelName:document.getElementById("current-channel-name"),channelTopic:document.getElementById("channel-topic"),onlineCount:document.getElementById("online-count"),messageInput:document.getElementById("message-input"),sendBtn:document.getElementById("send-btn"),boldBtn:document.getElementById("bold-btn"),emojiBtn:document.getElementById("emoji-btn"),searchTrigger:document.getElementById("search-trigger"),commandPaletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteChannelsSection:document.getElementById("palette-channels-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),paletteMessagesSection:document.getElementById("palette-messages-section"),typingIndicator:document.getElementById("typing-indicator"),threadPanelOverlay:document.getElementById("thread-panel-overlay"),threadPanelId:document.getElementById("thread-panel-id"),threadPanelClose:document.getElementById("thread-panel-close"),threadMessages:document.getElementById("thread-messages"),threadMessageInput:document.getElementById("thread-message-input"),threadSendBtn:document.getElementById("thread-send-btn"),mentionAutocomplete:document.getElementById("mention-autocomplete"),mentionAutocompleteList:document.getElementById("mention-autocomplete-list")},a}function k(){return a}function Y(){r.isConnected?a.connectionDot.classList.remove("offline"):a.connectionDot.classList.add("offline")}function G(){console.log("[UI] renderAgents called, agents:",r.agents.length,r.agents.map(e=>e.name));let t=r.agents.map(e=>{let s=b(e.lastSeen||e.lastActive)?"online":"",o=r.currentChannel===e.name,i=e.needsAttention?"needs-attention":"";return`
  • -
    - ${h(e.name)} - +
    + ${f(e.name)} +
    ${l(e.name)} ${e.needsAttention?'Needs Input':""}
  • - `}).join("");a.agentsList.innerHTML=t||'
  • No agents connected
  • ',a.agentsList.querySelectorAll(".channel-item[data-agent]").forEach(e=>{e.addEventListener("click",()=>{let s=e.dataset.agent;s&&p(s)})}),de()}function k(){let t=z();if(t.length===0){a.messagesList.innerHTML=` + `}).join("");a.agentsList.innerHTML=t||'
  • No agents connected
  • ',a.agentsList.querySelectorAll(".channel-item[data-agent]").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.agent;n&&p(n)})}),de()}function B(){let t=W();if(t.length===0){a.messagesList.innerHTML=`
    @@ -17,38 +17,38 @@ var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConne ${r.currentChannel==="general"?"Messages between agents will appear here":`Messages with ${r.currentChannel} will appear here`}
    - `;return}let e="",s=null;t.forEach(n=>{let o=new Date(n.timestamp).toDateString();o!==s&&(e+=` + `;return}let e="",n=null;t.forEach(s=>{let o=new Date(s.timestamp).toDateString();o!==n&&(e+=`
    - ${_(n.timestamp)} + ${J(s.timestamp)}
    - `,s=o);let i=n.to==="*",u=g(n.from),f=F(n.id),le=i?"@everyone":n.project?`${l(n.project)}@${l(n.to)}`:`@${l(n.to)}`;e+=` -
    -
    - ${h(n.from)} + `,n=o);let i=s.to==="*",d=h(s.from),g=_(s.id),y=i?"@everyone":s.project?`${l(s.project)}@${l(s.to)}`:`@${l(s.to)}`;e+=` +
    +
    + ${f(s.from)}
    - @${l(n.from)} + @${l(s.from)} - \u2192 ${le} + \u2192 ${y} - ${T(n.timestamp)} + ${C(s.timestamp)}
    -
    ${C(n.content)}
    - ${n.thread?` -
    +
    ${A(s.content)}
    + ${s.thread?` +
    - Thread: ${l(n.thread)} + Thread: ${l(s.thread)}
    `:""} - ${f>0?` -
    + ${g>0?` +
    - ${f} ${f===1?"reply":"replies"} + ${g} ${g===1?"reply":"replies"}
    `:""}
    @@ -68,20 +68,20 @@ var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConne
    - `}),a.messagesList.innerHTML=e,ue()}function p(t){R(t),a.channelsList.querySelectorAll(".channel-item").forEach(s=>{s.classList.toggle("active",s.dataset.channel===t)}),a.agentsList.querySelectorAll(".channel-item").forEach(s=>{s.classList.toggle("active",s.dataset.agent===t)});let e=document.querySelector(".channel-header-name .prefix");if(t==="general")a.currentChannelName.innerHTML="general",a.channelTopic.textContent="All agent communications",e&&(e.textContent="#");else{a.currentChannelName.innerHTML=l(t);let s=r.agents.find(n=>n.name===t);a.channelTopic.textContent=s?.status||"Direct messages",e&&(e.textContent="@")}a.messageInput.placeholder=t==="general"?"@AgentName message... (or @* to broadcast)":`@${t} your message here...`,k()}function X(){let t=r.agents.filter(e=>L(e.lastSeen||e.lastActive)).length;a.onlineCount.textContent=`${t} online`}function de(){let t=r.agents.map(n=>{let o=L(n.lastSeen||n.lastActive);return` -
    + `}),a.messagesList.innerHTML=e,ue()}function p(t){K(t),a.channelsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.channel===t)}),a.agentsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.agent===t)});let e=document.querySelector(".channel-header-name .prefix");if(t==="general")a.currentChannelName.innerHTML="general",a.channelTopic.textContent="All agent communications",e&&(e.textContent="#");else{a.currentChannelName.innerHTML=l(t);let n=r.agents.find(s=>s.name===t);a.channelTopic.textContent=n?.status||"Direct messages",e&&(e.textContent="@")}a.messageInput.placeholder=t==="general"?"@AgentName message... (or @* to broadcast)":`@${t} your message here...`,B()}function Z(){let t=r.agents.filter(e=>b(e.lastSeen||e.lastActive)).length;a.onlineCount.textContent=`${t} online`}function de(){let t=r.agents.map(s=>{let o=b(s.lastSeen||s.lastActive);return` +
    -
    - ${h(n.name)} +
    + ${f(s.name)}
    -
    ${l(n.name)}
    +
    ${l(s.name)}
    ${o?"Online":"Offline"}
    - `}).join(""),e=a.paletteAgentsSection;e.querySelectorAll(".palette-item").forEach(n=>n.remove()),e.insertAdjacentHTML("beforeend",t),e.querySelectorAll(".palette-item[data-jump-agent]").forEach(n=>{n.addEventListener("click",()=>{let o=n.dataset.jumpAgent;o&&(p(o),m())})})}function Z(){a.paletteChannelsSection.querySelectorAll(".palette-item[data-jump-channel]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.jumpChannel;e&&(p(e),m())})})}function $(){a.commandPaletteOverlay.classList.add("visible"),a.paletteSearch.value="",a.paletteSearch.focus(),c=-1,H("")}function ee(){return Array.from(a.paletteResults.querySelectorAll(".palette-item")).filter(e=>e.style.display!=="none")}function J(){let t=ee();if(t.forEach(e=>e.classList.remove("selected")),c>=0&&c0?c-1:e.length-1,J();break;case"Enter":t.preventDefault(),c>=0&&ci.classList.remove("highlighted"),2e3)),m();return}}function m(){a.commandPaletteOverlay.classList.remove("visible")}function H(t){let e=t.toLowerCase();if(c=-1,document.querySelectorAll(".palette-item[data-command]").forEach(s=>{let o=s.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";s.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-channel]").forEach(s=>{let o=s.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";s.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-agent]").forEach(s=>{let n=s.dataset.jumpAgent?.toLowerCase()||"";s.style.display=n.includes(e)?"flex":"none"}),e.length>=2){let s=r.messages.filter(n=>n.content.toLowerCase().includes(e)).slice(0,5);if(s.length>0){a.paletteMessagesSection.style.display="block";let n=s.map(i=>` + `}).join(""),e=a.paletteAgentsSection;e.querySelectorAll(".palette-item").forEach(s=>s.remove()),e.insertAdjacentHTML("beforeend",t),e.querySelectorAll(".palette-item[data-jump-agent]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.jumpAgent;o&&(p(o),u())})})}function ee(){a.paletteChannelsSection.querySelectorAll(".palette-item[data-jump-channel]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.jumpChannel;e&&(p(e),u())})})}function $(){a.commandPaletteOverlay.classList.add("visible"),a.paletteSearch.value="",a.paletteSearch.focus(),c=-1,D("")}function te(){return Array.from(a.paletteResults.querySelectorAll(".palette-item")).filter(e=>e.style.display!=="none")}function Q(){let t=te();if(t.forEach(e=>e.classList.remove("selected")),c>=0&&c0?c-1:e.length-1,Q();break;case"Enter":t.preventDefault(),c>=0&&ci.classList.remove("highlighted"),2e3)),u();return}}function u(){a.commandPaletteOverlay.classList.remove("visible")}function D(t){let e=t.toLowerCase();if(c=-1,document.querySelectorAll(".palette-item[data-command]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-channel]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-agent]").forEach(n=>{let s=n.dataset.jumpAgent?.toLowerCase()||"";n.style.display=s.includes(e)?"flex":"none"}),e.length>=2){let n=r.messages.filter(s=>s.content.toLowerCase().includes(e)).slice(0,5);if(n.length>0){a.paletteMessagesSection.style.display="block";let s=n.map(i=>`
    @@ -93,35 +93,35 @@ var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConne
    ${l(i.content.substring(0,60))}${i.content.length>60?"...":""}
    - `).join("");a.paletteMessagesSection.querySelectorAll(".palette-item").forEach(i=>i.remove()),a.paletteMessagesSection.insertAdjacentHTML("beforeend",n)}else a.paletteMessagesSection.style.display="none"}else a.paletteMessagesSection.style.display="none"}function w(t){x(t),a.threadPanelId.textContent=t,a.threadPanelOverlay.classList.add("visible"),a.threadMessageInput.value="",B(t),a.threadMessageInput.focus()}function D(){x(null),a.threadPanelOverlay.classList.remove("visible")}function B(t){let e=W(t);if(e.length===0){a.threadMessages.innerHTML=` + `).join("");a.paletteMessagesSection.querySelectorAll(".palette-item").forEach(i=>i.remove()),a.paletteMessagesSection.insertAdjacentHTML("beforeend",s)}else a.paletteMessagesSection.style.display="none"}else a.paletteMessagesSection.style.display="none"}function I(t){S(t),a.threadPanelId.textContent=t,a.threadPanelOverlay.classList.add("visible"),a.threadMessageInput.value="",P(t),a.threadMessageInput.focus()}function H(){S(null),a.threadPanelOverlay.classList.remove("visible")}function P(t){let e=U(t);if(e.length===0){a.threadMessages.innerHTML=`

    No messages in this thread yet.

    Start the conversation below!

    - `;return}let s=e.map(n=>` + `;return}let n=e.map(s=>`
    -
    - ${h(n.from)} +
    + ${f(s.from)}
    - ${l(n.from)} - ${T(n.timestamp)} + ${l(s.from)} + ${C(s.timestamp)}
    -
    ${C(n.content)}
    +
    ${A(s.content)}
    - `).join("");a.threadMessages.innerHTML=s,a.threadMessages.scrollTop=a.threadMessages.scrollHeight}function ue(){a.messagesList.querySelectorAll(".thread-indicator").forEach(t=>{t.style.cursor="pointer",t.addEventListener("click",e=>{e.stopPropagation();let s=t.dataset.thread;s&&w(s)})}),a.messagesList.querySelectorAll(".reply-count-badge").forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let s=t.dataset.thread;s&&w(s)})}),a.messagesList.querySelectorAll('.message-action-btn[data-action="reply"]').forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let s=t.closest(".message")?.getAttribute("data-id");s&&w(s)})})}var d=0,M=[];function ne(t){let e=t.toLowerCase();M=r.agents.filter(n=>n.name.toLowerCase().includes(e)),d=0;let s="";("*".includes(e)||"everyone".includes(e)||"all".includes(e)||"broadcast".includes(e))&&(s+=` -
    + `).join("");a.threadMessages.innerHTML=n,a.threadMessages.scrollTop=a.threadMessages.scrollHeight}function ue(){a.messagesList.querySelectorAll(".thread-indicator").forEach(t=>{t.style.cursor="pointer",t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&I(n)})}),a.messagesList.querySelectorAll(".reply-count-badge").forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&I(n)})}),a.messagesList.querySelectorAll('.message-action-btn[data-action="reply"]').forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.closest(".message")?.getAttribute("data-id");n&&I(n)})})}var m=0,M=[];function se(t){let e=t.toLowerCase();M=r.agents.filter(s=>s.name.toLowerCase().includes(e)),m=0;let n="";("*".includes(e)||"everyone".includes(e)||"all".includes(e)||"broadcast".includes(e))&&(n+=` +
    *
    @everyone Broadcast to all
    - `),M.forEach((n,o)=>{s+=` -
    -
    - ${h(n.name)} + `),M.forEach((s,o)=>{n+=` +
    +
    + ${f(s.name)}
    - @${l(n.name)} - ${l(n.role||"Agent")} + @${l(s.name)} + ${l(s.role||"Agent")}
    - `}),s===""&&(s='
    No matching agents
    '),a.mentionAutocompleteList.innerHTML=s,a.mentionAutocomplete.classList.add("visible"),a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]").forEach(n=>{n.addEventListener("click",()=>{let o=n.dataset.mention;o&&j(o)})})}function v(){a.mentionAutocomplete.classList.remove("visible"),M=[],d=0}function se(){return a.mentionAutocomplete.classList.contains("visible")}function P(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]");e.length!==0&&(e[d]?.classList.remove("selected"),t==="down"?d=(d+1)%e.length:d=(d-1+e.length)%e.length,e[d]?.classList.add("selected"),e[d]?.scrollIntoView({block:"nearest"}))}function j(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]"),s=t;if(!s&&e.length>0&&(s=e[d]?.dataset.mention),!s){v();return}let n=a.messageInput,o=n.value,i=o.match(/^@\S*/);if(i){let u=`@${s} `;n.value=u+o.substring(i[0].length),n.selectionStart=n.selectionEnd=u.length}v(),n.focus()}function ae(){let t=a.messageInput,e=t.value,s=t.selectionStart,n=e.match(/^@(\S*)/);return n&&s<=n[0].length?n[1]:null}function oe(){let t=Q();O(()=>{Y(),G(),k(),X()}),pe(t),S()}function pe(t){t.channelsList.querySelectorAll(".channel-item").forEach(e=>{e.addEventListener("click",()=>{let s=e.dataset.channel;s&&p(s)})}),t.sendBtn.addEventListener("click",ie),t.messageInput.addEventListener("keydown",e=>{if(se()){if(e.key==="Tab"||e.key==="Enter"){e.preventDefault(),j();return}if(e.key==="ArrowUp"){e.preventDefault(),P("up");return}if(e.key==="ArrowDown"){e.preventDefault(),P("down");return}if(e.key==="Escape"){e.preventDefault(),v();return}}e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),ie())}),t.messageInput.addEventListener("input",()=>{t.messageInput.style.height="auto",t.messageInput.style.height=Math.min(t.messageInput.scrollHeight,200)+"px";let e=ae();e!==null?ne(e):v()}),t.messageInput.addEventListener("blur",()=>{setTimeout(()=>{v()},150)}),t.boldBtn.addEventListener("click",()=>{let e=t.messageInput,s=e.selectionStart,n=e.selectionEnd,o=e.value;if(s===n){let i=o.substring(0,s),u=o.substring(n);e.value=i+"**bold**"+u,e.selectionStart=s+2,e.selectionEnd=s+6}else{let i=o.substring(0,s),u=o.substring(s,n),f=o.substring(n);e.value=i+"**"+u+"**"+f,e.selectionStart=s,e.selectionEnd=n+4}e.focus()}),t.emojiBtn.addEventListener("click",()=>{let e=["\u{1F44D}","\u{1F44E}","\u2705","\u274C","\u{1F389}","\u{1F525}","\u{1F4A1}","\u26A0\uFE0F","\u{1F4DD}","\u{1F680}"],s=e[Math.floor(Math.random()*e.length)],n=t.messageInput,o=n.selectionStart,i=n.value;n.value=i.substring(0,o)+s+i.substring(o),n.selectionStart=n.selectionEnd=o+s.length,n.focus()}),t.searchTrigger.addEventListener("click",$),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="k"&&(e.preventDefault(),t.commandPaletteOverlay.classList.contains("visible")?m():$()),e.key==="Escape"&&m()}),t.commandPaletteOverlay.addEventListener("click",e=>{e.target===t.commandPaletteOverlay&&m()}),t.paletteSearch.addEventListener("input",e=>{let s=e.target;H(s.value)}),t.paletteSearch.addEventListener("keydown",te),document.querySelectorAll(".palette-item[data-command]").forEach(e=>{e.addEventListener("click",()=>{let s=e.dataset.command;s==="broadcast"?(t.messageInput.value="@* ",t.messageInput.focus()):s==="clear"&&(t.messagesList.innerHTML=""),m()})}),Z(),t.threadPanelClose.addEventListener("click",D),t.threadSendBtn.addEventListener("click",re),t.threadMessageInput.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),re())}),document.addEventListener("keydown",e=>{e.key==="Escape"&&t.threadPanelOverlay.classList.contains("visible")&&D()})}function ge(t){let s=t.trim().match(/^@(\*|[^\s]+)\s+(.+)$/s);return s?{to:s[1],message:s[2].trim()}:null}async function ie(){let t=I(),e=t.messageInput.value.trim();if(!e)return;let s=ge(e);if(!s){alert('Message must start with @recipient (e.g., "@Lead hello" or "@* broadcast")');return}let{to:n,message:o}=s;t.sendBtn.disabled=!0;let i=await A(n,o);i.success?(t.messageInput.value="",t.messageInput.style.height="auto"):alert(i.error),t.sendBtn.disabled=!1}async function re(){let t=I(),e=t.threadMessageInput.value.trim(),s=r.currentThread;if(!e||!s)return;t.threadSendBtn.disabled=!0;let n=await A("*",e,s);n.success?(t.threadMessageInput.value="",B(s)):alert(n.error),t.threadSendBtn.disabled=!1}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",oe):oe());export{oe as initApp}; + `}),n===""&&(n='
    No matching agents
    '),a.mentionAutocompleteList.innerHTML=n,a.mentionAutocomplete.classList.add("visible"),a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.mention;o&&O(o)})})}function v(){a.mentionAutocomplete.classList.remove("visible"),M=[],m=0}function ae(){return a.mentionAutocomplete.classList.contains("visible")}function j(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]");e.length!==0&&(e[m]?.classList.remove("selected"),t==="down"?m=(m+1)%e.length:m=(m-1+e.length)%e.length,e[m]?.classList.add("selected"),e[m]?.scrollIntoView({block:"nearest"}))}function O(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]"),n=t;if(!n&&e.length>0&&(n=e[m]?.dataset.mention),!n){v();return}let s=a.messageInput,o=s.value,i=o.match(/^@\S*/);if(i){let d=`@${n} `;s.value=d+o.substring(i[0].length),s.selectionStart=s.selectionEnd=d.length}v(),s.focus()}function oe(){let t=a.messageInput,e=t.value,n=t.selectionStart,s=e.match(/^@(\S*)/);return s&&n<=s[0].length?s[1]:null}function pe(){let e=window.location.pathname.match(/^\/project\/([^/]+)$/);return e?{projectId:decodeURIComponent(e[1]),fromBridge:!0}:{projectId:null,fromBridge:!1}}async function ge(t){let e=document.querySelector(".workspace-name");if(e)try{let o=await fetch(`/api/project/${encodeURIComponent(t)}`);if(o.ok){let i=await o.json(),d=e.querySelector(":not(.status-dot)");d&&d.nodeType===Node.TEXT_NODE?d.textContent=i.name||t:(Array.from(e.childNodes).filter(y=>y.nodeType===Node.TEXT_NODE).forEach(y=>y.textContent=""),e.appendChild(document.createTextNode(" "+(i.name||t))))}}catch{}let n=document.getElementById("bridge-link-text"),s=document.getElementById("bridge-nav-link");n&&(n.textContent="\u2190 Back to Bridge"),s&&s.classList.add("back-to-bridge"),document.body.classList.add("project-view")}function ie(){let t=X(),{projectId:e,fromBridge:n}=pe();n&&e&&ge(e),N(()=>{Y(),G(),B(),Z()}),he(t),T()}function he(t){t.channelsList.querySelectorAll(".channel-item").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.channel;n&&p(n)})}),t.sendBtn.addEventListener("click",re),t.messageInput.addEventListener("keydown",e=>{if(ae()){if(e.key==="Tab"||e.key==="Enter"){e.preventDefault(),O();return}if(e.key==="ArrowUp"){e.preventDefault(),j("up");return}if(e.key==="ArrowDown"){e.preventDefault(),j("down");return}if(e.key==="Escape"){e.preventDefault(),v();return}}e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),re())}),t.messageInput.addEventListener("input",()=>{t.messageInput.style.height="auto",t.messageInput.style.height=Math.min(t.messageInput.scrollHeight,200)+"px";let e=oe();e!==null?se(e):v()}),t.messageInput.addEventListener("blur",()=>{setTimeout(()=>{v()},150)}),t.boldBtn.addEventListener("click",()=>{let e=t.messageInput,n=e.selectionStart,s=e.selectionEnd,o=e.value;if(n===s){let i=o.substring(0,n),d=o.substring(s);e.value=i+"**bold**"+d,e.selectionStart=n+2,e.selectionEnd=n+6}else{let i=o.substring(0,n),d=o.substring(n,s),g=o.substring(s);e.value=i+"**"+d+"**"+g,e.selectionStart=n,e.selectionEnd=s+4}e.focus()}),t.emojiBtn.addEventListener("click",()=>{let e=["\u{1F44D}","\u{1F44E}","\u2705","\u274C","\u{1F389}","\u{1F525}","\u{1F4A1}","\u26A0\uFE0F","\u{1F4DD}","\u{1F680}"],n=e[Math.floor(Math.random()*e.length)],s=t.messageInput,o=s.selectionStart,i=s.value;s.value=i.substring(0,o)+n+i.substring(o),s.selectionStart=s.selectionEnd=o+n.length,s.focus()}),t.searchTrigger.addEventListener("click",$),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="k"&&(e.preventDefault(),t.commandPaletteOverlay.classList.contains("visible")?u():$()),e.key==="Escape"&&u()}),t.commandPaletteOverlay.addEventListener("click",e=>{e.target===t.commandPaletteOverlay&&u()}),t.paletteSearch.addEventListener("input",e=>{let n=e.target;D(n.value)}),t.paletteSearch.addEventListener("keydown",ne),document.querySelectorAll(".palette-item[data-command]").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.command;n==="bridge"?window.location.href="/bridge":n==="broadcast"?(t.messageInput.value="@* ",t.messageInput.focus()):n==="clear"&&(t.messagesList.innerHTML=""),u()})}),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="b"&&(e.preventDefault(),window.location.href="/bridge")}),ee(),t.threadPanelClose.addEventListener("click",H),t.threadSendBtn.addEventListener("click",le),t.threadMessageInput.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),le())}),document.addEventListener("keydown",e=>{e.key==="Escape"&&t.threadPanelOverlay.classList.contains("visible")&&H()})}function fe(t){let n=t.trim().match(/^@(\*|[^\s]+)\s+(.+)$/s);return n?{to:n[1],message:n[2].trim()}:null}async function re(){let t=k(),e=t.messageInput.value.trim();if(!e)return;let n=fe(e);if(!n){alert('Message must start with @recipient (e.g., "@Lead hello" or "@* broadcast")');return}let{to:s,message:o}=n;t.sendBtn.disabled=!0;let i=await w(s,o);i.success?(t.messageInput.value="",t.messageInput.style.height="auto"):alert(i.error),t.sendBtn.disabled=!1}async function le(){let t=k(),e=t.threadMessageInput.value.trim(),n=r.currentThread;if(!e||!n)return;t.threadSendBtn.disabled=!0;let s=await w("*",e,n);s.success?(t.threadMessageInput.value="",P(n)):alert(s.error),t.threadSendBtn.disabled=!1}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ie):ie());export{ie as initApp}; //# sourceMappingURL=app.js.map diff --git a/src/dashboard/public/js/app.js.map b/src/dashboard/public/js/app.js.map index 457b5b508..9f0b95867 100644 --- a/src/dashboard/public/js/app.js.map +++ b/src/dashboard/public/js/app.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../frontend/state.ts", "../../frontend/websocket.ts", "../../frontend/utils.ts", "../../frontend/components.ts", "../../frontend/app.ts"], - "sourcesContent": ["/**\n * Dashboard State Management\n */\n\nimport type { Agent, Message, AppState, ChannelType } from './types.js';\n\n/**\n * Global application state\n */\nexport const state: AppState = {\n agents: [],\n messages: [],\n currentChannel: 'general',\n currentThread: null,\n isConnected: false,\n ws: null,\n reconnectAttempts: 0,\n};\n\n/**\n * State update callbacks\n */\ntype StateListener = () => void;\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => listener());\n}\n\n/**\n * Update agents in state\n */\nexport function setAgents(agents: Agent[]): void {\n state.agents = agents;\n notifyListeners();\n}\n\n/**\n * Update messages in state\n */\nexport function setMessages(messages: Message[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set current channel/conversation\n */\nexport function setCurrentChannel(channel: ChannelType): void {\n state.currentChannel = channel;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnectionStatus(connected: boolean): void {\n state.isConnected = connected;\n if (connected) {\n state.reconnectAttempts = 0;\n }\n notifyListeners();\n}\n\n/**\n * Increment reconnect attempts\n */\nexport function incrementReconnectAttempts(): void {\n state.reconnectAttempts++;\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Filter messages based on current channel\n */\nexport function getFilteredMessages(): Message[] {\n const { messages, currentChannel } = state;\n\n if (currentChannel === 'general') {\n return messages;\n }\n\n // Filter for specific agent - show messages to/from that agent\n return messages.filter(\n (m) => m.from === currentChannel || m.to === currentChannel\n );\n}\n\n/**\n * Set current thread for thread panel\n */\nexport function setCurrentThread(thread: string | null): void {\n state.currentThread = thread;\n}\n\n/**\n * Get messages for a specific thread\n */\nexport function getThreadMessages(threadId: string): Message[] {\n return state.messages.filter((m) => m.thread === threadId);\n}\n\n/**\n * Get reply count for a thread\n */\nexport function getThreadReplyCount(threadId: string): number {\n return state.messages.filter((m) => m.thread === threadId).length;\n}\n", "/**\n * WebSocket Connection Handler\n */\n\nimport type { DashboardData } from './types.js';\nimport {\n state,\n setAgents,\n setMessages,\n setConnectionStatus,\n setWebSocket,\n incrementReconnectAttempts,\n} from './state.js';\n\ntype DataHandler = (data: DashboardData) => void;\n\nlet dataHandler: DataHandler | null = null;\n\n/**\n * Set the handler for incoming data\n */\nexport function onData(handler: DataHandler): void {\n dataHandler = handler;\n}\n\n/**\n * Connect to the WebSocket server\n */\nexport function connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);\n\n ws.onopen = (): void => {\n setConnectionStatus(true);\n };\n\n ws.onclose = (): void => {\n setConnectionStatus(false);\n // Reconnect with exponential backoff\n const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);\n incrementReconnectAttempts();\n setTimeout(connect, delay);\n };\n\n ws.onerror = (error): void => {\n console.error('WebSocket error:', error);\n };\n\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const data: DashboardData = JSON.parse(event.data as string);\n handleData(data);\n } catch (e) {\n console.error('Failed to parse message:', e);\n }\n };\n\n setWebSocket(ws);\n}\n\n/**\n * Handle incoming dashboard data\n */\nfunction handleData(data: DashboardData): void {\n console.log('[WS] Received data:', { agentCount: data.agents?.length, messageCount: data.messages?.length });\n\n if (data.agents) {\n console.log('[WS] Setting agents:', data.agents.map(a => a.name));\n setAgents(data.agents);\n }\n\n if (data.messages) {\n setMessages(data.messages);\n }\n\n if (dataHandler) {\n dataHandler(data);\n }\n}\n\n/**\n * Send a message via the REST API\n */\nexport async function sendMessage(\n to: string,\n message: string,\n thread?: string\n): Promise<{ success: boolean; error?: string }> {\n try {\n const body: { to: string; message: string; thread?: string } = { to, message };\n if (thread) {\n body.thread = thread;\n }\n\n const response = await fetch('/api/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n return { success: true };\n } else {\n return { success: false, error: result.error || 'Failed to send message' };\n }\n } catch (err) {\n return { success: false, error: 'Network error - could not send message' };\n }\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Dashboard UI Components\n */\n\nimport type { Agent, Message, DOMElements, ChannelType } from './types.js';\nimport { state, getFilteredMessages, setCurrentChannel, setCurrentThread, getThreadMessages, getThreadReplyCount } from './state.js';\nimport {\n escapeHtml,\n formatTime,\n formatDate,\n getAvatarColor,\n getInitials,\n formatMessageBody,\n isAgentOnline,\n} from './utils.js';\n\nlet elements: DOMElements;\nlet paletteSelectedIndex = -1;\n\n/**\n * Initialize DOM element references\n */\nexport function initElements(): DOMElements {\n elements = {\n connectionDot: document.getElementById('connection-dot')!,\n channelsList: document.getElementById('channels-list')!,\n agentsList: document.getElementById('agents-list')!,\n messagesList: document.getElementById('messages-list')!,\n currentChannelName: document.getElementById('current-channel-name')!,\n channelTopic: document.getElementById('channel-topic')!,\n onlineCount: document.getElementById('online-count')!,\n messageInput: document.getElementById('message-input') as HTMLTextAreaElement,\n sendBtn: document.getElementById('send-btn') as HTMLButtonElement,\n boldBtn: document.getElementById('bold-btn') as HTMLButtonElement,\n emojiBtn: document.getElementById('emoji-btn') as HTMLButtonElement,\n searchTrigger: document.getElementById('search-trigger')!,\n commandPaletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteChannelsSection: document.getElementById('palette-channels-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n paletteMessagesSection: document.getElementById('palette-messages-section')!,\n typingIndicator: document.getElementById('typing-indicator')!,\n threadPanelOverlay: document.getElementById('thread-panel-overlay')!,\n threadPanelId: document.getElementById('thread-panel-id')!,\n threadPanelClose: document.getElementById('thread-panel-close') as HTMLButtonElement,\n threadMessages: document.getElementById('thread-messages')!,\n threadMessageInput: document.getElementById('thread-message-input') as HTMLTextAreaElement,\n threadSendBtn: document.getElementById('thread-send-btn') as HTMLButtonElement,\n mentionAutocomplete: document.getElementById('mention-autocomplete')!,\n mentionAutocompleteList: document.getElementById('mention-autocomplete-list')!,\n };\n return elements;\n}\n\n/**\n * Get DOM elements\n */\nexport function getElements(): DOMElements {\n return elements;\n}\n\n/**\n * Update connection status indicator\n */\nexport function updateConnectionStatus(): void {\n if (state.isConnected) {\n elements.connectionDot.classList.remove('offline');\n } else {\n elements.connectionDot.classList.add('offline');\n }\n}\n\n/**\n * Render agents list in sidebar\n */\nexport function renderAgents(): void {\n console.log('[UI] renderAgents called, agents:', state.agents.length, state.agents.map(a => a.name));\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n const presenceClass = online ? 'online' : '';\n const isActive = state.currentChannel === agent.name;\n const needsAttentionClass = agent.needsAttention ? 'needs-attention' : '';\n\n return `\n
  • \n
    \n ${getInitials(agent.name)}\n \n
    \n ${escapeHtml(agent.name)}\n ${agent.needsAttention ? 'Needs Input' : ''}\n
  • \n `;\n })\n .join('');\n\n elements.agentsList.innerHTML =\n html ||\n '
  • No agents connected
  • ';\n\n // Add click handlers\n elements.agentsList.querySelectorAll('.channel-item[data-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.agent;\n if (agentName) {\n selectChannel(agentName);\n }\n });\n });\n\n // Update command palette agents\n updatePaletteAgents();\n}\n\n/**\n * Render messages list\n */\nexport function renderMessages(): void {\n const filtered = getFilteredMessages();\n\n if (filtered.length === 0) {\n elements.messagesList.innerHTML = `\n
    \n \n \n \n
    No messages yet
    \n
    \n ${\n state.currentChannel === 'general'\n ? 'Messages between agents will appear here'\n : `Messages with ${state.currentChannel} will appear here`\n }\n
    \n
    \n `;\n return;\n }\n\n let html = '';\n let lastDate: string | null = null;\n\n filtered.forEach((msg) => {\n const msgDate = new Date(msg.timestamp).toDateString();\n\n // Add date divider if needed\n if (msgDate !== lastDate) {\n html += `\n
    \n ${formatDate(msg.timestamp)}\n
    \n `;\n lastDate = msgDate;\n }\n\n const isBroadcast = msg.to === '*';\n const avatarColor = getAvatarColor(msg.from);\n const replyCount = getThreadReplyCount(msg.id);\n\n // Format: @From \u2192 @To: message (like Slack)\n // For cross-project messages, show project badge before agent name\n const recipientDisplay = isBroadcast\n ? '@everyone'\n : msg.project\n ? `${escapeHtml(msg.project)}@${escapeHtml(msg.to)}`\n : `@${escapeHtml(msg.to)}`;\n\n html += `\n
    \n
    \n ${getInitials(msg.from)}\n
    \n
    \n
    \n @${escapeHtml(msg.from)}\n \n \u2192 ${recipientDisplay}\n \n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n ${\n msg.thread\n ? `\n
    \n \n \n \n Thread: ${escapeHtml(msg.thread)}\n
    \n `\n : ''\n }\n ${\n replyCount > 0\n ? `\n
    \n \n \n \n ${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}\n
    \n `\n : ''\n }\n
    \n
    \n \n \n
    \n
    \n `;\n });\n\n elements.messagesList.innerHTML = html;\n\n // Note: Auto-scroll removed - interferes with manual scrolling through history\n\n // Attach thread click handlers\n attachThreadHandlers();\n}\n\n/**\n * Select a channel and update UI\n */\nexport function selectChannel(channel: ChannelType): void {\n setCurrentChannel(channel);\n\n // Update sidebar active states\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.channel === channel);\n });\n elements.agentsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.agent === channel);\n });\n\n // Update header\n const prefixEl = document.querySelector('.channel-header-name .prefix');\n if (channel === 'general') {\n elements.currentChannelName.innerHTML = 'general';\n elements.channelTopic.textContent = 'All agent communications';\n if (prefixEl) prefixEl.textContent = '#';\n } else {\n elements.currentChannelName.innerHTML = escapeHtml(channel);\n const agent = state.agents.find((a) => a.name === channel);\n elements.channelTopic.textContent = agent?.status || 'Direct messages';\n if (prefixEl) prefixEl.textContent = '@';\n }\n\n // Update composer placeholder with @mention format\n elements.messageInput.placeholder =\n channel === 'general'\n ? '@AgentName message... (or @* to broadcast)'\n : `@${channel} your message here...`;\n\n // Re-render messages\n renderMessages();\n}\n\n/**\n * Update online count display\n */\nexport function updateOnlineCount(): void {\n const online = state.agents.filter((a) => isAgentOnline(a.lastSeen || a.lastActive)).length;\n elements.onlineCount.textContent = `${online} online`;\n}\n\n/**\n * Update agents in command palette\n */\nexport function updatePaletteAgents(): void {\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n return `\n
    \n
    \n
    \n ${getInitials(agent.name)}\n \n
    \n
    \n
    \n
    ${escapeHtml(agent.name)}
    \n
    ${online ? 'Online' : 'Offline'}
    \n
    \n
    \n `;\n })\n .join('');\n\n const section = elements.paletteAgentsSection;\n const items = section.querySelectorAll('.palette-item');\n items.forEach((item) => item.remove());\n section.insertAdjacentHTML('beforeend', html);\n\n // Add click handlers\n section.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.jumpAgent;\n if (agentName) {\n selectChannel(agentName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Initialize channel click handlers in command palette\n */\nexport function initPaletteChannels(): void {\n elements.paletteChannelsSection\n .querySelectorAll('.palette-item[data-jump-channel]')\n .forEach((item) => {\n item.addEventListener('click', () => {\n const channelName = item.dataset.jumpChannel;\n if (channelName) {\n selectChannel(channelName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Open command palette\n */\nexport function openCommandPalette(): void {\n elements.commandPaletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n paletteSelectedIndex = -1;\n filterPaletteResults('');\n}\n\n/**\n * Get all visible palette items\n */\nexport function getVisiblePaletteItems(): HTMLElement[] {\n const allItems = Array.from(\n elements.paletteResults.querySelectorAll('.palette-item')\n );\n return allItems.filter((item) => item.style.display !== 'none');\n}\n\n/**\n * Update the selected palette item visually\n */\nexport function updatePaletteSelection(): void {\n const items = getVisiblePaletteItems();\n\n // Remove selection from all items\n items.forEach((item) => item.classList.remove('selected'));\n\n // Add selection to current item\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n const selectedItem = items[paletteSelectedIndex];\n selectedItem.classList.add('selected');\n\n // Scroll into view if needed\n selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }\n}\n\n/**\n * Handle keyboard navigation in command palette\n */\nexport function handlePaletteKeydown(e: KeyboardEvent): void {\n const items = getVisiblePaletteItems();\n\n if (items.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex < items.length - 1\n ? paletteSelectedIndex + 1\n : 0;\n updatePaletteSelection();\n break;\n\n case 'ArrowUp':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex > 0\n ? paletteSelectedIndex - 1\n : items.length - 1;\n updatePaletteSelection();\n break;\n\n case 'Enter':\n e.preventDefault();\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n executePaletteItem(items[paletteSelectedIndex]);\n }\n break;\n }\n}\n\n/**\n * Execute the action for a palette item\n */\nexport function executePaletteItem(item: HTMLElement): void {\n // Check for command\n const command = item.dataset.command;\n if (command) {\n if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n closeCommandPalette();\n return;\n }\n\n // Check for channel jump\n const channel = item.dataset.jumpChannel;\n if (channel) {\n selectChannel(channel);\n closeCommandPalette();\n return;\n }\n\n // Check for agent jump\n const agent = item.dataset.jumpAgent;\n if (agent) {\n selectChannel(agent);\n closeCommandPalette();\n return;\n }\n\n // Check for message jump\n const messageId = item.dataset.jumpMessage;\n if (messageId) {\n // Find and scroll to the message\n const messageEl = elements.messagesList.querySelector(`[data-id=\"${messageId}\"]`);\n if (messageEl) {\n messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n messageEl.classList.add('highlighted');\n setTimeout(() => messageEl.classList.remove('highlighted'), 2000);\n }\n closeCommandPalette();\n return;\n }\n}\n\n/**\n * Close command palette\n */\nexport function closeCommandPalette(): void {\n elements.commandPaletteOverlay.classList.remove('visible');\n}\n\n/**\n * Filter command palette results based on query\n */\nexport function filterPaletteResults(query: string): void {\n const q = query.toLowerCase();\n\n // Reset selection when filtering\n paletteSelectedIndex = -1;\n\n // Filter command items\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter channel items\n document.querySelectorAll('.palette-item[data-jump-channel]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter agent items\n document.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n const name = item.dataset.jumpAgent?.toLowerCase() || '';\n item.style.display = name.includes(q) ? 'flex' : 'none';\n });\n\n // Show message search if query is long enough\n if (q.length >= 2) {\n const matches = state.messages.filter((m) => m.content.toLowerCase().includes(q)).slice(0, 5);\n\n if (matches.length > 0) {\n elements.paletteMessagesSection.style.display = 'block';\n const items = matches\n .map(\n (m) => `\n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(m.from)}
    \n
    ${escapeHtml(m.content.substring(0, 60))}${m.content.length > 60 ? '...' : ''}
    \n
    \n
    \n `\n )\n .join('');\n\n const existingItems = elements.paletteMessagesSection.querySelectorAll('.palette-item');\n existingItems.forEach((item) => item.remove());\n elements.paletteMessagesSection.insertAdjacentHTML('beforeend', items);\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n}\n\n/**\n * Open thread panel for a specific thread\n */\nexport function openThreadPanel(threadId: string): void {\n setCurrentThread(threadId);\n elements.threadPanelId.textContent = threadId;\n elements.threadPanelOverlay.classList.add('visible');\n elements.threadMessageInput.value = '';\n renderThreadMessages(threadId);\n elements.threadMessageInput.focus();\n}\n\n/**\n * Close thread panel\n */\nexport function closeThreadPanel(): void {\n setCurrentThread(null);\n elements.threadPanelOverlay.classList.remove('visible');\n}\n\n/**\n * Render messages in thread panel\n */\nexport function renderThreadMessages(threadId: string): void {\n const messages = getThreadMessages(threadId);\n\n if (messages.length === 0) {\n elements.threadMessages.innerHTML = `\n
    \n

    No messages in this thread yet.

    \n

    Start the conversation below!

    \n
    \n `;\n return;\n }\n\n const html = messages\n .map((msg) => `\n
    \n
    \n
    \n ${getInitials(msg.from)}\n
    \n ${escapeHtml(msg.from)}\n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n
    \n `)\n .join('');\n\n elements.threadMessages.innerHTML = html;\n\n // Scroll to bottom\n elements.threadMessages.scrollTop = elements.threadMessages.scrollHeight;\n}\n\n/**\n * Attach thread click handlers to messages (call after renderMessages)\n */\nexport function attachThreadHandlers(): void {\n // Thread indicator clicks\n elements.messagesList.querySelectorAll('.thread-indicator').forEach((el) => {\n el.style.cursor = 'pointer';\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply count badge clicks\n elements.messagesList.querySelectorAll('.reply-count-badge').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply in thread button clicks\n elements.messagesList.querySelectorAll('.message-action-btn[data-action=\"reply\"]').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const messageId = el.closest('.message')?.getAttribute('data-id');\n if (messageId) {\n // Use message ID as thread ID for new threads\n openThreadPanel(messageId);\n }\n });\n });\n}\n\n/**\n * @-Mention Autocomplete State\n */\nlet mentionSelectedIndex = 0;\nlet mentionFilteredAgents: typeof state.agents = [];\n\n/**\n * Show mention autocomplete dropdown with filtered agents\n */\nexport function showMentionAutocomplete(filter: string): void {\n const filterLower = filter.toLowerCase();\n\n // Filter agents by name, include broadcast option\n mentionFilteredAgents = state.agents.filter(agent =>\n agent.name.toLowerCase().includes(filterLower)\n );\n\n // Reset selection\n mentionSelectedIndex = 0;\n\n // Build HTML for agent list\n let html = '';\n\n // Add broadcast option if filter matches\n if ('*'.includes(filterLower) || 'everyone'.includes(filterLower) || 'all'.includes(filterLower) || 'broadcast'.includes(filterLower)) {\n html += `\n
    \n
    *
    \n @everyone\n Broadcast to all\n
    \n `;\n }\n\n // Add agents\n mentionFilteredAgents.forEach((agent, index) => {\n const isSelected = index === mentionSelectedIndex;\n html += `\n
    \n
    \n ${getInitials(agent.name)}\n
    \n @${escapeHtml(agent.name)}\n ${escapeHtml(agent.role || 'Agent')}\n
    \n `;\n });\n\n if (html === '') {\n html = '
    No matching agents
    ';\n }\n\n elements.mentionAutocompleteList.innerHTML = html;\n elements.mentionAutocomplete.classList.add('visible');\n\n // Add click handlers to items\n elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]').forEach((item) => {\n item.addEventListener('click', () => {\n const mention = item.dataset.mention;\n if (mention) {\n completeMention(mention);\n }\n });\n });\n}\n\n/**\n * Hide mention autocomplete dropdown\n */\nexport function hideMentionAutocomplete(): void {\n elements.mentionAutocomplete.classList.remove('visible');\n mentionFilteredAgents = [];\n mentionSelectedIndex = 0;\n}\n\n/**\n * Check if mention autocomplete is visible\n */\nexport function isMentionAutocompleteVisible(): boolean {\n return elements.mentionAutocomplete.classList.contains('visible');\n}\n\n/**\n * Navigate mention autocomplete selection\n */\nexport function navigateMentionAutocomplete(direction: 'up' | 'down'): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n if (items.length === 0) return;\n\n // Remove current selection\n items[mentionSelectedIndex]?.classList.remove('selected');\n\n // Update index\n if (direction === 'down') {\n mentionSelectedIndex = (mentionSelectedIndex + 1) % items.length;\n } else {\n mentionSelectedIndex = (mentionSelectedIndex - 1 + items.length) % items.length;\n }\n\n // Add new selection\n items[mentionSelectedIndex]?.classList.add('selected');\n items[mentionSelectedIndex]?.scrollIntoView({ block: 'nearest' });\n}\n\n/**\n * Complete the current mention selection\n */\nexport function completeMention(mention?: string): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n\n // Use provided mention or get from selected item\n let selectedMention = mention;\n if (!selectedMention && items.length > 0) {\n selectedMention = items[mentionSelectedIndex]?.dataset.mention;\n }\n\n if (!selectedMention) {\n hideMentionAutocomplete();\n return;\n }\n\n // Replace the @... text with the completed mention\n const input = elements.messageInput;\n const value = input.value;\n\n // Find the @ position (should be at start or after whitespace)\n const atMatch = value.match(/^@\\S*/);\n if (atMatch) {\n // Replace the @partial with @CompletedName\n const completedText = `@${selectedMention} `;\n input.value = completedText + value.substring(atMatch[0].length);\n input.selectionStart = input.selectionEnd = completedText.length;\n }\n\n hideMentionAutocomplete();\n input.focus();\n}\n\n/**\n * Get the current @mention being typed (if any)\n */\nexport function getCurrentMentionQuery(): string | null {\n const input = elements.messageInput;\n const value = input.value;\n const cursorPos = input.selectionStart;\n\n // Check if cursor is within an @mention at the start\n const atMatch = value.match(/^@(\\S*)/);\n if (atMatch && cursorPos <= atMatch[0].length) {\n return atMatch[1]; // Return the text after @\n }\n\n return null;\n}\n", "/**\n * Dashboard Application Entry Point\n */\n\nimport { subscribe } from './state.js';\nimport { connect, sendMessage } from './websocket.js';\nimport {\n initElements,\n getElements,\n updateConnectionStatus,\n renderAgents,\n renderMessages,\n selectChannel,\n updateOnlineCount,\n openCommandPalette,\n closeCommandPalette,\n filterPaletteResults,\n handlePaletteKeydown,\n initPaletteChannels,\n closeThreadPanel,\n renderThreadMessages,\n showMentionAutocomplete,\n hideMentionAutocomplete,\n isMentionAutocompleteVisible,\n navigateMentionAutocomplete,\n completeMention,\n getCurrentMentionQuery,\n} from './components.js';\nimport { state } from './state.js';\n\n/**\n * Initialize the dashboard application\n */\nexport function initApp(): void {\n const elements = initElements();\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderAgents();\n renderMessages();\n updateOnlineCount();\n });\n\n // Set up event listeners\n setupEventListeners(elements);\n\n // Connect to WebSocket\n connect();\n}\n\n/**\n * Set up all event listeners\n */\nfunction setupEventListeners(elements: ReturnType): void {\n // Channel clicks\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.addEventListener('click', () => {\n const channel = item.dataset.channel;\n if (channel) {\n selectChannel(channel);\n }\n });\n });\n\n // Send button\n elements.sendBtn.addEventListener('click', handleSend);\n\n // Keyboard shortcuts for composer\n elements.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n // Handle mention autocomplete keys first\n if (isMentionAutocompleteVisible()) {\n if (e.key === 'Tab' || e.key === 'Enter') {\n e.preventDefault();\n completeMention();\n return;\n }\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n navigateMentionAutocomplete('up');\n return;\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n navigateMentionAutocomplete('down');\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n hideMentionAutocomplete();\n return;\n }\n }\n\n // Enter to send (Slack-style), Shift+Enter for newline\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Auto-resize textarea and handle @-mention autocomplete\n elements.messageInput.addEventListener('input', () => {\n elements.messageInput.style.height = 'auto';\n elements.messageInput.style.height =\n Math.min(elements.messageInput.scrollHeight, 200) + 'px';\n\n // Check for @-mention at start of input\n const query = getCurrentMentionQuery();\n if (query !== null) {\n showMentionAutocomplete(query);\n } else {\n hideMentionAutocomplete();\n }\n });\n\n // Hide mention autocomplete when input loses focus (with delay to allow clicks)\n elements.messageInput.addEventListener('blur', () => {\n setTimeout(() => {\n hideMentionAutocomplete();\n }, 150);\n });\n\n // Bold button - wrap selected text with ** or insert **bold**\n elements.boldBtn.addEventListener('click', () => {\n const input = elements.messageInput;\n const start = input.selectionStart;\n const end = input.selectionEnd;\n const text = input.value;\n\n if (start === end) {\n // No selection - insert **bold** placeholder\n const before = text.substring(0, start);\n const after = text.substring(end);\n input.value = before + '**bold**' + after;\n input.selectionStart = start + 2;\n input.selectionEnd = start + 6;\n } else {\n // Wrap selection with **\n const before = text.substring(0, start);\n const selected = text.substring(start, end);\n const after = text.substring(end);\n input.value = before + '**' + selected + '**' + after;\n input.selectionStart = start;\n input.selectionEnd = end + 4;\n }\n input.focus();\n });\n\n // Emoji button - insert common emojis via simple picker\n elements.emojiBtn.addEventListener('click', () => {\n const emojis = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2705', '\u274C', '\uD83C\uDF89', '\uD83D\uDD25', '\uD83D\uDCA1', '\u26A0\uFE0F', '\uD83D\uDCDD', '\uD83D\uDE80'];\n const emoji = emojis[Math.floor(Math.random() * emojis.length)];\n const input = elements.messageInput;\n const start = input.selectionStart;\n const text = input.value;\n input.value = text.substring(0, start) + emoji + text.substring(start);\n input.selectionStart = input.selectionEnd = start + emoji.length;\n input.focus();\n });\n\n // Command palette\n elements.searchTrigger.addEventListener('click', openCommandPalette);\n\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.commandPaletteOverlay.classList.contains('visible')) {\n closeCommandPalette();\n } else {\n openCommandPalette();\n }\n }\n\n if (e.key === 'Escape') {\n closeCommandPalette();\n }\n });\n\n elements.commandPaletteOverlay.addEventListener('click', (e: MouseEvent) => {\n if (e.target === elements.commandPaletteOverlay) {\n closeCommandPalette();\n }\n });\n\n elements.paletteSearch.addEventListener('input', (e: Event) => {\n const target = e.target as HTMLInputElement;\n filterPaletteResults(target.value);\n });\n\n elements.paletteSearch.addEventListener('keydown', handlePaletteKeydown);\n\n // Command execution\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n item.addEventListener('click', () => {\n const command = item.dataset.command;\n\n if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n\n closeCommandPalette();\n });\n });\n\n // Initialize palette channel click handlers\n initPaletteChannels();\n\n // Thread panel close button\n elements.threadPanelClose.addEventListener('click', closeThreadPanel);\n\n // Thread panel send button\n elements.threadSendBtn.addEventListener('click', handleThreadSend);\n\n // Thread message input keyboard shortcuts (Slack-style)\n elements.threadMessageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleThreadSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Close thread panel on Escape\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape' && elements.threadPanelOverlay.classList.contains('visible')) {\n closeThreadPanel();\n }\n });\n}\n\n/**\n * Parse @mention from message text\n * Formats: \"@AgentName message\" or \"@* message\" for broadcast\n * Returns { to, message } or null if no valid mention found\n */\nfunction parseMention(text: string): { to: string; message: string } | null {\n const trimmed = text.trim();\n\n // Match @mention at the start of the message\n // @* for broadcast, @AgentName for direct message\n const match = trimmed.match(/^@(\\*|[^\\s]+)\\s+(.+)$/s);\n\n if (!match) {\n return null;\n }\n\n return {\n to: match[1],\n message: match[2].trim(),\n };\n}\n\n/**\n * Handle send button click\n */\nasync function handleSend(): Promise {\n const elements = getElements();\n const rawMessage = elements.messageInput.value.trim();\n\n if (!rawMessage) {\n return;\n }\n\n // Parse @mention from the message\n const parsed = parseMention(rawMessage);\n\n if (!parsed) {\n alert('Message must start with @recipient (e.g., \"@Lead hello\" or \"@* broadcast\")');\n return;\n }\n\n const { to, message } = parsed;\n\n elements.sendBtn.disabled = true;\n\n const result = await sendMessage(to, message);\n\n if (result.success) {\n elements.messageInput.value = '';\n elements.messageInput.style.height = 'auto';\n } else {\n alert(result.error);\n }\n\n elements.sendBtn.disabled = false;\n}\n\n/**\n * Handle thread panel send button click\n */\nasync function handleThreadSend(): Promise {\n const elements = getElements();\n const message = elements.threadMessageInput.value.trim();\n const threadId = state.currentThread;\n\n if (!message || !threadId) {\n return;\n }\n\n // For thread replies, send to broadcast or use original recipient\n // For now, send as broadcast with thread ID\n elements.threadSendBtn.disabled = true;\n\n const result = await sendMessage('*', message, threadId);\n\n if (result.success) {\n elements.threadMessageInput.value = '';\n // Re-render thread messages to show the new message\n renderThreadMessages(threadId);\n } else {\n alert(result.error);\n }\n\n elements.threadSendBtn.disabled = false;\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initApp);\n } else {\n initApp();\n }\n}\n"], - "mappings": "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", - "names": ["state", "listeners", "subscribe", "listener", "index", "notifyListeners", "setAgents", "agents", "setMessages", "messages", "setCurrentChannel", "channel", "setConnectionStatus", "connected", "incrementReconnectAttempts", "setWebSocket", "ws", "getFilteredMessages", "currentChannel", "m", "setCurrentThread", "thread", "getThreadMessages", "threadId", "getThreadReplyCount", "dataHandler", "connect", "protocol", "ws", "setConnectionStatus", "delay", "state", "incrementReconnectAttempts", "error", "event", "data", "handleData", "e", "setWebSocket", "a", "setAgents", "setMessages", "dataHandler", "sendMessage", "to", "message", "thread", "body", "response", "result", "isAgentOnline", "lastSeen", "ts", "escapeHtml", "text", "div", "formatTime", "timestamp", "formatDate", "date", "today", "yesterday", "getAvatarColor", "name", "colors", "hash", "i", "getInitials", "formatMessageBody", "content", "escaped", "elements", "paletteSelectedIndex", "initElements", "getElements", "updateConnectionStatus", "state", "renderAgents", "a", "html", "agent", "presenceClass", "isAgentOnline", "isActive", "needsAttentionClass", "escapeHtml", "getAvatarColor", "getInitials", "item", "agentName", "selectChannel", "updatePaletteAgents", "renderMessages", "filtered", "getFilteredMessages", "lastDate", "msg", "msgDate", "formatDate", "isBroadcast", "avatarColor", "replyCount", "getThreadReplyCount", "recipientDisplay", "formatTime", "formatMessageBody", "attachThreadHandlers", "channel", "setCurrentChannel", "prefixEl", "updateOnlineCount", "online", "section", "closeCommandPalette", "initPaletteChannels", "channelName", "openCommandPalette", "filterPaletteResults", "getVisiblePaletteItems", "updatePaletteSelection", "items", "selectedItem", "handlePaletteKeydown", "e", "executePaletteItem", "command", "messageId", "messageEl", "query", "q", "title", "name", "matches", "m", "openThreadPanel", "threadId", "setCurrentThread", "renderThreadMessages", "closeThreadPanel", "messages", "getThreadMessages", "el", "mentionSelectedIndex", "mentionFilteredAgents", "showMentionAutocomplete", "filter", "filterLower", "index", "mention", "completeMention", "hideMentionAutocomplete", "isMentionAutocompleteVisible", "navigateMentionAutocomplete", "direction", "selectedMention", "input", "value", "atMatch", "completedText", "getCurrentMentionQuery", "cursorPos", "initApp", "elements", "initElements", "subscribe", "updateConnectionStatus", "renderAgents", "renderMessages", "updateOnlineCount", "setupEventListeners", "connect", "item", "channel", "selectChannel", "handleSend", "isMentionAutocompleteVisible", "completeMention", "navigateMentionAutocomplete", "hideMentionAutocomplete", "query", "getCurrentMentionQuery", "showMentionAutocomplete", "input", "start", "end", "text", "before", "after", "selected", "emojis", "emoji", "openCommandPalette", "closeCommandPalette", "target", "filterPaletteResults", "handlePaletteKeydown", "command", "initPaletteChannels", "closeThreadPanel", "handleThreadSend", "parseMention", "match", "getElements", "rawMessage", "parsed", "to", "message", "result", "sendMessage", "threadId", "state", "renderThreadMessages"] + "sourcesContent": ["/**\n * Dashboard State Management\n */\n\nimport type { Agent, Message, AppState, ChannelType } from './types.js';\n\n/**\n * Global application state\n */\nexport const state: AppState = {\n agents: [],\n messages: [],\n currentChannel: 'general',\n currentThread: null,\n isConnected: false,\n ws: null,\n reconnectAttempts: 0,\n};\n\n/**\n * State update callbacks\n */\ntype StateListener = () => void;\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => listener());\n}\n\n/**\n * Update agents in state\n */\nexport function setAgents(agents: Agent[]): void {\n state.agents = agents;\n notifyListeners();\n}\n\n/**\n * Update messages in state\n */\nexport function setMessages(messages: Message[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set current channel/conversation\n */\nexport function setCurrentChannel(channel: ChannelType): void {\n state.currentChannel = channel;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnectionStatus(connected: boolean): void {\n state.isConnected = connected;\n if (connected) {\n state.reconnectAttempts = 0;\n }\n notifyListeners();\n}\n\n/**\n * Increment reconnect attempts\n */\nexport function incrementReconnectAttempts(): void {\n state.reconnectAttempts++;\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Filter messages based on current channel\n */\nexport function getFilteredMessages(): Message[] {\n const { messages, currentChannel } = state;\n\n if (currentChannel === 'general') {\n return messages;\n }\n\n // Filter for specific agent - show messages to/from that agent\n return messages.filter(\n (m) => m.from === currentChannel || m.to === currentChannel\n );\n}\n\n/**\n * Set current thread for thread panel\n */\nexport function setCurrentThread(thread: string | null): void {\n state.currentThread = thread;\n}\n\n/**\n * Get messages for a specific thread\n */\nexport function getThreadMessages(threadId: string): Message[] {\n return state.messages.filter((m) => m.thread === threadId);\n}\n\n/**\n * Get reply count for a thread\n */\nexport function getThreadReplyCount(threadId: string): number {\n return state.messages.filter((m) => m.thread === threadId).length;\n}\n", "/**\n * WebSocket Connection Handler\n */\n\nimport type { DashboardData } from './types.js';\nimport {\n state,\n setAgents,\n setMessages,\n setConnectionStatus,\n setWebSocket,\n incrementReconnectAttempts,\n} from './state.js';\n\ntype DataHandler = (data: DashboardData) => void;\n\nlet dataHandler: DataHandler | null = null;\n\n/**\n * Set the handler for incoming data\n */\nexport function onData(handler: DataHandler): void {\n dataHandler = handler;\n}\n\n/**\n * Connect to the WebSocket server\n */\nexport function connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);\n\n ws.onopen = (): void => {\n setConnectionStatus(true);\n };\n\n ws.onclose = (): void => {\n setConnectionStatus(false);\n // Reconnect with exponential backoff\n const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);\n incrementReconnectAttempts();\n setTimeout(connect, delay);\n };\n\n ws.onerror = (error): void => {\n console.error('WebSocket error:', error);\n };\n\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const data: DashboardData = JSON.parse(event.data as string);\n handleData(data);\n } catch (e) {\n console.error('Failed to parse message:', e);\n }\n };\n\n setWebSocket(ws);\n}\n\n/**\n * Handle incoming dashboard data\n */\nfunction handleData(data: DashboardData): void {\n console.log('[WS] Received data:', { agentCount: data.agents?.length, messageCount: data.messages?.length });\n\n if (data.agents) {\n console.log('[WS] Setting agents:', data.agents.map(a => a.name));\n setAgents(data.agents);\n }\n\n if (data.messages) {\n setMessages(data.messages);\n }\n\n if (dataHandler) {\n dataHandler(data);\n }\n}\n\n/**\n * Send a message via the REST API\n */\nexport async function sendMessage(\n to: string,\n message: string,\n thread?: string\n): Promise<{ success: boolean; error?: string }> {\n try {\n const body: { to: string; message: string; thread?: string } = { to, message };\n if (thread) {\n body.thread = thread;\n }\n\n const response = await fetch('/api/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n return { success: true };\n } else {\n return { success: false, error: result.error || 'Failed to send message' };\n }\n } catch (err) {\n return { success: false, error: 'Network error - could not send message' };\n }\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Dashboard UI Components\n */\n\nimport type { Agent, Message, DOMElements, ChannelType } from './types.js';\nimport { state, getFilteredMessages, setCurrentChannel, setCurrentThread, getThreadMessages, getThreadReplyCount } from './state.js';\nimport {\n escapeHtml,\n formatTime,\n formatDate,\n getAvatarColor,\n getInitials,\n formatMessageBody,\n isAgentOnline,\n} from './utils.js';\n\nlet elements: DOMElements;\nlet paletteSelectedIndex = -1;\n\n/**\n * Initialize DOM element references\n */\nexport function initElements(): DOMElements {\n elements = {\n connectionDot: document.getElementById('connection-dot')!,\n channelsList: document.getElementById('channels-list')!,\n agentsList: document.getElementById('agents-list')!,\n messagesList: document.getElementById('messages-list')!,\n currentChannelName: document.getElementById('current-channel-name')!,\n channelTopic: document.getElementById('channel-topic')!,\n onlineCount: document.getElementById('online-count')!,\n messageInput: document.getElementById('message-input') as HTMLTextAreaElement,\n sendBtn: document.getElementById('send-btn') as HTMLButtonElement,\n boldBtn: document.getElementById('bold-btn') as HTMLButtonElement,\n emojiBtn: document.getElementById('emoji-btn') as HTMLButtonElement,\n searchTrigger: document.getElementById('search-trigger')!,\n commandPaletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteChannelsSection: document.getElementById('palette-channels-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n paletteMessagesSection: document.getElementById('palette-messages-section')!,\n typingIndicator: document.getElementById('typing-indicator')!,\n threadPanelOverlay: document.getElementById('thread-panel-overlay')!,\n threadPanelId: document.getElementById('thread-panel-id')!,\n threadPanelClose: document.getElementById('thread-panel-close') as HTMLButtonElement,\n threadMessages: document.getElementById('thread-messages')!,\n threadMessageInput: document.getElementById('thread-message-input') as HTMLTextAreaElement,\n threadSendBtn: document.getElementById('thread-send-btn') as HTMLButtonElement,\n mentionAutocomplete: document.getElementById('mention-autocomplete')!,\n mentionAutocompleteList: document.getElementById('mention-autocomplete-list')!,\n };\n return elements;\n}\n\n/**\n * Get DOM elements\n */\nexport function getElements(): DOMElements {\n return elements;\n}\n\n/**\n * Update connection status indicator\n */\nexport function updateConnectionStatus(): void {\n if (state.isConnected) {\n elements.connectionDot.classList.remove('offline');\n } else {\n elements.connectionDot.classList.add('offline');\n }\n}\n\n/**\n * Render agents list in sidebar\n */\nexport function renderAgents(): void {\n console.log('[UI] renderAgents called, agents:', state.agents.length, state.agents.map(a => a.name));\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n const presenceClass = online ? 'online' : '';\n const isActive = state.currentChannel === agent.name;\n const needsAttentionClass = agent.needsAttention ? 'needs-attention' : '';\n\n return `\n
  • \n
    \n ${getInitials(agent.name)}\n \n
    \n ${escapeHtml(agent.name)}\n ${agent.needsAttention ? 'Needs Input' : ''}\n
  • \n `;\n })\n .join('');\n\n elements.agentsList.innerHTML =\n html ||\n '
  • No agents connected
  • ';\n\n // Add click handlers\n elements.agentsList.querySelectorAll('.channel-item[data-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.agent;\n if (agentName) {\n selectChannel(agentName);\n }\n });\n });\n\n // Update command palette agents\n updatePaletteAgents();\n}\n\n/**\n * Render messages list\n */\nexport function renderMessages(): void {\n const filtered = getFilteredMessages();\n\n if (filtered.length === 0) {\n elements.messagesList.innerHTML = `\n
    \n \n \n \n
    No messages yet
    \n
    \n ${\n state.currentChannel === 'general'\n ? 'Messages between agents will appear here'\n : `Messages with ${state.currentChannel} will appear here`\n }\n
    \n
    \n `;\n return;\n }\n\n let html = '';\n let lastDate: string | null = null;\n\n filtered.forEach((msg) => {\n const msgDate = new Date(msg.timestamp).toDateString();\n\n // Add date divider if needed\n if (msgDate !== lastDate) {\n html += `\n
    \n ${formatDate(msg.timestamp)}\n
    \n `;\n lastDate = msgDate;\n }\n\n const isBroadcast = msg.to === '*';\n const avatarColor = getAvatarColor(msg.from);\n const replyCount = getThreadReplyCount(msg.id);\n\n // Format: @From \u2192 @To: message (like Slack)\n // For cross-project messages, show project badge before agent name\n const recipientDisplay = isBroadcast\n ? '@everyone'\n : msg.project\n ? `${escapeHtml(msg.project)}@${escapeHtml(msg.to)}`\n : `@${escapeHtml(msg.to)}`;\n\n html += `\n
    \n
    \n ${getInitials(msg.from)}\n
    \n
    \n
    \n @${escapeHtml(msg.from)}\n \n \u2192 ${recipientDisplay}\n \n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n ${\n msg.thread\n ? `\n
    \n \n \n \n Thread: ${escapeHtml(msg.thread)}\n
    \n `\n : ''\n }\n ${\n replyCount > 0\n ? `\n
    \n \n \n \n ${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}\n
    \n `\n : ''\n }\n
    \n
    \n \n \n
    \n
    \n `;\n });\n\n elements.messagesList.innerHTML = html;\n\n // Note: Auto-scroll removed - interferes with manual scrolling through history\n\n // Attach thread click handlers\n attachThreadHandlers();\n}\n\n/**\n * Select a channel and update UI\n */\nexport function selectChannel(channel: ChannelType): void {\n setCurrentChannel(channel);\n\n // Update sidebar active states\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.channel === channel);\n });\n elements.agentsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.agent === channel);\n });\n\n // Update header\n const prefixEl = document.querySelector('.channel-header-name .prefix');\n if (channel === 'general') {\n elements.currentChannelName.innerHTML = 'general';\n elements.channelTopic.textContent = 'All agent communications';\n if (prefixEl) prefixEl.textContent = '#';\n } else {\n elements.currentChannelName.innerHTML = escapeHtml(channel);\n const agent = state.agents.find((a) => a.name === channel);\n elements.channelTopic.textContent = agent?.status || 'Direct messages';\n if (prefixEl) prefixEl.textContent = '@';\n }\n\n // Update composer placeholder with @mention format\n elements.messageInput.placeholder =\n channel === 'general'\n ? '@AgentName message... (or @* to broadcast)'\n : `@${channel} your message here...`;\n\n // Re-render messages\n renderMessages();\n}\n\n/**\n * Update online count display\n */\nexport function updateOnlineCount(): void {\n const online = state.agents.filter((a) => isAgentOnline(a.lastSeen || a.lastActive)).length;\n elements.onlineCount.textContent = `${online} online`;\n}\n\n/**\n * Update agents in command palette\n */\nexport function updatePaletteAgents(): void {\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n return `\n
    \n
    \n
    \n ${getInitials(agent.name)}\n \n
    \n
    \n
    \n
    ${escapeHtml(agent.name)}
    \n
    ${online ? 'Online' : 'Offline'}
    \n
    \n
    \n `;\n })\n .join('');\n\n const section = elements.paletteAgentsSection;\n const items = section.querySelectorAll('.palette-item');\n items.forEach((item) => item.remove());\n section.insertAdjacentHTML('beforeend', html);\n\n // Add click handlers\n section.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.jumpAgent;\n if (agentName) {\n selectChannel(agentName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Initialize channel click handlers in command palette\n */\nexport function initPaletteChannels(): void {\n elements.paletteChannelsSection\n .querySelectorAll('.palette-item[data-jump-channel]')\n .forEach((item) => {\n item.addEventListener('click', () => {\n const channelName = item.dataset.jumpChannel;\n if (channelName) {\n selectChannel(channelName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Open command palette\n */\nexport function openCommandPalette(): void {\n elements.commandPaletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n paletteSelectedIndex = -1;\n filterPaletteResults('');\n}\n\n/**\n * Get all visible palette items\n */\nexport function getVisiblePaletteItems(): HTMLElement[] {\n const allItems = Array.from(\n elements.paletteResults.querySelectorAll('.palette-item')\n );\n return allItems.filter((item) => item.style.display !== 'none');\n}\n\n/**\n * Update the selected palette item visually\n */\nexport function updatePaletteSelection(): void {\n const items = getVisiblePaletteItems();\n\n // Remove selection from all items\n items.forEach((item) => item.classList.remove('selected'));\n\n // Add selection to current item\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n const selectedItem = items[paletteSelectedIndex];\n selectedItem.classList.add('selected');\n\n // Scroll into view if needed\n selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }\n}\n\n/**\n * Handle keyboard navigation in command palette\n */\nexport function handlePaletteKeydown(e: KeyboardEvent): void {\n const items = getVisiblePaletteItems();\n\n if (items.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex < items.length - 1\n ? paletteSelectedIndex + 1\n : 0;\n updatePaletteSelection();\n break;\n\n case 'ArrowUp':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex > 0\n ? paletteSelectedIndex - 1\n : items.length - 1;\n updatePaletteSelection();\n break;\n\n case 'Enter':\n e.preventDefault();\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n executePaletteItem(items[paletteSelectedIndex]);\n }\n break;\n }\n}\n\n/**\n * Execute the action for a palette item\n */\nexport function executePaletteItem(item: HTMLElement): void {\n // Check for command\n const command = item.dataset.command;\n if (command) {\n if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n closeCommandPalette();\n return;\n }\n\n // Check for channel jump\n const channel = item.dataset.jumpChannel;\n if (channel) {\n selectChannel(channel);\n closeCommandPalette();\n return;\n }\n\n // Check for agent jump\n const agent = item.dataset.jumpAgent;\n if (agent) {\n selectChannel(agent);\n closeCommandPalette();\n return;\n }\n\n // Check for message jump\n const messageId = item.dataset.jumpMessage;\n if (messageId) {\n // Find and scroll to the message\n const messageEl = elements.messagesList.querySelector(`[data-id=\"${messageId}\"]`);\n if (messageEl) {\n messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n messageEl.classList.add('highlighted');\n setTimeout(() => messageEl.classList.remove('highlighted'), 2000);\n }\n closeCommandPalette();\n return;\n }\n}\n\n/**\n * Close command palette\n */\nexport function closeCommandPalette(): void {\n elements.commandPaletteOverlay.classList.remove('visible');\n}\n\n/**\n * Filter command palette results based on query\n */\nexport function filterPaletteResults(query: string): void {\n const q = query.toLowerCase();\n\n // Reset selection when filtering\n paletteSelectedIndex = -1;\n\n // Filter command items\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter channel items\n document.querySelectorAll('.palette-item[data-jump-channel]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter agent items\n document.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n const name = item.dataset.jumpAgent?.toLowerCase() || '';\n item.style.display = name.includes(q) ? 'flex' : 'none';\n });\n\n // Show message search if query is long enough\n if (q.length >= 2) {\n const matches = state.messages.filter((m) => m.content.toLowerCase().includes(q)).slice(0, 5);\n\n if (matches.length > 0) {\n elements.paletteMessagesSection.style.display = 'block';\n const items = matches\n .map(\n (m) => `\n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(m.from)}
    \n
    ${escapeHtml(m.content.substring(0, 60))}${m.content.length > 60 ? '...' : ''}
    \n
    \n
    \n `\n )\n .join('');\n\n const existingItems = elements.paletteMessagesSection.querySelectorAll('.palette-item');\n existingItems.forEach((item) => item.remove());\n elements.paletteMessagesSection.insertAdjacentHTML('beforeend', items);\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n}\n\n/**\n * Open thread panel for a specific thread\n */\nexport function openThreadPanel(threadId: string): void {\n setCurrentThread(threadId);\n elements.threadPanelId.textContent = threadId;\n elements.threadPanelOverlay.classList.add('visible');\n elements.threadMessageInput.value = '';\n renderThreadMessages(threadId);\n elements.threadMessageInput.focus();\n}\n\n/**\n * Close thread panel\n */\nexport function closeThreadPanel(): void {\n setCurrentThread(null);\n elements.threadPanelOverlay.classList.remove('visible');\n}\n\n/**\n * Render messages in thread panel\n */\nexport function renderThreadMessages(threadId: string): void {\n const messages = getThreadMessages(threadId);\n\n if (messages.length === 0) {\n elements.threadMessages.innerHTML = `\n
    \n

    No messages in this thread yet.

    \n

    Start the conversation below!

    \n
    \n `;\n return;\n }\n\n const html = messages\n .map((msg) => `\n
    \n
    \n
    \n ${getInitials(msg.from)}\n
    \n ${escapeHtml(msg.from)}\n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n
    \n `)\n .join('');\n\n elements.threadMessages.innerHTML = html;\n\n // Scroll to bottom\n elements.threadMessages.scrollTop = elements.threadMessages.scrollHeight;\n}\n\n/**\n * Attach thread click handlers to messages (call after renderMessages)\n */\nexport function attachThreadHandlers(): void {\n // Thread indicator clicks\n elements.messagesList.querySelectorAll('.thread-indicator').forEach((el) => {\n el.style.cursor = 'pointer';\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply count badge clicks\n elements.messagesList.querySelectorAll('.reply-count-badge').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply in thread button clicks\n elements.messagesList.querySelectorAll('.message-action-btn[data-action=\"reply\"]').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const messageId = el.closest('.message')?.getAttribute('data-id');\n if (messageId) {\n // Use message ID as thread ID for new threads\n openThreadPanel(messageId);\n }\n });\n });\n}\n\n/**\n * @-Mention Autocomplete State\n */\nlet mentionSelectedIndex = 0;\nlet mentionFilteredAgents: typeof state.agents = [];\n\n/**\n * Show mention autocomplete dropdown with filtered agents\n */\nexport function showMentionAutocomplete(filter: string): void {\n const filterLower = filter.toLowerCase();\n\n // Filter agents by name, include broadcast option\n mentionFilteredAgents = state.agents.filter(agent =>\n agent.name.toLowerCase().includes(filterLower)\n );\n\n // Reset selection\n mentionSelectedIndex = 0;\n\n // Build HTML for agent list\n let html = '';\n\n // Add broadcast option if filter matches\n if ('*'.includes(filterLower) || 'everyone'.includes(filterLower) || 'all'.includes(filterLower) || 'broadcast'.includes(filterLower)) {\n html += `\n
    \n
    *
    \n @everyone\n Broadcast to all\n
    \n `;\n }\n\n // Add agents\n mentionFilteredAgents.forEach((agent, index) => {\n const isSelected = index === mentionSelectedIndex;\n html += `\n
    \n
    \n ${getInitials(agent.name)}\n
    \n @${escapeHtml(agent.name)}\n ${escapeHtml(agent.role || 'Agent')}\n
    \n `;\n });\n\n if (html === '') {\n html = '
    No matching agents
    ';\n }\n\n elements.mentionAutocompleteList.innerHTML = html;\n elements.mentionAutocomplete.classList.add('visible');\n\n // Add click handlers to items\n elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]').forEach((item) => {\n item.addEventListener('click', () => {\n const mention = item.dataset.mention;\n if (mention) {\n completeMention(mention);\n }\n });\n });\n}\n\n/**\n * Hide mention autocomplete dropdown\n */\nexport function hideMentionAutocomplete(): void {\n elements.mentionAutocomplete.classList.remove('visible');\n mentionFilteredAgents = [];\n mentionSelectedIndex = 0;\n}\n\n/**\n * Check if mention autocomplete is visible\n */\nexport function isMentionAutocompleteVisible(): boolean {\n return elements.mentionAutocomplete.classList.contains('visible');\n}\n\n/**\n * Navigate mention autocomplete selection\n */\nexport function navigateMentionAutocomplete(direction: 'up' | 'down'): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n if (items.length === 0) return;\n\n // Remove current selection\n items[mentionSelectedIndex]?.classList.remove('selected');\n\n // Update index\n if (direction === 'down') {\n mentionSelectedIndex = (mentionSelectedIndex + 1) % items.length;\n } else {\n mentionSelectedIndex = (mentionSelectedIndex - 1 + items.length) % items.length;\n }\n\n // Add new selection\n items[mentionSelectedIndex]?.classList.add('selected');\n items[mentionSelectedIndex]?.scrollIntoView({ block: 'nearest' });\n}\n\n/**\n * Complete the current mention selection\n */\nexport function completeMention(mention?: string): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n\n // Use provided mention or get from selected item\n let selectedMention = mention;\n if (!selectedMention && items.length > 0) {\n selectedMention = items[mentionSelectedIndex]?.dataset.mention;\n }\n\n if (!selectedMention) {\n hideMentionAutocomplete();\n return;\n }\n\n // Replace the @... text with the completed mention\n const input = elements.messageInput;\n const value = input.value;\n\n // Find the @ position (should be at start or after whitespace)\n const atMatch = value.match(/^@\\S*/);\n if (atMatch) {\n // Replace the @partial with @CompletedName\n const completedText = `@${selectedMention} `;\n input.value = completedText + value.substring(atMatch[0].length);\n input.selectionStart = input.selectionEnd = completedText.length;\n }\n\n hideMentionAutocomplete();\n input.focus();\n}\n\n/**\n * Get the current @mention being typed (if any)\n */\nexport function getCurrentMentionQuery(): string | null {\n const input = elements.messageInput;\n const value = input.value;\n const cursorPos = input.selectionStart;\n\n // Check if cursor is within an @mention at the start\n const atMatch = value.match(/^@(\\S*)/);\n if (atMatch && cursorPos <= atMatch[0].length) {\n return atMatch[1]; // Return the text after @\n }\n\n return null;\n}\n", "/**\n * Dashboard Application Entry Point\n */\n\nimport { subscribe } from './state.js';\nimport { connect, sendMessage } from './websocket.js';\nimport {\n initElements,\n getElements,\n updateConnectionStatus,\n renderAgents,\n renderMessages,\n selectChannel,\n updateOnlineCount,\n openCommandPalette,\n closeCommandPalette,\n filterPaletteResults,\n handlePaletteKeydown,\n initPaletteChannels,\n closeThreadPanel,\n renderThreadMessages,\n showMentionAutocomplete,\n hideMentionAutocomplete,\n isMentionAutocompleteVisible,\n navigateMentionAutocomplete,\n completeMention,\n getCurrentMentionQuery,\n} from './components.js';\nimport { state } from './state.js';\n\n/**\n * Detect if we're viewing a project dashboard from bridge context\n */\nfunction detectProjectContext(): { projectId: string | null; fromBridge: boolean } {\n const pathname = window.location.pathname;\n const match = pathname.match(/^\\/project\\/([^/]+)$/);\n\n if (match) {\n return { projectId: decodeURIComponent(match[1]), fromBridge: true };\n }\n\n return { projectId: null, fromBridge: false };\n}\n\n/**\n * Update the UI for project context (when accessed from bridge)\n */\nasync function setupProjectContext(projectId: string): Promise {\n // Update workspace name to show project\n const workspaceName = document.querySelector('.workspace-name');\n if (workspaceName) {\n // Fetch project info\n try {\n const response = await fetch(`/api/project/${encodeURIComponent(projectId)}`);\n if (response.ok) {\n const project = await response.json();\n const nameSpan = workspaceName.querySelector(':not(.status-dot)');\n if (nameSpan && nameSpan.nodeType === Node.TEXT_NODE) {\n nameSpan.textContent = project.name || projectId;\n } else {\n // Replace text content after status-dot\n const textNodes = Array.from(workspaceName.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);\n textNodes.forEach(n => n.textContent = '');\n workspaceName.appendChild(document.createTextNode(' ' + (project.name || projectId)));\n }\n }\n } catch {\n // Fallback - just show project ID\n }\n }\n\n // Update bridge nav link to show \"Back to Bridge\" with back arrow\n const bridgeLinkText = document.getElementById('bridge-link-text');\n const bridgeNavLink = document.getElementById('bridge-nav-link');\n if (bridgeLinkText) {\n bridgeLinkText.textContent = '\u2190 Back to Bridge';\n }\n if (bridgeNavLink) {\n bridgeNavLink.classList.add('back-to-bridge');\n }\n\n // Add a subtle indicator that we're in project view\n document.body.classList.add('project-view');\n}\n\n/**\n * Initialize the dashboard application\n */\nexport function initApp(): void {\n const elements = initElements();\n\n // Check if we're in project context (from bridge)\n const { projectId, fromBridge } = detectProjectContext();\n if (fromBridge && projectId) {\n setupProjectContext(projectId);\n }\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderAgents();\n renderMessages();\n updateOnlineCount();\n });\n\n // Set up event listeners\n setupEventListeners(elements);\n\n // Connect to WebSocket\n connect();\n}\n\n/**\n * Set up all event listeners\n */\nfunction setupEventListeners(elements: ReturnType): void {\n // Channel clicks\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.addEventListener('click', () => {\n const channel = item.dataset.channel;\n if (channel) {\n selectChannel(channel);\n }\n });\n });\n\n // Send button\n elements.sendBtn.addEventListener('click', handleSend);\n\n // Keyboard shortcuts for composer\n elements.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n // Handle mention autocomplete keys first\n if (isMentionAutocompleteVisible()) {\n if (e.key === 'Tab' || e.key === 'Enter') {\n e.preventDefault();\n completeMention();\n return;\n }\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n navigateMentionAutocomplete('up');\n return;\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n navigateMentionAutocomplete('down');\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n hideMentionAutocomplete();\n return;\n }\n }\n\n // Enter to send (Slack-style), Shift+Enter for newline\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Auto-resize textarea and handle @-mention autocomplete\n elements.messageInput.addEventListener('input', () => {\n elements.messageInput.style.height = 'auto';\n elements.messageInput.style.height =\n Math.min(elements.messageInput.scrollHeight, 200) + 'px';\n\n // Check for @-mention at start of input\n const query = getCurrentMentionQuery();\n if (query !== null) {\n showMentionAutocomplete(query);\n } else {\n hideMentionAutocomplete();\n }\n });\n\n // Hide mention autocomplete when input loses focus (with delay to allow clicks)\n elements.messageInput.addEventListener('blur', () => {\n setTimeout(() => {\n hideMentionAutocomplete();\n }, 150);\n });\n\n // Bold button - wrap selected text with ** or insert **bold**\n elements.boldBtn.addEventListener('click', () => {\n const input = elements.messageInput;\n const start = input.selectionStart;\n const end = input.selectionEnd;\n const text = input.value;\n\n if (start === end) {\n // No selection - insert **bold** placeholder\n const before = text.substring(0, start);\n const after = text.substring(end);\n input.value = before + '**bold**' + after;\n input.selectionStart = start + 2;\n input.selectionEnd = start + 6;\n } else {\n // Wrap selection with **\n const before = text.substring(0, start);\n const selected = text.substring(start, end);\n const after = text.substring(end);\n input.value = before + '**' + selected + '**' + after;\n input.selectionStart = start;\n input.selectionEnd = end + 4;\n }\n input.focus();\n });\n\n // Emoji button - insert common emojis via simple picker\n elements.emojiBtn.addEventListener('click', () => {\n const emojis = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2705', '\u274C', '\uD83C\uDF89', '\uD83D\uDD25', '\uD83D\uDCA1', '\u26A0\uFE0F', '\uD83D\uDCDD', '\uD83D\uDE80'];\n const emoji = emojis[Math.floor(Math.random() * emojis.length)];\n const input = elements.messageInput;\n const start = input.selectionStart;\n const text = input.value;\n input.value = text.substring(0, start) + emoji + text.substring(start);\n input.selectionStart = input.selectionEnd = start + emoji.length;\n input.focus();\n });\n\n // Command palette\n elements.searchTrigger.addEventListener('click', openCommandPalette);\n\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.commandPaletteOverlay.classList.contains('visible')) {\n closeCommandPalette();\n } else {\n openCommandPalette();\n }\n }\n\n if (e.key === 'Escape') {\n closeCommandPalette();\n }\n });\n\n elements.commandPaletteOverlay.addEventListener('click', (e: MouseEvent) => {\n if (e.target === elements.commandPaletteOverlay) {\n closeCommandPalette();\n }\n });\n\n elements.paletteSearch.addEventListener('input', (e: Event) => {\n const target = e.target as HTMLInputElement;\n filterPaletteResults(target.value);\n });\n\n elements.paletteSearch.addEventListener('keydown', handlePaletteKeydown);\n\n // Command execution\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n item.addEventListener('click', () => {\n const command = item.dataset.command;\n\n if (command === 'bridge') {\n // Navigate to bridge view\n window.location.href = '/bridge';\n } else if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n\n closeCommandPalette();\n });\n });\n\n // Add Cmd/Ctrl+B shortcut for bridge navigation\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'b') {\n e.preventDefault();\n window.location.href = '/bridge';\n }\n });\n\n // Initialize palette channel click handlers\n initPaletteChannels();\n\n // Thread panel close button\n elements.threadPanelClose.addEventListener('click', closeThreadPanel);\n\n // Thread panel send button\n elements.threadSendBtn.addEventListener('click', handleThreadSend);\n\n // Thread message input keyboard shortcuts (Slack-style)\n elements.threadMessageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleThreadSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Close thread panel on Escape\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape' && elements.threadPanelOverlay.classList.contains('visible')) {\n closeThreadPanel();\n }\n });\n}\n\n/**\n * Parse @mention from message text\n * Formats: \"@AgentName message\" or \"@* message\" for broadcast\n * Returns { to, message } or null if no valid mention found\n */\nfunction parseMention(text: string): { to: string; message: string } | null {\n const trimmed = text.trim();\n\n // Match @mention at the start of the message\n // @* for broadcast, @AgentName for direct message\n const match = trimmed.match(/^@(\\*|[^\\s]+)\\s+(.+)$/s);\n\n if (!match) {\n return null;\n }\n\n return {\n to: match[1],\n message: match[2].trim(),\n };\n}\n\n/**\n * Handle send button click\n */\nasync function handleSend(): Promise {\n const elements = getElements();\n const rawMessage = elements.messageInput.value.trim();\n\n if (!rawMessage) {\n return;\n }\n\n // Parse @mention from the message\n const parsed = parseMention(rawMessage);\n\n if (!parsed) {\n alert('Message must start with @recipient (e.g., \"@Lead hello\" or \"@* broadcast\")');\n return;\n }\n\n const { to, message } = parsed;\n\n elements.sendBtn.disabled = true;\n\n const result = await sendMessage(to, message);\n\n if (result.success) {\n elements.messageInput.value = '';\n elements.messageInput.style.height = 'auto';\n } else {\n alert(result.error);\n }\n\n elements.sendBtn.disabled = false;\n}\n\n/**\n * Handle thread panel send button click\n */\nasync function handleThreadSend(): Promise {\n const elements = getElements();\n const message = elements.threadMessageInput.value.trim();\n const threadId = state.currentThread;\n\n if (!message || !threadId) {\n return;\n }\n\n // For thread replies, send to broadcast or use original recipient\n // For now, send as broadcast with thread ID\n elements.threadSendBtn.disabled = true;\n\n const result = await sendMessage('*', message, threadId);\n\n if (result.success) {\n elements.threadMessageInput.value = '';\n // Re-render thread messages to show the new message\n renderThreadMessages(threadId);\n } else {\n alert(result.error);\n }\n\n elements.threadSendBtn.disabled = false;\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initApp);\n } else {\n initApp();\n }\n}\n"], + "mappings": "AASO,IAAMA,EAAkB,CAC7B,OAAQ,CAAC,EACT,SAAU,CAAC,EACX,eAAgB,UAChB,cAAe,KACf,YAAa,GACb,GAAI,KACJ,kBAAmB,CACrB,EAMMC,EAA6B,CAAC,EAK7B,SAASC,EAAUC,EAAqC,CAC7D,OAAAF,EAAU,KAAKE,CAAQ,EAChB,IAAM,CACX,IAAMC,EAAQH,EAAU,QAAQE,CAAQ,EACpCC,EAAQ,IACVH,EAAU,OAAOG,EAAO,CAAC,CAE7B,CACF,CAKA,SAASC,GAAwB,CAC/BJ,EAAU,QAASE,GAAaA,EAAS,CAAC,CAC5C,CAKO,SAASG,EAAUC,EAAuB,CAC/CP,EAAM,OAASO,EACfF,EAAgB,CAClB,CAKO,SAASG,EAAYC,EAA2B,CACrDT,EAAM,SAAWS,EACjBJ,EAAgB,CAClB,CAKO,SAASK,EAAkBC,EAA4B,CAC5DX,EAAM,eAAiBW,EACvBN,EAAgB,CAClB,CAKO,SAASO,EAAoBC,EAA0B,CAC5Db,EAAM,YAAca,EAChBA,IACFb,EAAM,kBAAoB,GAE5BK,EAAgB,CAClB,CAKO,SAASS,GAAmC,CACjDd,EAAM,mBACR,CAKO,SAASe,EAAaC,EAA4B,CACvDhB,EAAM,GAAKgB,CACb,CAKO,SAASC,GAAiC,CAC/C,GAAM,CAAE,SAAAR,EAAU,eAAAS,CAAe,EAAIlB,EAErC,OAAIkB,IAAmB,UACdT,EAIFA,EAAS,OACbU,GAAMA,EAAE,OAASD,GAAkBC,EAAE,KAAOD,CAC/C,CACF,CAKO,SAASE,EAAiBC,EAA6B,CAC5DrB,EAAM,cAAgBqB,CACxB,CAKO,SAASC,EAAkBC,EAA6B,CAC7D,OAAOvB,EAAM,SAAS,OAAQmB,GAAMA,EAAE,SAAWI,CAAQ,CAC3D,CAKO,SAASC,EAAoBD,EAA0B,CAC5D,OAAOvB,EAAM,SAAS,OAAQmB,GAAMA,EAAE,SAAWI,CAAQ,EAAE,MAC7D,CCjHA,IAAIE,EAAkC,KAY/B,SAASC,GAAgB,CAC9B,IAAMC,EAAW,OAAO,SAAS,WAAa,SAAW,OAAS,MAC5DC,EAAK,IAAI,UAAU,GAAGD,CAAQ,KAAK,OAAO,SAAS,IAAI,KAAK,EAElEC,EAAG,OAAS,IAAY,CACtBC,EAAoB,EAAI,CAC1B,EAEAD,EAAG,QAAU,IAAY,CACvBC,EAAoB,EAAK,EAEzB,IAAMC,EAAQ,KAAK,IAAI,IAAO,KAAK,IAAI,EAAGC,EAAM,iBAAiB,EAAG,GAAK,EACzEC,EAA2B,EAC3B,WAAWN,EAASI,CAAK,CAC3B,EAEAF,EAAG,QAAWK,GAAgB,CAC5B,QAAQ,MAAM,mBAAoBA,CAAK,CACzC,EAEAL,EAAG,UAAaM,GAA8B,CAC5C,GAAI,CACF,IAAMC,EAAsB,KAAK,MAAMD,EAAM,IAAc,EAC3DE,GAAWD,CAAI,CACjB,OAASE,EAAG,CACV,QAAQ,MAAM,2BAA4BA,CAAC,CAC7C,CACF,EAEAC,EAAaV,CAAE,CACjB,CAKA,SAASQ,GAAWD,EAA2B,CAC7C,QAAQ,IAAI,sBAAuB,CAAE,WAAYA,EAAK,QAAQ,OAAQ,aAAcA,EAAK,UAAU,MAAO,CAAC,EAEvGA,EAAK,SACP,QAAQ,IAAI,uBAAwBA,EAAK,OAAO,IAAII,GAAKA,EAAE,IAAI,CAAC,EAChEC,EAAUL,EAAK,MAAM,GAGnBA,EAAK,UACPM,EAAYN,EAAK,QAAQ,EAGvBO,GACFA,EAAYP,CAAI,CAEpB,CAKA,eAAsBQ,EACpBC,EACAC,EACAC,EAC+C,CAC/C,GAAI,CACF,IAAMC,EAAyD,CAAE,GAAAH,EAAI,QAAAC,CAAQ,EACzEC,IACFC,EAAK,OAASD,GAGhB,IAAME,EAAW,MAAM,MAAM,YAAa,CACxC,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAUD,CAAI,CAC3B,CAAC,EAEKE,EAAS,MAAMD,EAAS,KAAK,EAEnC,OAAIA,EAAS,IAAMC,EAAO,QACjB,CAAE,QAAS,EAAK,EAEhB,CAAE,QAAS,GAAO,MAAOA,EAAO,OAAS,wBAAyB,CAE7E,MAAc,CACZ,MAAO,CAAE,QAAS,GAAO,MAAO,wCAAyC,CAC3E,CACF,CCpGO,SAASC,EAAcC,EAAuC,CACnE,GAAI,CAACA,EAAU,MAAO,GACtB,IAAMC,EAAK,KAAK,MAAMD,CAAQ,EAC9B,OAAI,OAAO,MAAMC,CAAE,EAAU,GACtB,KAAK,IAAI,EAAIA,EAAK,GAC3B,CAKO,SAASC,EAAWC,EAAkC,CAC3D,GAAI,CAACA,EAAM,MAAO,GAClB,IAAMC,EAAM,SAAS,cAAc,KAAK,EACxC,OAAAA,EAAI,YAAcD,EACXC,EAAI,SACb,CAKO,SAASC,EAAWC,EAA2B,CAEpD,OADa,IAAI,KAAKA,CAAS,EACnB,mBAAmB,CAAC,EAAG,CAAE,KAAM,UAAW,OAAQ,SAAU,CAAC,CAC3E,CAKO,SAASC,EAAWD,EAA2B,CACpD,IAAME,EAAO,IAAI,KAAKF,CAAS,EACzBG,EAAQ,IAAI,KACZC,EAAY,IAAI,KAAKD,CAAK,EAGhC,OAFAC,EAAU,QAAQA,EAAU,QAAQ,EAAI,CAAC,EAErCF,EAAK,aAAa,IAAMC,EAAM,aAAa,EACtC,QACED,EAAK,aAAa,IAAME,EAAU,aAAa,EACjD,YAEAF,EAAK,mBAAmB,CAAC,EAAG,CACjC,QAAS,OACT,MAAO,OACP,IAAK,SACP,CAAC,CAEL,CAKO,SAASG,EAAeC,EAAsB,CACnD,IAAMC,EAAS,CACb,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,UACA,SACF,EACIC,EAAO,EACX,QAASC,EAAI,EAAGA,EAAIH,EAAK,OAAQG,IAC/BD,EAAOF,EAAK,WAAWG,CAAC,IAAMD,GAAQ,GAAKA,GAE7C,OAAOD,EAAO,KAAK,IAAIC,CAAI,EAAID,EAAO,MAAM,CAC9C,CAKO,SAASG,EAAYJ,EAAsB,CAChD,OAAOA,EAAK,UAAU,EAAG,CAAC,EAAE,YAAY,CAC1C,CAKO,SAASK,EAAkBC,EAAqC,CACrE,GAAI,CAACA,EAAS,MAAO,GAErB,IAAIC,EAAUjB,EAAWgB,CAAO,EAGhC,OAAAC,EAAUA,EAAQ,QAAQ,oBAAqB,eAAe,EAC9DA,EAAUA,EAAQ,QAAQ,aAAc,iBAAiB,EAGzDA,EAAUA,EAAQ,QAAQ,MAAO,MAAM,EAEhCA,CACT,CCvFA,IAAIC,EACAC,EAAuB,GAKpB,SAASC,GAA4B,CAC1C,OAAAF,EAAW,CACT,cAAe,SAAS,eAAe,gBAAgB,EACvD,aAAc,SAAS,eAAe,eAAe,EACrD,WAAY,SAAS,eAAe,aAAa,EACjD,aAAc,SAAS,eAAe,eAAe,EACrD,mBAAoB,SAAS,eAAe,sBAAsB,EAClE,aAAc,SAAS,eAAe,eAAe,EACrD,YAAa,SAAS,eAAe,cAAc,EACnD,aAAc,SAAS,eAAe,eAAe,EACrD,QAAS,SAAS,eAAe,UAAU,EAC3C,QAAS,SAAS,eAAe,UAAU,EAC3C,SAAU,SAAS,eAAe,WAAW,EAC7C,cAAe,SAAS,eAAe,gBAAgB,EACvD,sBAAuB,SAAS,eAAe,yBAAyB,EACxE,cAAe,SAAS,eAAe,gBAAgB,EACvD,eAAgB,SAAS,eAAe,iBAAiB,EACzD,uBAAwB,SAAS,eAAe,0BAA0B,EAC1E,qBAAsB,SAAS,eAAe,wBAAwB,EACtE,uBAAwB,SAAS,eAAe,0BAA0B,EAC1E,gBAAiB,SAAS,eAAe,kBAAkB,EAC3D,mBAAoB,SAAS,eAAe,sBAAsB,EAClE,cAAe,SAAS,eAAe,iBAAiB,EACxD,iBAAkB,SAAS,eAAe,oBAAoB,EAC9D,eAAgB,SAAS,eAAe,iBAAiB,EACzD,mBAAoB,SAAS,eAAe,sBAAsB,EAClE,cAAe,SAAS,eAAe,iBAAiB,EACxD,oBAAqB,SAAS,eAAe,sBAAsB,EACnE,wBAAyB,SAAS,eAAe,2BAA2B,CAC9E,EACOA,CACT,CAKO,SAASG,GAA2B,CACzC,OAAOH,CACT,CAKO,SAASI,GAA+B,CACzCC,EAAM,YACRL,EAAS,cAAc,UAAU,OAAO,SAAS,EAEjDA,EAAS,cAAc,UAAU,IAAI,SAAS,CAElD,CAKO,SAASM,GAAqB,CACnC,QAAQ,IAAI,oCAAqCD,EAAM,OAAO,OAAQA,EAAM,OAAO,IAAIE,GAAKA,EAAE,IAAI,CAAC,EACnG,IAAMC,EAAOH,EAAM,OAChB,IAAKI,GAAU,CAEd,IAAMC,EADSC,EAAcF,EAAM,UAAYA,EAAM,UAAU,EAChC,SAAW,GACpCG,EAAWP,EAAM,iBAAmBI,EAAM,KAC1CI,EAAsBJ,EAAM,eAAiB,kBAAoB,GAEvE,MAAO;AAAA,gCACmBG,EAAW,SAAW,EAAE,IAAIC,CAAmB,iBAAiBC,EAAWL,EAAM,IAAI,CAAC;AAAA,uDAC/DM,EAAeN,EAAM,IAAI,CAAC;AAAA,YACrEO,EAAYP,EAAM,IAAI,CAAC;AAAA,4CACSC,CAAa;AAAA;AAAA,qCAEpBI,EAAWL,EAAM,IAAI,CAAC;AAAA,UACjDA,EAAM,eAAiB,mDAAqD,EAAE;AAAA;AAAA,KAGpF,CAAC,EACA,KAAK,EAAE,EAEVT,EAAS,WAAW,UAClBQ,GACA,uGAGFR,EAAS,WAAW,iBAA8B,2BAA2B,EAAE,QAASiB,GAAS,CAC/FA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAMC,EAAYD,EAAK,QAAQ,MAC3BC,GACFC,EAAcD,CAAS,CAE3B,CAAC,CACH,CAAC,EAGDE,GAAoB,CACtB,CAKO,SAASC,GAAuB,CACrC,IAAMC,EAAWC,EAAoB,EAErC,GAAID,EAAS,SAAW,EAAG,CACzBtB,EAAS,aAAa,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,YAQ1BK,EAAM,iBAAmB,UACrB,2CACA,iBAAiBA,EAAM,cAAc,mBAC3C;AAAA;AAAA;AAAA,MAIN,MACF,CAEA,IAAIG,EAAO,GACPgB,EAA0B,KAE9BF,EAAS,QAASG,GAAQ,CACxB,IAAMC,EAAU,IAAI,KAAKD,EAAI,SAAS,EAAE,aAAa,EAGjDC,IAAYF,IACdhB,GAAQ;AAAA;AAAA,4CAE8BmB,EAAWF,EAAI,SAAS,CAAC;AAAA;AAAA,QAG/DD,EAAWE,GAGb,IAAME,EAAcH,EAAI,KAAO,IACzBI,EAAcd,EAAeU,EAAI,IAAI,EACrCK,EAAaC,EAAoBN,EAAI,EAAE,EAIvCO,EAAmBJ,EACrB,YACAH,EAAI,QACF,+BAA+BX,EAAWW,EAAI,OAAO,CAAC,WAAWX,EAAWW,EAAI,EAAE,CAAC,GACnF,IAAIX,EAAWW,EAAI,EAAE,CAAC,GAE5BjB,GAAQ;AAAA,4BACgBoB,EAAc,YAAc,EAAE,cAAcd,EAAWW,EAAI,EAAE,CAAC;AAAA,yDACjCI,CAAW;AAAA,YACxDb,EAAYS,EAAI,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA,4CAIWX,EAAWW,EAAI,IAAI,CAAC;AAAA;AAAA,4CAEzBO,CAAgB;AAAA;AAAA,8CAETC,EAAWR,EAAI,SAAS,CAAC;AAAA;AAAA,sCAEjCS,EAAkBT,EAAI,OAAO,CAAC;AAAA,YAExDA,EAAI,OACA;AAAA,yDACyCX,EAAWW,EAAI,MAAM,CAAC;AAAA;AAAA;AAAA;AAAA,wBAIvDX,EAAWW,EAAI,MAAM,CAAC;AAAA;AAAA,YAG9B,EACN;AAAA,YAEEK,EAAa,EACT;AAAA,0DAC0ChB,EAAWW,EAAI,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA,gBAI5DK,CAAU,IAAIA,IAAe,EAAI,QAAU,SAAS;AAAA;AAAA,YAGpD,EACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAmBR,CAAC,EAED9B,EAAS,aAAa,UAAYQ,EAKlC2B,GAAqB,CACvB,CAKO,SAAShB,EAAciB,EAA4B,CACxDC,EAAkBD,CAAO,EAGzBpC,EAAS,aAAa,iBAA8B,eAAe,EAAE,QAASiB,GAAS,CACrFA,EAAK,UAAU,OAAO,SAAUA,EAAK,QAAQ,UAAYmB,CAAO,CAClE,CAAC,EACDpC,EAAS,WAAW,iBAA8B,eAAe,EAAE,QAASiB,GAAS,CACnFA,EAAK,UAAU,OAAO,SAAUA,EAAK,QAAQ,QAAUmB,CAAO,CAChE,CAAC,EAGD,IAAME,EAAW,SAAS,cAAc,8BAA8B,EACtE,GAAIF,IAAY,UACdpC,EAAS,mBAAmB,UAAY,UACxCA,EAAS,aAAa,YAAc,2BAChCsC,IAAUA,EAAS,YAAc,SAChC,CACLtC,EAAS,mBAAmB,UAAYc,EAAWsB,CAAO,EAC1D,IAAM3B,EAAQJ,EAAM,OAAO,KAAME,GAAMA,EAAE,OAAS6B,CAAO,EACzDpC,EAAS,aAAa,YAAcS,GAAO,QAAU,kBACjD6B,IAAUA,EAAS,YAAc,IACvC,CAGAtC,EAAS,aAAa,YACpBoC,IAAY,UACR,6CACA,IAAIA,CAAO,wBAGjBf,EAAe,CACjB,CAKO,SAASkB,GAA0B,CACxC,IAAMC,EAASnC,EAAM,OAAO,OAAQE,GAAMI,EAAcJ,EAAE,UAAYA,EAAE,UAAU,CAAC,EAAE,OACrFP,EAAS,YAAY,YAAc,GAAGwC,CAAM,SAC9C,CAKO,SAASpB,IAA4B,CAC1C,IAAMZ,EAAOH,EAAM,OAChB,IAAKI,GAAU,CACd,IAAM+B,EAAS7B,EAAcF,EAAM,UAAYA,EAAM,UAAU,EAC/D,MAAO;AAAA,mDACsCK,EAAWL,EAAM,IAAI,CAAC;AAAA;AAAA,yDAEhBM,EAAeN,EAAM,IAAI,CAAC;AAAA,cACrEO,EAAYP,EAAM,IAAI,CAAC;AAAA,8CACS+B,EAAS,SAAW,EAAE;AAAA;AAAA;AAAA;AAAA,4CAIxB1B,EAAWL,EAAM,IAAI,CAAC;AAAA,+CACnB+B,EAAS,SAAW,SAAS;AAAA;AAAA;AAAA,KAIxE,CAAC,EACA,KAAK,EAAE,EAEJC,EAAUzC,EAAS,qBACXyC,EAAQ,iBAAiB,eAAe,EAChD,QAASxB,GAASA,EAAK,OAAO,CAAC,EACrCwB,EAAQ,mBAAmB,YAAajC,CAAI,EAG5CiC,EAAQ,iBAA8B,gCAAgC,EAAE,QAASxB,GAAS,CACxFA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAMC,EAAYD,EAAK,QAAQ,UAC3BC,IACFC,EAAcD,CAAS,EACvBwB,EAAoB,EAExB,CAAC,CACH,CAAC,CACH,CAKO,SAASC,IAA4B,CAC1C3C,EAAS,uBACN,iBAA8B,kCAAkC,EAChE,QAASiB,GAAS,CACjBA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAM2B,EAAc3B,EAAK,QAAQ,YAC7B2B,IACFzB,EAAcyB,CAAW,EACzBF,EAAoB,EAExB,CAAC,CACH,CAAC,CACL,CAKO,SAASG,GAA2B,CACzC7C,EAAS,sBAAsB,UAAU,IAAI,SAAS,EACtDA,EAAS,cAAc,MAAQ,GAC/BA,EAAS,cAAc,MAAM,EAC7BC,EAAuB,GACvB6C,EAAqB,EAAE,CACzB,CAKO,SAASC,IAAwC,CAItD,OAHiB,MAAM,KACrB/C,EAAS,eAAe,iBAA8B,eAAe,CACvE,EACgB,OAAQiB,GAASA,EAAK,MAAM,UAAY,MAAM,CAChE,CAKO,SAAS+B,GAA+B,CAC7C,IAAMC,EAAQF,GAAuB,EAMrC,GAHAE,EAAM,QAAShC,GAASA,EAAK,UAAU,OAAO,UAAU,CAAC,EAGrDhB,GAAwB,GAAKA,EAAuBgD,EAAM,OAAQ,CACpE,IAAMC,EAAeD,EAAMhD,CAAoB,EAC/CiD,EAAa,UAAU,IAAI,UAAU,EAGrCA,EAAa,eAAe,CAAE,MAAO,UAAW,SAAU,QAAS,CAAC,CACtE,CACF,CAKO,SAASC,GAAqBC,EAAwB,CAC3D,IAAMH,EAAQF,GAAuB,EAErC,GAAIE,EAAM,SAAW,EAErB,OAAQG,EAAE,IAAK,CACb,IAAK,YACHA,EAAE,eAAe,EACjBnD,EAAuBA,EAAuBgD,EAAM,OAAS,EACzDhD,EAAuB,EACvB,EACJ+C,EAAuB,EACvB,MAEF,IAAK,UACHI,EAAE,eAAe,EACjBnD,EAAuBA,EAAuB,EAC1CA,EAAuB,EACvBgD,EAAM,OAAS,EACnBD,EAAuB,EACvB,MAEF,IAAK,QACHI,EAAE,eAAe,EACbnD,GAAwB,GAAKA,EAAuBgD,EAAM,QAC5DI,GAAmBJ,EAAMhD,CAAoB,CAAC,EAEhD,KACJ,CACF,CAKO,SAASoD,GAAmBpC,EAAyB,CAE1D,IAAMqC,EAAUrC,EAAK,QAAQ,QAC7B,GAAIqC,EAAS,CACPA,IAAY,aAEdtD,EAAS,aAAa,MAAQ,MAC9BA,EAAS,aAAa,MAAM,GACnBsD,IAAY,UACrBtD,EAAS,aAAa,UAAY,IAEpC0C,EAAoB,EACpB,MACF,CAGA,IAAMN,EAAUnB,EAAK,QAAQ,YAC7B,GAAImB,EAAS,CACXjB,EAAciB,CAAO,EACrBM,EAAoB,EACpB,MACF,CAGA,IAAMjC,EAAQQ,EAAK,QAAQ,UAC3B,GAAIR,EAAO,CACTU,EAAcV,CAAK,EACnBiC,EAAoB,EACpB,MACF,CAGA,IAAMa,EAAYtC,EAAK,QAAQ,YAC/B,GAAIsC,EAAW,CAEb,IAAMC,EAAYxD,EAAS,aAAa,cAAc,aAAauD,CAAS,IAAI,EAC5EC,IACFA,EAAU,eAAe,CAAE,SAAU,SAAU,MAAO,QAAS,CAAC,EAChEA,EAAU,UAAU,IAAI,aAAa,EACrC,WAAW,IAAMA,EAAU,UAAU,OAAO,aAAa,EAAG,GAAI,GAElEd,EAAoB,EACpB,MACF,CACF,CAKO,SAASA,GAA4B,CAC1C1C,EAAS,sBAAsB,UAAU,OAAO,SAAS,CAC3D,CAKO,SAAS8C,EAAqBW,EAAqB,CACxD,IAAMC,EAAID,EAAM,YAAY,EA0B5B,GAvBAxD,EAAuB,GAGvB,SAAS,iBAA8B,6BAA6B,EAAE,QAASgB,GAAS,CAEtF,IAAM0C,EADU1C,EAAK,cAAc,qBAAqB,GACjC,aAAa,YAAY,GAAK,GACrDA,EAAK,MAAM,QAAU0C,EAAM,SAASD,CAAC,EAAI,OAAS,MACpD,CAAC,EAGD,SAAS,iBAA8B,kCAAkC,EAAE,QAASzC,GAAS,CAE3F,IAAM0C,EADU1C,EAAK,cAAc,qBAAqB,GACjC,aAAa,YAAY,GAAK,GACrDA,EAAK,MAAM,QAAU0C,EAAM,SAASD,CAAC,EAAI,OAAS,MACpD,CAAC,EAGD,SAAS,iBAA8B,gCAAgC,EAAE,QAASzC,GAAS,CACzF,IAAM2C,EAAO3C,EAAK,QAAQ,WAAW,YAAY,GAAK,GACtDA,EAAK,MAAM,QAAU2C,EAAK,SAASF,CAAC,EAAI,OAAS,MACnD,CAAC,EAGGA,EAAE,QAAU,EAAG,CACjB,IAAMG,EAAUxD,EAAM,SAAS,OAAQyD,GAAMA,EAAE,QAAQ,YAAY,EAAE,SAASJ,CAAC,CAAC,EAAE,MAAM,EAAG,CAAC,EAE5F,GAAIG,EAAQ,OAAS,EAAG,CACtB7D,EAAS,uBAAuB,MAAM,QAAU,QAChD,IAAMiD,EAAQY,EACX,IACEC,GAAM;AAAA,uDACsChD,EAAWgD,EAAE,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8CAOzBhD,EAAWgD,EAAE,IAAI,CAAC;AAAA,iDACfhD,EAAWgD,EAAE,QAAQ,UAAU,EAAG,EAAE,CAAC,CAAC,GAAGA,EAAE,QAAQ,OAAS,GAAK,MAAQ,EAAE;AAAA;AAAA;AAAA,OAIpH,EACC,KAAK,EAAE,EAEY9D,EAAS,uBAAuB,iBAAiB,eAAe,EACxE,QAASiB,GAASA,EAAK,OAAO,CAAC,EAC7CjB,EAAS,uBAAuB,mBAAmB,YAAaiD,CAAK,CACvE,MACEjD,EAAS,uBAAuB,MAAM,QAAU,MAEpD,MACEA,EAAS,uBAAuB,MAAM,QAAU,MAEpD,CAKO,SAAS+D,EAAgBC,EAAwB,CACtDC,EAAiBD,CAAQ,EACzBhE,EAAS,cAAc,YAAcgE,EACrChE,EAAS,mBAAmB,UAAU,IAAI,SAAS,EACnDA,EAAS,mBAAmB,MAAQ,GACpCkE,EAAqBF,CAAQ,EAC7BhE,EAAS,mBAAmB,MAAM,CACpC,CAKO,SAASmE,GAAyB,CACvCF,EAAiB,IAAI,EACrBjE,EAAS,mBAAmB,UAAU,OAAO,SAAS,CACxD,CAKO,SAASkE,EAAqBF,EAAwB,CAC3D,IAAMI,EAAWC,EAAkBL,CAAQ,EAE3C,GAAII,EAAS,SAAW,EAAG,CACzBpE,EAAS,eAAe,UAAY;AAAA;AAAA;AAAA;AAAA;AAAA,MAMpC,MACF,CAEA,IAAMQ,EAAO4D,EACV,IAAK3C,GAAQ;AAAA;AAAA;AAAA,kEAGgDV,EAAeU,EAAI,IAAI,CAAC;AAAA,cAC5ET,EAAYS,EAAI,IAAI,CAAC;AAAA;AAAA,gDAEaX,EAAWW,EAAI,IAAI,CAAC;AAAA,8CACtBQ,EAAWR,EAAI,SAAS,CAAC;AAAA;AAAA,2CAE5BS,EAAkBT,EAAI,OAAO,CAAC;AAAA;AAAA,KAEpE,EACA,KAAK,EAAE,EAEVzB,EAAS,eAAe,UAAYQ,EAGpCR,EAAS,eAAe,UAAYA,EAAS,eAAe,YAC9D,CAKO,SAASmC,IAA6B,CAE3CnC,EAAS,aAAa,iBAA8B,mBAAmB,EAAE,QAASsE,GAAO,CACvFA,EAAG,MAAM,OAAS,UAClBA,EAAG,iBAAiB,QAAU,GAAM,CAClC,EAAE,gBAAgB,EAClB,IAAMN,EAAWM,EAAG,QAAQ,OACxBN,GACFD,EAAgBC,CAAQ,CAE5B,CAAC,CACH,CAAC,EAGDhE,EAAS,aAAa,iBAA8B,oBAAoB,EAAE,QAASsE,GAAO,CACxFA,EAAG,iBAAiB,QAAU,GAAM,CAClC,EAAE,gBAAgB,EAClB,IAAMN,EAAWM,EAAG,QAAQ,OACxBN,GACFD,EAAgBC,CAAQ,CAE5B,CAAC,CACH,CAAC,EAGDhE,EAAS,aAAa,iBAA8B,0CAA0C,EAAE,QAASsE,GAAO,CAC9GA,EAAG,iBAAiB,QAAU,GAAM,CAClC,EAAE,gBAAgB,EAClB,IAAMf,EAAYe,EAAG,QAAQ,UAAU,GAAG,aAAa,SAAS,EAC5Df,GAEFQ,EAAgBR,CAAS,CAE7B,CAAC,CACH,CAAC,CACH,CAKA,IAAIgB,EAAuB,EACvBC,EAA6C,CAAC,EAK3C,SAASC,GAAwBC,EAAsB,CAC5D,IAAMC,EAAcD,EAAO,YAAY,EAGvCF,EAAwBnE,EAAM,OAAO,OAAOI,GAC1CA,EAAM,KAAK,YAAY,EAAE,SAASkE,CAAW,CAC/C,EAGAJ,EAAuB,EAGvB,IAAI/D,EAAO,IAGP,IAAI,SAASmE,CAAW,GAAK,WAAW,SAASA,CAAW,GAAK,MAAM,SAASA,CAAW,GAAK,YAAY,SAASA,CAAW,KAClInE,GAAQ;AAAA,8CACkC+D,IAAyB,GAAKC,EAAsB,SAAW,EAAI,WAAa,EAAE;AAAA;AAAA;AAAA;AAAA;AAAA,OAS9HA,EAAsB,QAAQ,CAAC/D,EAAOmE,IAAU,CAE9CpE,GAAQ;AAAA,8CADWoE,IAAUL,EAE0B,WAAa,EAAE,mBAAmBzD,EAAWL,EAAM,IAAI,CAAC;AAAA,uDAC5DM,EAAeN,EAAM,IAAI,CAAC;AAAA,YACrEO,EAAYP,EAAM,IAAI,CAAC;AAAA;AAAA,mDAEgBK,EAAWL,EAAM,IAAI,CAAC;AAAA,kDACvBK,EAAWL,EAAM,MAAQ,OAAO,CAAC;AAAA;AAAA,KAGjF,CAAC,EAEGD,IAAS,KACXA,EAAO,sHAGTR,EAAS,wBAAwB,UAAYQ,EAC7CR,EAAS,oBAAoB,UAAU,IAAI,SAAS,EAGpDA,EAAS,wBAAwB,iBAA8B,0CAA0C,EAAE,QAASiB,GAAS,CAC3HA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAM4D,EAAU5D,EAAK,QAAQ,QACzB4D,GACFC,EAAgBD,CAAO,CAE3B,CAAC,CACH,CAAC,CACH,CAKO,SAASE,GAAgC,CAC9C/E,EAAS,oBAAoB,UAAU,OAAO,SAAS,EACvDwE,EAAwB,CAAC,EACzBD,EAAuB,CACzB,CAKO,SAASS,IAAwC,CACtD,OAAOhF,EAAS,oBAAoB,UAAU,SAAS,SAAS,CAClE,CAKO,SAASiF,EAA4BC,EAAgC,CAC1E,IAAMjC,EAAQjD,EAAS,wBAAwB,iBAA8B,0CAA0C,EACnHiD,EAAM,SAAW,IAGrBA,EAAMsB,CAAoB,GAAG,UAAU,OAAO,UAAU,EAGpDW,IAAc,OAChBX,GAAwBA,EAAuB,GAAKtB,EAAM,OAE1DsB,GAAwBA,EAAuB,EAAItB,EAAM,QAAUA,EAAM,OAI3EA,EAAMsB,CAAoB,GAAG,UAAU,IAAI,UAAU,EACrDtB,EAAMsB,CAAoB,GAAG,eAAe,CAAE,MAAO,SAAU,CAAC,EAClE,CAKO,SAASO,EAAgBD,EAAwB,CACtD,IAAM5B,EAAQjD,EAAS,wBAAwB,iBAA8B,0CAA0C,EAGnHmF,EAAkBN,EAKtB,GAJI,CAACM,GAAmBlC,EAAM,OAAS,IACrCkC,EAAkBlC,EAAMsB,CAAoB,GAAG,QAAQ,SAGrD,CAACY,EAAiB,CACpBJ,EAAwB,EACxB,MACF,CAGA,IAAMK,EAAQpF,EAAS,aACjBqF,EAAQD,EAAM,MAGdE,EAAUD,EAAM,MAAM,OAAO,EACnC,GAAIC,EAAS,CAEX,IAAMC,EAAgB,IAAIJ,CAAe,IACzCC,EAAM,MAAQG,EAAgBF,EAAM,UAAUC,EAAQ,CAAC,EAAE,MAAM,EAC/DF,EAAM,eAAiBA,EAAM,aAAeG,EAAc,MAC5D,CAEAR,EAAwB,EACxBK,EAAM,MAAM,CACd,CAKO,SAASI,IAAwC,CACtD,IAAMJ,EAAQpF,EAAS,aACjBqF,EAAQD,EAAM,MACdK,EAAYL,EAAM,eAGlBE,EAAUD,EAAM,MAAM,SAAS,EACrC,OAAIC,GAAWG,GAAaH,EAAQ,CAAC,EAAE,OAC9BA,EAAQ,CAAC,EAGX,IACT,CC7uBA,SAASI,IAA0E,CAEjF,IAAMC,EADW,OAAO,SAAS,SACV,MAAM,sBAAsB,EAEnD,OAAIA,EACK,CAAE,UAAW,mBAAmBA,EAAM,CAAC,CAAC,EAAG,WAAY,EAAK,EAG9D,CAAE,UAAW,KAAM,WAAY,EAAM,CAC9C,CAKA,eAAeC,GAAoBC,EAAkC,CAEnE,IAAMC,EAAgB,SAAS,cAAc,iBAAiB,EAC9D,GAAIA,EAEF,GAAI,CACF,IAAMC,EAAW,MAAM,MAAM,gBAAgB,mBAAmBF,CAAS,CAAC,EAAE,EAC5E,GAAIE,EAAS,GAAI,CACf,IAAMC,EAAU,MAAMD,EAAS,KAAK,EAC9BE,EAAWH,EAAc,cAAc,mBAAmB,EAC5DG,GAAYA,EAAS,WAAa,KAAK,UACzCA,EAAS,YAAcD,EAAQ,MAAQH,GAGrB,MAAM,KAAKC,EAAc,UAAU,EAAE,OAAOI,GAAKA,EAAE,WAAa,KAAK,SAAS,EACtF,QAAQA,GAAKA,EAAE,YAAc,EAAE,EACzCJ,EAAc,YAAY,SAAS,eAAe,KAAOE,EAAQ,MAAQH,EAAU,CAAC,EAExF,CACF,MAAQ,CAER,CAIF,IAAMM,EAAiB,SAAS,eAAe,kBAAkB,EAC3DC,EAAgB,SAAS,eAAe,iBAAiB,EAC3DD,IACFA,EAAe,YAAc,yBAE3BC,GACFA,EAAc,UAAU,IAAI,gBAAgB,EAI9C,SAAS,KAAK,UAAU,IAAI,cAAc,CAC5C,CAKO,SAASC,IAAgB,CAC9B,IAAMC,EAAWC,EAAa,EAGxB,CAAE,UAAAV,EAAW,WAAAW,CAAW,EAAId,GAAqB,EACnDc,GAAcX,GAChBD,GAAoBC,CAAS,EAI/BY,EAAU,IAAM,CACdC,EAAuB,EACvBC,EAAa,EACbC,EAAe,EACfC,EAAkB,CACpB,CAAC,EAGDC,GAAoBR,CAAQ,EAG5BS,EAAQ,CACV,CAKA,SAASD,GAAoBR,EAAgD,CAE3EA,EAAS,aAAa,iBAA8B,eAAe,EAAE,QAASU,GAAS,CACrFA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAMC,EAAUD,EAAK,QAAQ,QACzBC,GACFC,EAAcD,CAAO,CAEzB,CAAC,CACH,CAAC,EAGDX,EAAS,QAAQ,iBAAiB,QAASa,EAAU,EAGrDb,EAAS,aAAa,iBAAiB,UAAY,GAAqB,CAEtE,GAAIc,GAA6B,EAAG,CAClC,GAAI,EAAE,MAAQ,OAAS,EAAE,MAAQ,QAAS,CACxC,EAAE,eAAe,EACjBC,EAAgB,EAChB,MACF,CACA,GAAI,EAAE,MAAQ,UAAW,CACvB,EAAE,eAAe,EACjBC,EAA4B,IAAI,EAChC,MACF,CACA,GAAI,EAAE,MAAQ,YAAa,CACzB,EAAE,eAAe,EACjBA,EAA4B,MAAM,EAClC,MACF,CACA,GAAI,EAAE,MAAQ,SAAU,CACtB,EAAE,eAAe,EACjBC,EAAwB,EACxB,MACF,CACF,CAGI,EAAE,MAAQ,SAAW,CAAC,EAAE,WAC1B,EAAE,eAAe,EACjBJ,GAAW,EAGf,CAAC,EAGDb,EAAS,aAAa,iBAAiB,QAAS,IAAM,CACpDA,EAAS,aAAa,MAAM,OAAS,OACrCA,EAAS,aAAa,MAAM,OAC1B,KAAK,IAAIA,EAAS,aAAa,aAAc,GAAG,EAAI,KAGtD,IAAMkB,EAAQC,GAAuB,EACjCD,IAAU,KACZE,GAAwBF,CAAK,EAE7BD,EAAwB,CAE5B,CAAC,EAGDjB,EAAS,aAAa,iBAAiB,OAAQ,IAAM,CACnD,WAAW,IAAM,CACfiB,EAAwB,CAC1B,EAAG,GAAG,CACR,CAAC,EAGDjB,EAAS,QAAQ,iBAAiB,QAAS,IAAM,CAC/C,IAAMqB,EAAQrB,EAAS,aACjBsB,EAAQD,EAAM,eACdE,EAAMF,EAAM,aACZG,EAAOH,EAAM,MAEnB,GAAIC,IAAUC,EAAK,CAEjB,IAAME,EAASD,EAAK,UAAU,EAAGF,CAAK,EAChCI,EAAQF,EAAK,UAAUD,CAAG,EAChCF,EAAM,MAAQI,EAAS,WAAaC,EACpCL,EAAM,eAAiBC,EAAQ,EAC/BD,EAAM,aAAeC,EAAQ,CAC/B,KAAO,CAEL,IAAMG,EAASD,EAAK,UAAU,EAAGF,CAAK,EAChCK,EAAWH,EAAK,UAAUF,EAAOC,CAAG,EACpCG,EAAQF,EAAK,UAAUD,CAAG,EAChCF,EAAM,MAAQI,EAAS,KAAOE,EAAW,KAAOD,EAChDL,EAAM,eAAiBC,EACvBD,EAAM,aAAeE,EAAM,CAC7B,CACAF,EAAM,MAAM,CACd,CAAC,EAGDrB,EAAS,SAAS,iBAAiB,QAAS,IAAM,CAChD,IAAM4B,EAAS,CAAC,YAAM,YAAM,SAAK,SAAK,YAAM,YAAM,YAAM,eAAM,YAAM,WAAI,EAClEC,EAAQD,EAAO,KAAK,MAAM,KAAK,OAAO,EAAIA,EAAO,MAAM,CAAC,EACxDP,EAAQrB,EAAS,aACjBsB,EAAQD,EAAM,eACdG,EAAOH,EAAM,MACnBA,EAAM,MAAQG,EAAK,UAAU,EAAGF,CAAK,EAAIO,EAAQL,EAAK,UAAUF,CAAK,EACrED,EAAM,eAAiBA,EAAM,aAAeC,EAAQO,EAAM,OAC1DR,EAAM,MAAM,CACd,CAAC,EAGDrB,EAAS,cAAc,iBAAiB,QAAS8B,CAAkB,EAEnE,SAAS,iBAAiB,UAAY,GAAqB,EACpD,EAAE,SAAW,EAAE,UAAY,EAAE,MAAQ,MACxC,EAAE,eAAe,EACb9B,EAAS,sBAAsB,UAAU,SAAS,SAAS,EAC7D+B,EAAoB,EAEpBD,EAAmB,GAInB,EAAE,MAAQ,UACZC,EAAoB,CAExB,CAAC,EAED/B,EAAS,sBAAsB,iBAAiB,QAAU,GAAkB,CACtE,EAAE,SAAWA,EAAS,uBACxB+B,EAAoB,CAExB,CAAC,EAED/B,EAAS,cAAc,iBAAiB,QAAU,GAAa,CAC7D,IAAMgC,EAAS,EAAE,OACjBC,EAAqBD,EAAO,KAAK,CACnC,CAAC,EAEDhC,EAAS,cAAc,iBAAiB,UAAWkC,EAAoB,EAGvE,SAAS,iBAA8B,6BAA6B,EAAE,QAASxB,GAAS,CACtFA,EAAK,iBAAiB,QAAS,IAAM,CACnC,IAAMyB,EAAUzB,EAAK,QAAQ,QAEzByB,IAAY,SAEd,OAAO,SAAS,KAAO,UACdA,IAAY,aAErBnC,EAAS,aAAa,MAAQ,MAC9BA,EAAS,aAAa,MAAM,GACnBmC,IAAY,UACrBnC,EAAS,aAAa,UAAY,IAGpC+B,EAAoB,CACtB,CAAC,CACH,CAAC,EAGD,SAAS,iBAAiB,UAAY,GAAqB,EACpD,EAAE,SAAW,EAAE,UAAY,EAAE,MAAQ,MACxC,EAAE,eAAe,EACjB,OAAO,SAAS,KAAO,UAE3B,CAAC,EAGDK,GAAoB,EAGpBpC,EAAS,iBAAiB,iBAAiB,QAASqC,CAAgB,EAGpErC,EAAS,cAAc,iBAAiB,QAASsC,EAAgB,EAGjEtC,EAAS,mBAAmB,iBAAiB,UAAY,GAAqB,CACxE,EAAE,MAAQ,SAAW,CAAC,EAAE,WAC1B,EAAE,eAAe,EACjBsC,GAAiB,EAGrB,CAAC,EAGD,SAAS,iBAAiB,UAAY,GAAqB,CACrD,EAAE,MAAQ,UAAYtC,EAAS,mBAAmB,UAAU,SAAS,SAAS,GAChFqC,EAAiB,CAErB,CAAC,CACH,CAOA,SAASE,GAAaf,EAAsD,CAK1E,IAAMnC,EAJUmC,EAAK,KAAK,EAIJ,MAAM,wBAAwB,EAEpD,OAAKnC,EAIE,CACL,GAAIA,EAAM,CAAC,EACX,QAASA,EAAM,CAAC,EAAE,KAAK,CACzB,EANS,IAOX,CAKA,eAAewB,IAA4B,CACzC,IAAMb,EAAWwC,EAAY,EACvBC,EAAazC,EAAS,aAAa,MAAM,KAAK,EAEpD,GAAI,CAACyC,EACH,OAIF,IAAMC,EAASH,GAAaE,CAAU,EAEtC,GAAI,CAACC,EAAQ,CACX,MAAM,4EAA4E,EAClF,MACF,CAEA,GAAM,CAAE,GAAAC,EAAI,QAAAC,CAAQ,EAAIF,EAExB1C,EAAS,QAAQ,SAAW,GAE5B,IAAM6C,EAAS,MAAMC,EAAYH,EAAIC,CAAO,EAExCC,EAAO,SACT7C,EAAS,aAAa,MAAQ,GAC9BA,EAAS,aAAa,MAAM,OAAS,QAErC,MAAM6C,EAAO,KAAK,EAGpB7C,EAAS,QAAQ,SAAW,EAC9B,CAKA,eAAesC,IAAkC,CAC/C,IAAMtC,EAAWwC,EAAY,EACvBI,EAAU5C,EAAS,mBAAmB,MAAM,KAAK,EACjD+C,EAAWC,EAAM,cAEvB,GAAI,CAACJ,GAAW,CAACG,EACf,OAKF/C,EAAS,cAAc,SAAW,GAElC,IAAM6C,EAAS,MAAMC,EAAY,IAAKF,EAASG,CAAQ,EAEnDF,EAAO,SACT7C,EAAS,mBAAmB,MAAQ,GAEpCiD,EAAqBF,CAAQ,GAE7B,MAAMF,EAAO,KAAK,EAGpB7C,EAAS,cAAc,SAAW,EACpC,CAGI,OAAO,SAAa,MAClB,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoBD,EAAO,EAErDA,GAAQ", + "names": ["state", "listeners", "subscribe", "listener", "index", "notifyListeners", "setAgents", "agents", "setMessages", "messages", "setCurrentChannel", "channel", "setConnectionStatus", "connected", "incrementReconnectAttempts", "setWebSocket", "ws", "getFilteredMessages", "currentChannel", "m", "setCurrentThread", "thread", "getThreadMessages", "threadId", "getThreadReplyCount", "dataHandler", "connect", "protocol", "ws", "setConnectionStatus", "delay", "state", "incrementReconnectAttempts", "error", "event", "data", "handleData", "e", "setWebSocket", "a", "setAgents", "setMessages", "dataHandler", "sendMessage", "to", "message", "thread", "body", "response", "result", "isAgentOnline", "lastSeen", "ts", "escapeHtml", "text", "div", "formatTime", "timestamp", "formatDate", "date", "today", "yesterday", "getAvatarColor", "name", "colors", "hash", "i", "getInitials", "formatMessageBody", "content", "escaped", "elements", "paletteSelectedIndex", "initElements", "getElements", "updateConnectionStatus", "state", "renderAgents", "a", "html", "agent", "presenceClass", "isAgentOnline", "isActive", "needsAttentionClass", "escapeHtml", "getAvatarColor", "getInitials", "item", "agentName", "selectChannel", "updatePaletteAgents", "renderMessages", "filtered", "getFilteredMessages", "lastDate", "msg", "msgDate", "formatDate", "isBroadcast", "avatarColor", "replyCount", "getThreadReplyCount", "recipientDisplay", "formatTime", "formatMessageBody", "attachThreadHandlers", "channel", "setCurrentChannel", "prefixEl", "updateOnlineCount", "online", "section", "closeCommandPalette", "initPaletteChannels", "channelName", "openCommandPalette", "filterPaletteResults", "getVisiblePaletteItems", "updatePaletteSelection", "items", "selectedItem", "handlePaletteKeydown", "e", "executePaletteItem", "command", "messageId", "messageEl", "query", "q", "title", "name", "matches", "m", "openThreadPanel", "threadId", "setCurrentThread", "renderThreadMessages", "closeThreadPanel", "messages", "getThreadMessages", "el", "mentionSelectedIndex", "mentionFilteredAgents", "showMentionAutocomplete", "filter", "filterLower", "index", "mention", "completeMention", "hideMentionAutocomplete", "isMentionAutocompleteVisible", "navigateMentionAutocomplete", "direction", "selectedMention", "input", "value", "atMatch", "completedText", "getCurrentMentionQuery", "cursorPos", "detectProjectContext", "match", "setupProjectContext", "projectId", "workspaceName", "response", "project", "nameSpan", "n", "bridgeLinkText", "bridgeNavLink", "initApp", "elements", "initElements", "fromBridge", "subscribe", "updateConnectionStatus", "renderAgents", "renderMessages", "updateOnlineCount", "setupEventListeners", "connect", "item", "channel", "selectChannel", "handleSend", "isMentionAutocompleteVisible", "completeMention", "navigateMentionAutocomplete", "hideMentionAutocomplete", "query", "getCurrentMentionQuery", "showMentionAutocomplete", "input", "start", "end", "text", "before", "after", "selected", "emojis", "emoji", "openCommandPalette", "closeCommandPalette", "target", "filterPaletteResults", "handlePaletteKeydown", "command", "initPaletteChannels", "closeThreadPanel", "handleThreadSend", "parseMention", "getElements", "rawMessage", "parsed", "to", "message", "result", "sendMessage", "threadId", "state", "renderThreadMessages"] } diff --git a/src/dashboard/public/js/bridge.js b/src/dashboard/public/js/bridge.js new file mode 100644 index 000000000..d5ddc5e38 --- /dev/null +++ b/src/dashboard/public/js/bridge.js @@ -0,0 +1,117 @@ +var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,connectionStart:null},g=[];function S(e){return g.push(e),()=>{let s=g.indexOf(e);s>-1&&g.splice(s,1)}}function u(){g.forEach(e=>{try{e()}catch(s){console.error("[bridge-state] Listener error:",s)}})}function L(e){c.projects=e,u()}function b(e){c.messages=e,u()}function E(e){c.selectedProjectId=e,u()}function v(e){c.isConnected=e,e&&!c.connectionStart&&(c.connectionStart=Date.now()),u()}function j(e){c.ws=e}function h(){let e=[];return c.projects.forEach(s=>{(s.agents||[]).forEach(n=>{e.push({name:n.name,projectId:s.id,projectName:s.name||s.id,cli:n.cli})})}),e}function M(){return c.projects.filter(e=>e.connected)}function y(e){return c.projects.find(s=>s.id===e)}function x(){if(!c.connectionStart)return"--";let e=Date.now()-c.connectionStart,s=Math.floor(e/1e3);if(s<60)return`${s}s`;let n=Math.floor(s/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function r(e){if(!e)return"";let s=document.createElement("div");return s.textContent=e,s.innerHTML}function P(e){return new Date(e).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}var t;function C(){return{statusDot:document.getElementById("status-dot"),projectList:document.getElementById("project-list"),cardsGrid:document.getElementById("cards-grid"),emptyState:document.getElementById("empty-state"),messagesList:document.getElementById("messages-list"),searchBar:document.getElementById("search-bar"),paletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteProjectsSection:document.getElementById("palette-projects-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),channelName:document.getElementById("channel-name"),statAgents:document.getElementById("stat-agents"),statMessages:document.getElementById("stat-messages"),composerProject:document.getElementById("composer-project"),composerAgent:document.getElementById("composer-agent"),composerMessage:document.getElementById("composer-message"),composerSend:document.getElementById("composer-send"),composerStatus:document.getElementById("composer-status"),uptime:document.getElementById("uptime")}}function k(){t.statusDot.classList.toggle("offline",!c.isConnected)}function A(){let{projects:e,selectedProjectId:s}=c;if(!e||e.length===0){t.projectList.innerHTML='
  • No projects
  • ',document.getElementById("project-count").textContent="0";return}document.getElementById("project-count").textContent=String(e.length),t.projectList.innerHTML=e.map(n=>` +
  • + + ${r(n.name||n.id)} + +
  • + `).join("")}function H(){let{projects:e,selectedProjectId:s}=c;if(!e||e.length===0){t.cardsGrid.innerHTML="",t.cardsGrid.appendChild(t.emptyState),t.emptyState.style.display="flex";return}t.emptyState.style.display="none",t.cardsGrid.innerHTML=e.map(n=>{let o=n.agents||[],i=o.length>0?o.map(m=>` +
    + + ${r(m.name)} + ${r(m.cli||"")} +
    + `).join(""):'
    No agents connected
    ',a=s===n.id;return` +
    +
    +
    +
    + + + +
    +
    +
    ${r(n.name||n.id)}
    +
    ${r(n.path||"")}
    +
    +
    +
    + + ${n.connected?"Online":n.reconnecting?"Reconnecting...":"Offline"} +
    +
    + +
    +
    + Agents + ${o.length} active +
    +
    + ${i} +
    +
    + +
    + + +
    +
    + `}).join("")}function D(){let{messages:e}=c;if(!e||e.length===0){t.messagesList.innerHTML='

    No messages yet

    ';return}t.messagesList.innerHTML=e.slice(-50).reverse().map(s=>` +
    +
    + ${r(s.sourceProject||"local")} + ${r(s.from)} + \u2192 + ${r(s.to||"*")} + ${P(s.timestamp)} +
    +
    ${r(s.body||s.content||"")}
    +
    + `).join("")}function O(){let e=h();t.statAgents.textContent=String(e.length),t.statMessages.textContent=String(c.messages.length)}function N(){let e=M(),s=t.composerProject.value;t.composerProject.innerHTML=''+e.map(n=>``).join(""),s&&e.some(n=>n.id===s)?t.composerProject.value=s:c.selectedProjectId&&e.some(n=>n.id===c.selectedProjectId)&&(t.composerProject.value=c.selectedProjectId,p())}function p(){let e=t.composerProject.value;if(!e){t.composerAgent.innerHTML='',t.composerAgent.disabled=!0,t.composerMessage.disabled=!0,t.composerSend.disabled=!0;return}let s=t.composerAgent.value,o=y(e)?.agents||[];t.composerAgent.innerHTML=''+o.map(i=>``).join(""),t.composerAgent.disabled=!1,s&&["*","lead",...o.map(a=>a.name)].includes(s)&&(t.composerAgent.value=s)}function l(){let e=!!t.composerProject.value,s=!!t.composerAgent.value,n=t.composerMessage.value.trim().length>0;t.composerMessage.disabled=!e||!s,t.composerSend.disabled=!e||!s||!n}async function w(){let e=t.composerProject.value,s=t.composerAgent.value,n=t.composerMessage.value.trim();if(!(!e||!s||!n)){t.composerSend.disabled=!0,t.composerStatus.textContent="Sending...",t.composerStatus.className="composer-status";try{let o=await fetch("/api/bridge/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({projectId:e,to:s,message:n})}),i=await o.json();if(o.ok&&i.success)t.composerStatus.textContent="Message sent!",t.composerStatus.className="composer-status success",t.composerMessage.value="",setTimeout(()=>{t.composerStatus.textContent="",t.composerStatus.className="composer-status"},2e3);else throw new Error(i.error||"Failed to send")}catch(o){t.composerStatus.textContent=o.message||"Failed to send message",t.composerStatus.className="composer-status error"}l()}}function R(){let{selectedProjectId:e}=c;if(e){let s=y(e);s&&(t.channelName.innerHTML=` + \u2190 All Projects + ${r(s.name||s.id)} + `)}else t.channelName.textContent="All Projects"}function f(e){E(e),e&&(t.composerProject.value=e,p(),l()),document.querySelectorAll(".project-card").forEach(s=>{s.classList.toggle("selected",s.dataset.projectId===e)})}function $(){t.paletteOverlay.classList.add("visible"),t.paletteSearch.value="",t.paletteSearch.focus(),B()}function d(){t.paletteOverlay.classList.remove("visible")}function B(){let e=t.paletteSearch.value.toLowerCase(),{projects:s}=c,n=e?s.filter(a=>(a.name||a.id).toLowerCase().includes(e)):s;n.length>0?t.paletteProjectsSection.innerHTML=` +
    Open Project Dashboard
    + ${n.map(a=>` +
    +
    + + + + + +
    +
    +
    ${r(a.name||a.id)}
    +
    ${a.connected?"Online":"Offline"} \xB7 ${(a.agents||[]).length} agents \xB7 Click to open dashboard
    +
    +
    + \u23CE +
    +
    + `).join("")} + `:t.paletteProjectsSection.innerHTML='
    Open Project Dashboard
    ';let o=h(),i=e?o.filter(a=>a.name.toLowerCase().includes(e)):o;i.length>0?t.paletteAgentsSection.innerHTML=` +
    Message Agent
    + ${i.map(a=>` +
    +
    + + + + +
    +
    +
    ${r(a.name)}
    +
    ${r(a.projectName)} \xB7 ${r(a.cli||"unknown")}
    +
    +
    + `).join("")} + `:t.paletteAgentsSection.innerHTML='
    Message Agent
    '}function U(){t.searchBar.addEventListener("click",$),t.paletteOverlay.addEventListener("click",e=>{e.target===t.paletteOverlay&&d()}),t.paletteSearch.addEventListener("input",B),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&e.key==="k"&&(e.preventDefault(),t.paletteOverlay.classList.contains("visible")?d():$()),e.key==="Escape"&&t.paletteOverlay.classList.contains("visible")&&d()}),t.paletteResults.addEventListener("click",e=>{let s=e.target.closest(".palette-item");if(!s)return;let n=s.dataset.command,o=s.dataset.project,i=s.dataset.agent,a=s.dataset.action;if(n==="broadcast")d(),t.composerMessage.focus(),t.composerStatus.textContent="Select a project and agent to send a message";else if(n==="refresh")d(),location.reload();else if(n==="go-dashboard")d(),window.location.href="/";else if(a==="open-dashboard"&&o)d(),window.location.href=`/project/${encodeURIComponent(o)}`;else if(i&&o)d(),t.composerProject.value=o,p(),setTimeout(()=>{t.composerAgent.value=i,l(),t.composerMessage.focus()},50);else if(o){d(),f(o);let m=document.querySelector(`.project-card[data-project-id="${o}"]`);m&&m.scrollIntoView({behavior:"smooth",block:"center"})}}),t.cardsGrid.addEventListener("click",e=>{let s=e.target,n=s.closest("[data-open-dashboard]");if(n){e.stopPropagation();let a=n.dataset.openDashboard;a&&(window.location.href=`/project/${encodeURIComponent(a)}`);return}let o=s.closest("[data-message-lead]");if(o&&!o.disabled){e.stopPropagation();let a=o.dataset.messageLead;a&&(t.composerProject.value=a,p(),setTimeout(()=>{t.composerAgent.value="lead",l(),t.composerMessage.focus()},50));return}let i=s.closest(".project-card");i&&f(i.dataset.projectId||null)}),t.projectList.addEventListener("click",e=>{let s=e.target,n=s.closest(".project-dashboard-btn");if(n){e.stopPropagation();let i=n.dataset.dashboardProject;i&&(window.location.href=`/project/${encodeURIComponent(i)}`);return}let o=s.closest(".project-item");o&&f(o.dataset.projectId||null)}),t.channelName.addEventListener("click",e=>{let s=e.target;(s.id==="back-to-all"||s.classList.contains("back-link"))&&f(null)}),t.composerProject.addEventListener("change",()=>{p(),l()}),t.composerAgent.addEventListener("change",l),t.composerMessage.addEventListener("input",l),t.composerSend.addEventListener("click",w),t.composerMessage.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!t.composerSend.disabled&&(e.preventDefault(),w())})}function T(){let e=window.location.protocol==="https:"?"wss:":"ws:",s=new WebSocket(`${e}//${window.location.host}/ws/bridge`);s.onopen=()=>{v(!0),j(s)},s.onclose=()=>{v(!1),j(null),setTimeout(T,3e3)},s.onerror=()=>{v(!1)},s.onmessage=n=>{try{let o=JSON.parse(n.data);L(o.projects||[]),b(o.messages||[])}catch(o){console.error("[bridge] Parse error:",o)}}}function I(){t=C(),S(()=>{k(),A(),H(),D(),O(),N(),R(),t.composerProject.value&&(p(),l())}),U(),T(),setInterval(()=>{t.uptime.textContent=`Uptime: ${x()}`},1e3)}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I):I());export{I as initBridgeApp}; +//# sourceMappingURL=bridge.js.map diff --git a/src/dashboard/public/js/bridge.js.map b/src/dashboard/public/js/bridge.js.map new file mode 100644 index 000000000..006f06bfa --- /dev/null +++ b/src/dashboard/public/js/bridge.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../../frontend/bridge/state.ts", "../../frontend/utils.ts", "../../frontend/bridge/app.ts"], + "sourcesContent": ["/**\n * Bridge State Management\n * Centralized state for the bridge dashboard\n */\n\nimport type { BridgeState, Project, BridgeMessage } from './types.js';\n\ntype StateListener = () => void;\n\n// Bridge state\nexport const state: BridgeState = {\n projects: [],\n messages: [],\n selectedProjectId: null,\n isConnected: false,\n ws: null,\n connectionStart: null,\n};\n\n// State subscribers\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => {\n try {\n listener();\n } catch (err) {\n console.error('[bridge-state] Listener error:', err);\n }\n });\n}\n\n/**\n * Update projects\n */\nexport function setProjects(projects: Project[]): void {\n state.projects = projects;\n notifyListeners();\n}\n\n/**\n * Update messages\n */\nexport function setMessages(messages: BridgeMessage[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set selected project\n */\nexport function setSelectedProject(projectId: string | null): void {\n state.selectedProjectId = projectId;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnected(connected: boolean): void {\n state.isConnected = connected;\n if (connected && !state.connectionStart) {\n state.connectionStart = Date.now();\n }\n notifyListeners();\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Get all agents across all projects\n */\nexport function getAllAgents(): { name: string; projectId: string; projectName: string; cli?: string }[] {\n const agents: { name: string; projectId: string; projectName: string; cli?: string }[] = [];\n\n state.projects.forEach((project) => {\n (project.agents || []).forEach((agent) => {\n agents.push({\n name: agent.name,\n projectId: project.id,\n projectName: project.name || project.id,\n cli: agent.cli,\n });\n });\n });\n\n return agents;\n}\n\n/**\n * Get connected projects\n */\nexport function getConnectedProjects(): Project[] {\n return state.projects.filter((p) => p.connected);\n}\n\n/**\n * Get project by ID\n */\nexport function getProject(projectId: string): Project | undefined {\n return state.projects.find((p) => p.id === projectId);\n}\n\n/**\n * Get uptime formatted string\n */\nexport function getUptimeString(): string {\n if (!state.connectionStart) return '--';\n\n const ms = Date.now() - state.connectionStart;\n const seconds = Math.floor(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m`;\n\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${minutes % 60}m`;\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Bridge Dashboard Application Entry Point\n */\n\nimport { subscribe, state, setProjects, setMessages, setConnected, setWebSocket, setSelectedProject, getUptimeString, getConnectedProjects, getAllAgents, getProject } from './state.js';\nimport type { BridgeDOMElements, Project, BridgeMessage } from './types.js';\nimport { escapeHtml, formatTime, getAvatarColor, getInitials } from '../utils.js';\n\nlet elements: BridgeDOMElements;\n\n/**\n * Initialize DOM element references\n */\nfunction initElements(): BridgeDOMElements {\n return {\n statusDot: document.getElementById('status-dot')!,\n projectList: document.getElementById('project-list')!,\n cardsGrid: document.getElementById('cards-grid')!,\n emptyState: document.getElementById('empty-state')!,\n messagesList: document.getElementById('messages-list')!,\n searchBar: document.getElementById('search-bar')!,\n paletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteProjectsSection: document.getElementById('palette-projects-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n channelName: document.getElementById('channel-name')!,\n statAgents: document.getElementById('stat-agents')!,\n statMessages: document.getElementById('stat-messages')!,\n composerProject: document.getElementById('composer-project') as HTMLSelectElement,\n composerAgent: document.getElementById('composer-agent') as HTMLSelectElement,\n composerMessage: document.getElementById('composer-message') as HTMLInputElement,\n composerSend: document.getElementById('composer-send') as HTMLButtonElement,\n composerStatus: document.getElementById('composer-status')!,\n uptime: document.getElementById('uptime')!,\n };\n}\n\n/**\n * Update connection status indicator\n */\nfunction updateConnectionStatus(): void {\n elements.statusDot.classList.toggle('offline', !state.isConnected);\n}\n\n/**\n * Render sidebar projects list\n */\nfunction renderSidebarProjects(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.projectList.innerHTML = '
  • No projects
  • ';\n document.getElementById('project-count')!.textContent = '0';\n return;\n }\n\n document.getElementById('project-count')!.textContent = String(projects.length);\n\n elements.projectList.innerHTML = projects.map((p) => `\n
  • \n \n ${escapeHtml(p.name || p.id)}\n \n
  • \n `).join('');\n}\n\n/**\n * Render project cards grid\n */\nfunction renderProjectCards(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.cardsGrid.innerHTML = '';\n elements.cardsGrid.appendChild(elements.emptyState);\n elements.emptyState.style.display = 'flex';\n return;\n }\n\n elements.emptyState.style.display = 'none';\n\n elements.cardsGrid.innerHTML = projects.map((p) => {\n const agents = p.agents || [];\n const agentsHtml = agents.length > 0\n ? agents.map((a) => `\n
    \n \n ${escapeHtml(a.name)}\n ${escapeHtml(a.cli || '')}\n
    \n `).join('')\n : '
    No agents connected
    ';\n\n const isSelected = selectedProjectId === p.id;\n return `\n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${escapeHtml(p.path || '')}
    \n
    \n
    \n
    \n \n ${p.connected ? 'Online' : p.reconnecting ? 'Reconnecting...' : 'Offline'}\n
    \n
    \n\n
    \n
    \n Agents\n ${agents.length} active\n
    \n
    \n ${agentsHtml}\n
    \n
    \n\n
    \n \n \n
    \n
    \n `;\n }).join('');\n}\n\n/**\n * Render messages list\n */\nfunction renderMessages(): void {\n const { messages } = state;\n\n if (!messages || messages.length === 0) {\n elements.messagesList.innerHTML = '

    No messages yet

    ';\n return;\n }\n\n elements.messagesList.innerHTML = messages.slice(-50).reverse().map((m) => `\n
    \n
    \n ${escapeHtml(m.sourceProject || 'local')}\n ${escapeHtml(m.from)}\n \u2192\n ${escapeHtml(m.to || '*')}\n ${formatTime(m.timestamp)}\n
    \n
    ${escapeHtml(m.body || m.content || '')}
    \n
    \n `).join('');\n}\n\n/**\n * Update stats display\n */\nfunction updateStats(): void {\n const allAgents = getAllAgents();\n elements.statAgents.textContent = String(allAgents.length);\n elements.statMessages.textContent = String(state.messages.length);\n}\n\n/**\n * Update composer project options\n */\nfunction updateComposerProjects(): void {\n const connectedProjects = getConnectedProjects();\n const currentValue = elements.composerProject.value;\n\n elements.composerProject.innerHTML = '' +\n connectedProjects.map((p) =>\n ``\n ).join('');\n\n // Restore selection if still valid\n if (currentValue && connectedProjects.some((p) => p.id === currentValue)) {\n elements.composerProject.value = currentValue;\n } else if (state.selectedProjectId && connectedProjects.some((p) => p.id === state.selectedProjectId)) {\n elements.composerProject.value = state.selectedProjectId;\n updateComposerAgents();\n }\n}\n\n/**\n * Update composer agent options\n */\nfunction updateComposerAgents(): void {\n const projectId = elements.composerProject.value;\n if (!projectId) {\n elements.composerAgent.innerHTML = '';\n elements.composerAgent.disabled = true;\n elements.composerMessage.disabled = true;\n elements.composerSend.disabled = true;\n return;\n }\n\n const currentAgent = elements.composerAgent.value;\n const project = getProject(projectId);\n const agents = project?.agents || [];\n\n elements.composerAgent.innerHTML = '' +\n '' +\n '' +\n agents.map((a) =>\n ``\n ).join('');\n\n elements.composerAgent.disabled = false;\n\n // Restore agent selection if still valid\n if (currentAgent) {\n const validAgents = ['*', 'lead', ...agents.map((a) => a.name)];\n if (validAgents.includes(currentAgent)) {\n elements.composerAgent.value = currentAgent;\n }\n }\n}\n\n/**\n * Update composer state based on selections\n */\nfunction updateComposerState(): void {\n const hasProject = !!elements.composerProject.value;\n const hasAgent = !!elements.composerAgent.value;\n const hasMessage = elements.composerMessage.value.trim().length > 0;\n\n elements.composerMessage.disabled = !hasProject || !hasAgent;\n elements.composerSend.disabled = !hasProject || !hasAgent || !hasMessage;\n}\n\n/**\n * Send message via bridge API\n */\nasync function sendBridgeMessage(): Promise {\n const projectId = elements.composerProject.value;\n const to = elements.composerAgent.value;\n const message = elements.composerMessage.value.trim();\n\n if (!projectId || !to || !message) return;\n\n elements.composerSend.disabled = true;\n elements.composerStatus.textContent = 'Sending...';\n elements.composerStatus.className = 'composer-status';\n\n try {\n const response = await fetch('/api/bridge/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ projectId, to, message }),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n elements.composerStatus.textContent = 'Message sent!';\n elements.composerStatus.className = 'composer-status success';\n elements.composerMessage.value = '';\n setTimeout(() => {\n elements.composerStatus.textContent = '';\n elements.composerStatus.className = 'composer-status';\n }, 2000);\n } else {\n throw new Error(result.error || 'Failed to send');\n }\n } catch (err) {\n elements.composerStatus.textContent = (err as Error).message || 'Failed to send message';\n elements.composerStatus.className = 'composer-status error';\n }\n\n updateComposerState();\n}\n\n/**\n * Update header for project selection\n */\nfunction updateHeader(): void {\n const { selectedProjectId } = state;\n\n if (selectedProjectId) {\n const project = getProject(selectedProjectId);\n if (project) {\n elements.channelName.innerHTML = `\n \u2190 All Projects\n ${escapeHtml(project.name || project.id)}\n `;\n }\n } else {\n elements.channelName.textContent = 'All Projects';\n }\n}\n\n/**\n * Select a project\n */\nfunction selectProject(projectId: string | null): void {\n setSelectedProject(projectId);\n\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n updateComposerState();\n }\n\n // Update card selection visually\n document.querySelectorAll('.project-card').forEach((card) => {\n card.classList.toggle('selected', (card as HTMLElement).dataset.projectId === projectId);\n });\n}\n\n/**\n * Open command palette\n */\nfunction openPalette(): void {\n elements.paletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n updatePaletteResults();\n}\n\n/**\n * Close command palette\n */\nfunction closePalette(): void {\n elements.paletteOverlay.classList.remove('visible');\n}\n\n/**\n * Update palette search results\n */\nfunction updatePaletteResults(): void {\n const query = elements.paletteSearch.value.toLowerCase();\n const { projects } = state;\n\n // Update projects section\n const filteredProjects = query\n ? projects.filter((p) => (p.name || p.id).toLowerCase().includes(query))\n : projects;\n\n if (filteredProjects.length > 0) {\n elements.paletteProjectsSection.innerHTML = `\n
    Open Project Dashboard
    \n ${filteredProjects.map((p) => `\n
    \n
    \n \n \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${p.connected ? 'Online' : 'Offline'} \u00B7 ${(p.agents || []).length} agents \u00B7 Click to open dashboard
    \n
    \n
    \n \u23CE\n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteProjectsSection.innerHTML = '
    Open Project Dashboard
    ';\n }\n\n // Update agents section\n const allAgents = getAllAgents();\n const filteredAgents = query\n ? allAgents.filter((a) => a.name.toLowerCase().includes(query))\n : allAgents;\n\n if (filteredAgents.length > 0) {\n elements.paletteAgentsSection.innerHTML = `\n
    Message Agent
    \n ${filteredAgents.map((a) => `\n
    \n
    \n \n \n \n \n
    \n
    \n
    ${escapeHtml(a.name)}
    \n
    ${escapeHtml(a.projectName)} \u00B7 ${escapeHtml(a.cli || 'unknown')}
    \n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteAgentsSection.innerHTML = '
    Message Agent
    ';\n }\n}\n\n/**\n * Set up event listeners\n */\nfunction setupEventListeners(): void {\n // Search bar opens palette\n elements.searchBar.addEventListener('click', openPalette);\n\n // Palette overlay click closes\n elements.paletteOverlay.addEventListener('click', (e) => {\n if (e.target === elements.paletteOverlay) closePalette();\n });\n\n // Palette search filtering\n elements.paletteSearch.addEventListener('input', updatePaletteResults);\n\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => {\n // Cmd/Ctrl + K to open palette\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n } else {\n openPalette();\n }\n }\n // Escape to close\n if (e.key === 'Escape' && elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n }\n });\n\n // Palette item clicks\n elements.paletteResults.addEventListener('click', (e) => {\n const item = (e.target as HTMLElement).closest('.palette-item') as HTMLElement | null;\n if (!item) return;\n\n const command = item.dataset.command;\n const projectId = item.dataset.project;\n const agentName = item.dataset.agent;\n const action = item.dataset.action;\n\n if (command === 'broadcast') {\n closePalette();\n elements.composerMessage.focus();\n elements.composerStatus.textContent = 'Select a project and agent to send a message';\n } else if (command === 'refresh') {\n closePalette();\n location.reload();\n } else if (command === 'go-dashboard') {\n closePalette();\n window.location.href = '/';\n } else if (action === 'open-dashboard' && projectId) {\n closePalette();\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n } else if (agentName && projectId) {\n closePalette();\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = agentName;\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n } else if (projectId) {\n closePalette();\n selectProject(projectId);\n const card = document.querySelector(`.project-card[data-project-id=\"${projectId}\"]`);\n if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });\n }\n });\n\n // Project card clicks\n elements.cardsGrid.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Handle \"Open Dashboard\" button\n const dashboardBtn = target.closest('[data-open-dashboard]') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.openDashboard;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n // Handle \"Message Lead\" button\n const messageLeadBtn = target.closest('[data-message-lead]') as HTMLButtonElement | null;\n if (messageLeadBtn && !messageLeadBtn.disabled) {\n e.stopPropagation();\n const projectId = messageLeadBtn.dataset.messageLead;\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = 'lead';\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n }\n return;\n }\n\n const card = target.closest('.project-card') as HTMLElement | null;\n if (card) {\n selectProject(card.dataset.projectId || null);\n }\n });\n\n // Sidebar project clicks\n elements.projectList.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Check if dashboard button was clicked\n const dashboardBtn = target.closest('.project-dashboard-btn') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.dashboardProject;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n const item = target.closest('.project-item') as HTMLElement | null;\n if (item) {\n selectProject(item.dataset.projectId || null);\n }\n });\n\n // Header back link\n elements.channelName.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (target.id === 'back-to-all' || target.classList.contains('back-link')) {\n selectProject(null);\n }\n });\n\n // Composer events\n elements.composerProject.addEventListener('change', () => {\n updateComposerAgents();\n updateComposerState();\n });\n\n elements.composerAgent.addEventListener('change', updateComposerState);\n elements.composerMessage.addEventListener('input', updateComposerState);\n\n elements.composerSend.addEventListener('click', sendBridgeMessage);\n elements.composerMessage.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !e.shiftKey && !elements.composerSend.disabled) {\n e.preventDefault();\n sendBridgeMessage();\n }\n });\n}\n\n/**\n * Connect to WebSocket\n */\nfunction connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws/bridge`);\n\n ws.onopen = () => {\n setConnected(true);\n setWebSocket(ws);\n };\n\n ws.onclose = () => {\n setConnected(false);\n setWebSocket(null);\n setTimeout(connect, 3000);\n };\n\n ws.onerror = () => {\n setConnected(false);\n };\n\n ws.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n setProjects(data.projects || []);\n setMessages(data.messages || []);\n } catch (err) {\n console.error('[bridge] Parse error:', err);\n }\n };\n}\n\n/**\n * Initialize the bridge application\n */\nexport function initBridgeApp(): void {\n elements = initElements();\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderSidebarProjects();\n renderProjectCards();\n renderMessages();\n updateStats();\n updateComposerProjects();\n updateHeader();\n if (elements.composerProject.value) {\n updateComposerAgents();\n updateComposerState();\n }\n });\n\n // Set up event listeners\n setupEventListeners();\n\n // Connect to WebSocket\n connect();\n\n // Update uptime periodically\n setInterval(() => {\n elements.uptime.textContent = `Uptime: ${getUptimeString()}`;\n }, 1000);\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initBridgeApp);\n } else {\n initBridgeApp();\n }\n}\n"], + "mappings": "AAUO,IAAMA,EAAqB,CAChC,SAAU,CAAC,EACX,SAAU,CAAC,EACX,kBAAmB,KACnB,YAAa,GACb,GAAI,KACJ,gBAAiB,IACnB,EAGMC,EAA6B,CAAC,EAK7B,SAASC,EAAUC,EAAqC,CAC7D,OAAAF,EAAU,KAAKE,CAAQ,EAChB,IAAM,CACX,IAAMC,EAAQH,EAAU,QAAQE,CAAQ,EACpCC,EAAQ,IACVH,EAAU,OAAOG,EAAO,CAAC,CAE7B,CACF,CAKA,SAASC,GAAwB,CAC/BJ,EAAU,QAASE,GAAa,CAC9B,GAAI,CACFA,EAAS,CACX,OAASG,EAAK,CACZ,QAAQ,MAAM,iCAAkCA,CAAG,CACrD,CACF,CAAC,CACH,CAKO,SAASC,EAAYC,EAA2B,CACrDR,EAAM,SAAWQ,EACjBH,EAAgB,CAClB,CAKO,SAASI,EAAYC,EAAiC,CAC3DV,EAAM,SAAWU,EACjBL,EAAgB,CAClB,CAKO,SAASM,EAAmBC,EAAgC,CACjEZ,EAAM,kBAAoBY,EAC1BP,EAAgB,CAClB,CAKO,SAASQ,EAAaC,EAA0B,CACrDd,EAAM,YAAcc,EAChBA,GAAa,CAACd,EAAM,kBACtBA,EAAM,gBAAkB,KAAK,IAAI,GAEnCK,EAAgB,CAClB,CAKO,SAASU,EAAaC,EAA4B,CACvDhB,EAAM,GAAKgB,CACb,CAKO,SAASC,GAAyF,CACvG,IAAMC,EAAmF,CAAC,EAE1F,OAAAlB,EAAM,SAAS,QAASmB,GAAY,EACjCA,EAAQ,QAAU,CAAC,GAAG,QAASC,GAAU,CACxCF,EAAO,KAAK,CACV,KAAME,EAAM,KACZ,UAAWD,EAAQ,GACnB,YAAaA,EAAQ,MAAQA,EAAQ,GACrC,IAAKC,EAAM,GACb,CAAC,CACH,CAAC,CACH,CAAC,EAEMF,CACT,CAKO,SAASG,GAAkC,CAChD,OAAOrB,EAAM,SAAS,OAAQsB,GAAMA,EAAE,SAAS,CACjD,CAKO,SAASC,EAAWX,EAAwC,CACjE,OAAOZ,EAAM,SAAS,KAAMsB,GAAMA,EAAE,KAAOV,CAAS,CACtD,CAKO,SAASY,GAA0B,CACxC,GAAI,CAACxB,EAAM,gBAAiB,MAAO,KAEnC,IAAMyB,EAAK,KAAK,IAAI,EAAIzB,EAAM,gBACxB0B,EAAU,KAAK,MAAMD,EAAK,GAAI,EACpC,GAAIC,EAAU,GAAI,MAAO,GAAGA,CAAO,IAEnC,IAAMC,EAAU,KAAK,MAAMD,EAAU,EAAE,EACvC,OAAIC,EAAU,GAAW,GAAGA,CAAO,IAG5B,GADO,KAAK,MAAMA,EAAU,EAAE,CACtB,KAAKA,EAAU,EAAE,GAClC,CCvHO,SAASC,EAAWC,EAAkC,CAC3D,GAAI,CAACA,EAAM,MAAO,GAClB,IAAMC,EAAM,SAAS,cAAc,KAAK,EACxC,OAAAA,EAAI,YAAcD,EACXC,EAAI,SACb,CAKO,SAASC,EAAWC,EAA2B,CAEpD,OADa,IAAI,KAAKA,CAAS,EACnB,mBAAmB,CAAC,EAAG,CAAE,KAAM,UAAW,OAAQ,SAAU,CAAC,CAC3E,CCzBA,IAAIC,EAKJ,SAASC,GAAkC,CACzC,MAAO,CACL,UAAW,SAAS,eAAe,YAAY,EAC/C,YAAa,SAAS,eAAe,cAAc,EACnD,UAAW,SAAS,eAAe,YAAY,EAC/C,WAAY,SAAS,eAAe,aAAa,EACjD,aAAc,SAAS,eAAe,eAAe,EACrD,UAAW,SAAS,eAAe,YAAY,EAC/C,eAAgB,SAAS,eAAe,yBAAyB,EACjE,cAAe,SAAS,eAAe,gBAAgB,EACvD,eAAgB,SAAS,eAAe,iBAAiB,EACzD,uBAAwB,SAAS,eAAe,0BAA0B,EAC1E,qBAAsB,SAAS,eAAe,wBAAwB,EACtE,YAAa,SAAS,eAAe,cAAc,EACnD,WAAY,SAAS,eAAe,aAAa,EACjD,aAAc,SAAS,eAAe,eAAe,EACrD,gBAAiB,SAAS,eAAe,kBAAkB,EAC3D,cAAe,SAAS,eAAe,gBAAgB,EACvD,gBAAiB,SAAS,eAAe,kBAAkB,EAC3D,aAAc,SAAS,eAAe,eAAe,EACrD,eAAgB,SAAS,eAAe,iBAAiB,EACzD,OAAQ,SAAS,eAAe,QAAQ,CAC1C,CACF,CAKA,SAASC,GAA+B,CACtCF,EAAS,UAAU,UAAU,OAAO,UAAW,CAACG,EAAM,WAAW,CACnE,CAKA,SAASC,GAA8B,CACrC,GAAM,CAAE,SAAAC,EAAU,kBAAAC,CAAkB,EAAIH,EAExC,GAAI,CAACE,GAAYA,EAAS,SAAW,EAAG,CACtCL,EAAS,YAAY,UAAY,+FACjC,SAAS,eAAe,eAAe,EAAG,YAAc,IACxD,MACF,CAEA,SAAS,eAAe,eAAe,EAAG,YAAc,OAAOK,EAAS,MAAM,EAE9EL,EAAS,YAAY,UAAYK,EAAS,IAAKE,GAAM;AAAA,8BACzBA,EAAE,UAAY,YAAc,EAAE,IAAID,IAAsBC,EAAE,GAAK,SAAW,EAAE,sBAAsBC,EAAWD,EAAE,EAAE,CAAC;AAAA;AAAA,mCAE7GC,EAAWD,EAAE,MAAQA,EAAE,EAAE,CAAC;AAAA,sEACSC,EAAWD,EAAE,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAQnF,EAAE,KAAK,EAAE,CACZ,CAKA,SAASE,GAA2B,CAClC,GAAM,CAAE,SAAAJ,EAAU,kBAAAC,CAAkB,EAAIH,EAExC,GAAI,CAACE,GAAYA,EAAS,SAAW,EAAG,CACtCL,EAAS,UAAU,UAAY,GAC/BA,EAAS,UAAU,YAAYA,EAAS,UAAU,EAClDA,EAAS,WAAW,MAAM,QAAU,OACpC,MACF,CAEAA,EAAS,WAAW,MAAM,QAAU,OAEpCA,EAAS,UAAU,UAAYK,EAAS,IAAKE,GAAM,CACjD,IAAMG,EAASH,EAAE,QAAU,CAAC,EACtBI,EAAaD,EAAO,OAAS,EAC/BA,EAAO,IAAKE,GAAM;AAAA;AAAA;AAAA,uCAGaJ,EAAWI,EAAE,IAAI,CAAC;AAAA,sCACnBJ,EAAWI,EAAE,KAAO,EAAE,CAAC;AAAA;AAAA,SAEpD,EAAE,KAAK,EAAE,EACV,mDAEEC,EAAaP,IAAsBC,EAAE,GAC3C,MAAO;AAAA,iCACsBA,EAAE,UAAY,GAAK,SAAS,IAAIM,EAAa,WAAa,EAAE,sBAAsBL,EAAWD,EAAE,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wCAS3FC,EAAWD,EAAE,MAAQA,EAAE,EAAE,CAAC;AAAA,uCAC3BC,EAAWD,EAAE,MAAQ,EAAE,CAAC;AAAA;AAAA;AAAA,oCAG3BA,EAAE,UAAY,SAAWA,EAAE,aAAe,eAAiB,SAAS;AAAA;AAAA,oBAEpFA,EAAE,UAAY,SAAWA,EAAE,aAAe,kBAAoB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yCAOlDG,EAAO,MAAM;AAAA;AAAA;AAAA,cAGxCC,CAAU;AAAA;AAAA;AAAA;AAAA;AAAA,+DAKuCH,EAAWD,EAAE,EAAE,CAAC,KAAMA,EAAE,UAAyB,GAAb,UAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,yEAMzCC,EAAWD,EAAE,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,KAWvF,CAAC,EAAE,KAAK,EAAE,CACZ,CAKA,SAASO,GAAuB,CAC9B,GAAM,CAAE,SAAAC,CAAS,EAAIZ,EAErB,GAAI,CAACY,GAAYA,EAAS,SAAW,EAAG,CACtCf,EAAS,aAAa,UAAY,2DAClC,MACF,CAEAA,EAAS,aAAa,UAAYe,EAAS,MAAM,GAAG,EAAE,QAAQ,EAAE,IAAKC,GAAM;AAAA;AAAA;AAAA,kCAG3CR,EAAWQ,EAAE,eAAiB,OAAO,CAAC;AAAA,oCACpCR,EAAWQ,EAAE,IAAI,CAAC;AAAA;AAAA,oCAElBR,EAAWQ,EAAE,IAAM,GAAG,CAAC;AAAA,mCACxBC,EAAWD,EAAE,SAAS,CAAC;AAAA;AAAA,kCAExBR,EAAWQ,EAAE,MAAQA,EAAE,SAAW,EAAE,CAAC;AAAA;AAAA,GAEpE,EAAE,KAAK,EAAE,CACZ,CAKA,SAASE,GAAoB,CAC3B,IAAMC,EAAYC,EAAa,EAC/BpB,EAAS,WAAW,YAAc,OAAOmB,EAAU,MAAM,EACzDnB,EAAS,aAAa,YAAc,OAAOG,EAAM,SAAS,MAAM,CAClE,CAKA,SAASkB,GAA+B,CACtC,IAAMC,EAAoBC,EAAqB,EACzCC,EAAexB,EAAS,gBAAgB,MAE9CA,EAAS,gBAAgB,UAAY,gDACnCsB,EAAkB,IAAKf,GACrB,kBAAkBC,EAAWD,EAAE,EAAE,CAAC,KAAKC,EAAWD,EAAE,MAAQA,EAAE,EAAE,CAAC,WACnE,EAAE,KAAK,EAAE,EAGPiB,GAAgBF,EAAkB,KAAMf,GAAMA,EAAE,KAAOiB,CAAY,EACrExB,EAAS,gBAAgB,MAAQwB,EACxBrB,EAAM,mBAAqBmB,EAAkB,KAAMf,GAAMA,EAAE,KAAOJ,EAAM,iBAAiB,IAClGH,EAAS,gBAAgB,MAAQG,EAAM,kBACvCsB,EAAqB,EAEzB,CAKA,SAASA,GAA6B,CACpC,IAAMC,EAAY1B,EAAS,gBAAgB,MAC3C,GAAI,CAAC0B,EAAW,CACd1B,EAAS,cAAc,UAAY,4CACnCA,EAAS,cAAc,SAAW,GAClCA,EAAS,gBAAgB,SAAW,GACpCA,EAAS,aAAa,SAAW,GACjC,MACF,CAEA,IAAM2B,EAAe3B,EAAS,cAAc,MAEtCU,EADUkB,EAAWF,CAAS,GACZ,QAAU,CAAC,EAEnC1B,EAAS,cAAc,UAAY,6HAGjCU,EAAO,IAAKE,GACV,kBAAkBJ,EAAWI,EAAE,IAAI,CAAC,KAAKJ,EAAWI,EAAE,IAAI,CAAC,WAC7D,EAAE,KAAK,EAAE,EAEXZ,EAAS,cAAc,SAAW,GAG9B2B,GACkB,CAAC,IAAK,OAAQ,GAAGjB,EAAO,IAAK,GAAM,EAAE,IAAI,CAAC,EAC9C,SAASiB,CAAY,IACnC3B,EAAS,cAAc,MAAQ2B,EAGrC,CAKA,SAASE,GAA4B,CACnC,IAAMC,EAAa,CAAC,CAAC9B,EAAS,gBAAgB,MACxC+B,EAAW,CAAC,CAAC/B,EAAS,cAAc,MACpCgC,EAAahC,EAAS,gBAAgB,MAAM,KAAK,EAAE,OAAS,EAElEA,EAAS,gBAAgB,SAAW,CAAC8B,GAAc,CAACC,EACpD/B,EAAS,aAAa,SAAW,CAAC8B,GAAc,CAACC,GAAY,CAACC,CAChE,CAKA,eAAeC,GAAmC,CAChD,IAAMP,EAAY1B,EAAS,gBAAgB,MACrCkC,EAAKlC,EAAS,cAAc,MAC5BmC,EAAUnC,EAAS,gBAAgB,MAAM,KAAK,EAEpD,GAAI,GAAC0B,GAAa,CAACQ,GAAM,CAACC,GAE1B,CAAAnC,EAAS,aAAa,SAAW,GACjCA,EAAS,eAAe,YAAc,aACtCA,EAAS,eAAe,UAAY,kBAEpC,GAAI,CACF,IAAMoC,EAAW,MAAM,MAAM,mBAAoB,CAC/C,OAAQ,OACR,QAAS,CAAE,eAAgB,kBAAmB,EAC9C,KAAM,KAAK,UAAU,CAAE,UAAAV,EAAW,GAAAQ,EAAI,QAAAC,CAAQ,CAAC,CACjD,CAAC,EAEKE,EAAS,MAAMD,EAAS,KAAK,EAEnC,GAAIA,EAAS,IAAMC,EAAO,QACxBrC,EAAS,eAAe,YAAc,gBACtCA,EAAS,eAAe,UAAY,0BACpCA,EAAS,gBAAgB,MAAQ,GACjC,WAAW,IAAM,CACfA,EAAS,eAAe,YAAc,GACtCA,EAAS,eAAe,UAAY,iBACtC,EAAG,GAAI,MAEP,OAAM,IAAI,MAAMqC,EAAO,OAAS,gBAAgB,CAEpD,OAASC,EAAK,CACZtC,EAAS,eAAe,YAAesC,EAAc,SAAW,yBAChEtC,EAAS,eAAe,UAAY,uBACtC,CAEA6B,EAAoB,EACtB,CAKA,SAASU,GAAqB,CAC5B,GAAM,CAAE,kBAAAjC,CAAkB,EAAIH,EAE9B,GAAIG,EAAmB,CACrB,IAAMkC,EAAUZ,EAAWtB,CAAiB,EACxCkC,IACFxC,EAAS,YAAY,UAAY;AAAA;AAAA,sCAEDQ,EAAWgC,EAAQ,MAAQA,EAAQ,EAAE,CAAC;AAAA,QAG1E,MACExC,EAAS,YAAY,YAAc,cAEvC,CAKA,SAASyC,EAAcf,EAAgC,CACrDgB,EAAmBhB,CAAS,EAExBA,IACF1B,EAAS,gBAAgB,MAAQ0B,EACjCD,EAAqB,EACrBI,EAAoB,GAItB,SAAS,iBAAiB,eAAe,EAAE,QAASc,GAAS,CAC3DA,EAAK,UAAU,OAAO,WAAaA,EAAqB,QAAQ,YAAcjB,CAAS,CACzF,CAAC,CACH,CAKA,SAASkB,GAAoB,CAC3B5C,EAAS,eAAe,UAAU,IAAI,SAAS,EAC/CA,EAAS,cAAc,MAAQ,GAC/BA,EAAS,cAAc,MAAM,EAC7B6C,EAAqB,CACvB,CAKA,SAASC,GAAqB,CAC5B9C,EAAS,eAAe,UAAU,OAAO,SAAS,CACpD,CAKA,SAAS6C,GAA6B,CACpC,IAAME,EAAQ/C,EAAS,cAAc,MAAM,YAAY,EACjD,CAAE,SAAAK,CAAS,EAAIF,EAGf6C,EAAmBD,EACrB1C,EAAS,OAAQE,IAAOA,EAAE,MAAQA,EAAE,IAAI,YAAY,EAAE,SAASwC,CAAK,CAAC,EACrE1C,EAEA2C,EAAiB,OAAS,EAC5BhD,EAAS,uBAAuB,UAAY;AAAA;AAAA,QAExCgD,EAAiB,IAAKzC,GAAM;AAAA,kDACcC,EAAWD,EAAE,EAAE,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8CASpBC,EAAWD,EAAE,MAAQA,EAAE,EAAE,CAAC;AAAA,iDACvBA,EAAE,UAAY,SAAW,SAAS,UAAOA,EAAE,QAAU,CAAC,GAAG,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,OAMzG,EAAE,KAAK,EAAE,CAAC;AAAA,MAGbP,EAAS,uBAAuB,UAAY,kEAI9C,IAAMmB,EAAYC,EAAa,EACzB6B,EAAiBF,EACnB5B,EAAU,OAAQ,GAAM,EAAE,KAAK,YAAY,EAAE,SAAS4B,CAAK,CAAC,EAC5D5B,EAEA8B,EAAe,OAAS,EAC1BjD,EAAS,qBAAqB,UAAY;AAAA;AAAA,QAEtCiD,EAAe,IAAK,GAAM;AAAA,gDACczC,EAAW,EAAE,IAAI,CAAC,mBAAmBA,EAAW,EAAE,SAAS,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,8CAQ9DA,EAAW,EAAE,IAAI,CAAC;AAAA,iDACfA,EAAW,EAAE,WAAW,CAAC,SAAMA,EAAW,EAAE,KAAO,SAAS,CAAC;AAAA;AAAA;AAAA,OAGvG,EAAE,KAAK,EAAE,CAAC;AAAA,MAGbR,EAAS,qBAAqB,UAAY,wDAE9C,CAKA,SAASkD,GAA4B,CAEnClD,EAAS,UAAU,iBAAiB,QAAS4C,CAAW,EAGxD5C,EAAS,eAAe,iBAAiB,QAAU,GAAM,CACnD,EAAE,SAAWA,EAAS,gBAAgB8C,EAAa,CACzD,CAAC,EAGD9C,EAAS,cAAc,iBAAiB,QAAS6C,CAAoB,EAGrE,SAAS,iBAAiB,UAAY,GAAM,EAErC,EAAE,SAAW,EAAE,UAAY,EAAE,MAAQ,MACxC,EAAE,eAAe,EACb7C,EAAS,eAAe,UAAU,SAAS,SAAS,EACtD8C,EAAa,EAEbF,EAAY,GAIZ,EAAE,MAAQ,UAAY5C,EAAS,eAAe,UAAU,SAAS,SAAS,GAC5E8C,EAAa,CAEjB,CAAC,EAGD9C,EAAS,eAAe,iBAAiB,QAAU,GAAM,CACvD,IAAMmD,EAAQ,EAAE,OAAuB,QAAQ,eAAe,EAC9D,GAAI,CAACA,EAAM,OAEX,IAAMC,EAAUD,EAAK,QAAQ,QACvBzB,EAAYyB,EAAK,QAAQ,QACzBE,EAAYF,EAAK,QAAQ,MACzBG,EAASH,EAAK,QAAQ,OAE5B,GAAIC,IAAY,YACdN,EAAa,EACb9C,EAAS,gBAAgB,MAAM,EAC/BA,EAAS,eAAe,YAAc,uDAC7BoD,IAAY,UACrBN,EAAa,EACb,SAAS,OAAO,UACPM,IAAY,eACrBN,EAAa,EACb,OAAO,SAAS,KAAO,YACdQ,IAAW,kBAAoB5B,EACxCoB,EAAa,EACb,OAAO,SAAS,KAAO,YAAY,mBAAmBpB,CAAS,CAAC,WACvD2B,GAAa3B,EACtBoB,EAAa,EACb9C,EAAS,gBAAgB,MAAQ0B,EACjCD,EAAqB,EACrB,WAAW,IAAM,CACfzB,EAAS,cAAc,MAAQqD,EAC/BxB,EAAoB,EACpB7B,EAAS,gBAAgB,MAAM,CACjC,EAAG,EAAE,UACI0B,EAAW,CACpBoB,EAAa,EACbL,EAAcf,CAAS,EACvB,IAAMiB,EAAO,SAAS,cAAc,kCAAkCjB,CAAS,IAAI,EAC/EiB,GAAMA,EAAK,eAAe,CAAE,SAAU,SAAU,MAAO,QAAS,CAAC,CACvE,CACF,CAAC,EAGD3C,EAAS,UAAU,iBAAiB,QAAU,GAAM,CAClD,IAAMuD,EAAS,EAAE,OAGXC,EAAeD,EAAO,QAAQ,uBAAuB,EAC3D,GAAIC,EAAc,CAChB,EAAE,gBAAgB,EAClB,IAAM9B,EAAY8B,EAAa,QAAQ,cACnC9B,IACF,OAAO,SAAS,KAAO,YAAY,mBAAmBA,CAAS,CAAC,IAElE,MACF,CAGA,IAAM+B,EAAiBF,EAAO,QAAQ,qBAAqB,EAC3D,GAAIE,GAAkB,CAACA,EAAe,SAAU,CAC9C,EAAE,gBAAgB,EAClB,IAAM/B,EAAY+B,EAAe,QAAQ,YACrC/B,IACF1B,EAAS,gBAAgB,MAAQ0B,EACjCD,EAAqB,EACrB,WAAW,IAAM,CACfzB,EAAS,cAAc,MAAQ,OAC/B6B,EAAoB,EACpB7B,EAAS,gBAAgB,MAAM,CACjC,EAAG,EAAE,GAEP,MACF,CAEA,IAAM2C,EAAOY,EAAO,QAAQ,eAAe,EACvCZ,GACFF,EAAcE,EAAK,QAAQ,WAAa,IAAI,CAEhD,CAAC,EAGD3C,EAAS,YAAY,iBAAiB,QAAU,GAAM,CACpD,IAAMuD,EAAS,EAAE,OAGXC,EAAeD,EAAO,QAAQ,wBAAwB,EAC5D,GAAIC,EAAc,CAChB,EAAE,gBAAgB,EAClB,IAAM9B,EAAY8B,EAAa,QAAQ,iBACnC9B,IACF,OAAO,SAAS,KAAO,YAAY,mBAAmBA,CAAS,CAAC,IAElE,MACF,CAEA,IAAMyB,EAAOI,EAAO,QAAQ,eAAe,EACvCJ,GACFV,EAAcU,EAAK,QAAQ,WAAa,IAAI,CAEhD,CAAC,EAGDnD,EAAS,YAAY,iBAAiB,QAAU,GAAM,CACpD,IAAMuD,EAAS,EAAE,QACbA,EAAO,KAAO,eAAiBA,EAAO,UAAU,SAAS,WAAW,IACtEd,EAAc,IAAI,CAEtB,CAAC,EAGDzC,EAAS,gBAAgB,iBAAiB,SAAU,IAAM,CACxDyB,EAAqB,EACrBI,EAAoB,CACtB,CAAC,EAED7B,EAAS,cAAc,iBAAiB,SAAU6B,CAAmB,EACrE7B,EAAS,gBAAgB,iBAAiB,QAAS6B,CAAmB,EAEtE7B,EAAS,aAAa,iBAAiB,QAASiC,CAAiB,EACjEjC,EAAS,gBAAgB,iBAAiB,UAAY,GAAM,CACtD,EAAE,MAAQ,SAAW,CAAC,EAAE,UAAY,CAACA,EAAS,aAAa,WAC7D,EAAE,eAAe,EACjBiC,EAAkB,EAEtB,CAAC,CACH,CAKA,SAASyB,GAAgB,CACvB,IAAMC,EAAW,OAAO,SAAS,WAAa,SAAW,OAAS,MAC5DC,EAAK,IAAI,UAAU,GAAGD,CAAQ,KAAK,OAAO,SAAS,IAAI,YAAY,EAEzEC,EAAG,OAAS,IAAM,CAChBC,EAAa,EAAI,EACjBC,EAAaF,CAAE,CACjB,EAEAA,EAAG,QAAU,IAAM,CACjBC,EAAa,EAAK,EAClBC,EAAa,IAAI,EACjB,WAAWJ,EAAS,GAAI,CAC1B,EAEAE,EAAG,QAAU,IAAM,CACjBC,EAAa,EAAK,CACpB,EAEAD,EAAG,UAAaG,GAAM,CACpB,GAAI,CACF,IAAMC,EAAO,KAAK,MAAMD,EAAE,IAAI,EAC9BE,EAAYD,EAAK,UAAY,CAAC,CAAC,EAC/BE,EAAYF,EAAK,UAAY,CAAC,CAAC,CACjC,OAAS1B,EAAK,CACZ,QAAQ,MAAM,wBAAyBA,CAAG,CAC5C,CACF,CACF,CAKO,SAAS6B,GAAsB,CACpCnE,EAAWC,EAAa,EAGxBmE,EAAU,IAAM,CACdlE,EAAuB,EACvBE,EAAsB,EACtBK,EAAmB,EACnBK,EAAe,EACfI,EAAY,EACZG,EAAuB,EACvBkB,EAAa,EACTvC,EAAS,gBAAgB,QAC3ByB,EAAqB,EACrBI,EAAoB,EAExB,CAAC,EAGDqB,EAAoB,EAGpBQ,EAAQ,EAGR,YAAY,IAAM,CAChB1D,EAAS,OAAO,YAAc,WAAWqE,EAAgB,CAAC,EAC5D,EAAG,GAAI,CACT,CAGI,OAAO,SAAa,MAClB,SAAS,aAAe,UAC1B,SAAS,iBAAiB,mBAAoBF,CAAa,EAE3DA,EAAc", + "names": ["state", "listeners", "subscribe", "listener", "index", "notifyListeners", "err", "setProjects", "projects", "setMessages", "messages", "setSelectedProject", "projectId", "setConnected", "connected", "setWebSocket", "ws", "getAllAgents", "agents", "project", "agent", "getConnectedProjects", "p", "getProject", "getUptimeString", "ms", "seconds", "minutes", "escapeHtml", "text", "div", "formatTime", "timestamp", "elements", "initElements", "updateConnectionStatus", "state", "renderSidebarProjects", "projects", "selectedProjectId", "p", "escapeHtml", "renderProjectCards", "agents", "agentsHtml", "a", "isSelected", "renderMessages", "messages", "m", "formatTime", "updateStats", "allAgents", "getAllAgents", "updateComposerProjects", "connectedProjects", "getConnectedProjects", "currentValue", "updateComposerAgents", "projectId", "currentAgent", "getProject", "updateComposerState", "hasProject", "hasAgent", "hasMessage", "sendBridgeMessage", "to", "message", "response", "result", "err", "updateHeader", "project", "selectProject", "setSelectedProject", "card", "openPalette", "updatePaletteResults", "closePalette", "query", "filteredProjects", "filteredAgents", "setupEventListeners", "item", "command", "agentName", "action", "target", "dashboardBtn", "messageLeadBtn", "connect", "protocol", "ws", "setConnected", "setWebSocket", "e", "data", "setProjects", "setMessages", "initBridgeApp", "subscribe", "getUptimeString"] +} diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index f1c8789cd..ff68b811f 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -741,6 +741,57 @@ export async function startDashboard(port: number, dataDir: string, teamDir: str res.sendFile(path.join(publicDir, 'bridge.html')); }); + // Project dashboard route - serves main dashboard with project context + app.get('/project/:projectId', (req, res) => { + // Serve the main index.html - it will detect the project context from URL + res.sendFile(path.join(publicDir, 'index.html')); + }); + + // API endpoint to get project info by ID (for project dashboard context) + app.get('/api/project/:projectId', async (req, res) => { + const { projectId } = req.params; + + try { + const bridgeStatePath = path.join(dataDir, 'bridge-state.json'); + if (fs.existsSync(bridgeStatePath)) { + const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8')); + const project = (bridgeState.projects || []).find((p: { id: string }) => p.id === projectId); + + if (project) { + // Get project's data directory and enrich with agent info + const projectHash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 12); + const projectDataDir = path.join(path.dirname(dataDir), projectHash); + const projectTeamDir = path.join(projectDataDir, 'team'); + const agentsPath = path.join(projectTeamDir, 'agents.json'); + + let agents: { name: string; cli?: string; lastSeen?: string }[] = []; + if (fs.existsSync(agentsPath)) { + try { + const agentsData = JSON.parse(fs.readFileSync(agentsPath, 'utf-8')); + agents = agentsData.agents || []; + } catch { + // Ignore + } + } + + res.json({ + ...project, + agents, + socketPath: path.join(projectDataDir, 'relay.sock'), + dataDir: projectDataDir, + }); + } else { + res.status(404).json({ error: 'Project not found' }); + } + } else { + res.status(404).json({ error: 'Bridge not configured' }); + } + } catch (err) { + console.error('Failed to get project info:', err); + res.status(500).json({ error: 'Failed to get project info' }); + } + }); + // Bridge API endpoint - returns multi-project data // This is a placeholder that returns empty data when not in bridge mode // The actual bridge data comes from MultiProjectClient when running `agent-relay bridge` From cb3f9bdbcf2a8f4a63598475e1293eb46e0763be Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Dec 2025 21:39:06 +0000 Subject: [PATCH 2/4] fix: remove unused imports in bridge/app.ts --- src/dashboard/frontend/bridge/app.ts | 4 +-- src/dashboard/public/js/bridge.js | 52 +++++++++++++-------------- src/dashboard/public/js/bridge.js.map | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/dashboard/frontend/bridge/app.ts b/src/dashboard/frontend/bridge/app.ts index 9a52c3ab0..efa1c59fd 100644 --- a/src/dashboard/frontend/bridge/app.ts +++ b/src/dashboard/frontend/bridge/app.ts @@ -3,8 +3,8 @@ */ import { subscribe, state, setProjects, setMessages, setConnected, setWebSocket, setSelectedProject, getUptimeString, getConnectedProjects, getAllAgents, getProject } from './state.js'; -import type { BridgeDOMElements, Project, BridgeMessage } from './types.js'; -import { escapeHtml, formatTime, getAvatarColor, getInitials } from '../utils.js'; +import type { BridgeDOMElements } from './types.js'; +import { escapeHtml, formatTime } from '../utils.js'; let elements: BridgeDOMElements; diff --git a/src/dashboard/public/js/bridge.js b/src/dashboard/public/js/bridge.js index d5ddc5e38..0ab20c496 100644 --- a/src/dashboard/public/js/bridge.js +++ b/src/dashboard/public/js/bridge.js @@ -1,8 +1,8 @@ -var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,connectionStart:null},g=[];function S(e){return g.push(e),()=>{let s=g.indexOf(e);s>-1&&g.splice(s,1)}}function u(){g.forEach(e=>{try{e()}catch(s){console.error("[bridge-state] Listener error:",s)}})}function L(e){c.projects=e,u()}function b(e){c.messages=e,u()}function E(e){c.selectedProjectId=e,u()}function v(e){c.isConnected=e,e&&!c.connectionStart&&(c.connectionStart=Date.now()),u()}function j(e){c.ws=e}function h(){let e=[];return c.projects.forEach(s=>{(s.agents||[]).forEach(n=>{e.push({name:n.name,projectId:s.id,projectName:s.name||s.id,cli:n.cli})})}),e}function M(){return c.projects.filter(e=>e.connected)}function y(e){return c.projects.find(s=>s.id===e)}function x(){if(!c.connectionStart)return"--";let e=Date.now()-c.connectionStart,s=Math.floor(e/1e3);if(s<60)return`${s}s`;let n=Math.floor(s/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function r(e){if(!e)return"";let s=document.createElement("div");return s.textContent=e,s.innerHTML}function P(e){return new Date(e).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}var t;function C(){return{statusDot:document.getElementById("status-dot"),projectList:document.getElementById("project-list"),cardsGrid:document.getElementById("cards-grid"),emptyState:document.getElementById("empty-state"),messagesList:document.getElementById("messages-list"),searchBar:document.getElementById("search-bar"),paletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteProjectsSection:document.getElementById("palette-projects-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),channelName:document.getElementById("channel-name"),statAgents:document.getElementById("stat-agents"),statMessages:document.getElementById("stat-messages"),composerProject:document.getElementById("composer-project"),composerAgent:document.getElementById("composer-agent"),composerMessage:document.getElementById("composer-message"),composerSend:document.getElementById("composer-send"),composerStatus:document.getElementById("composer-status"),uptime:document.getElementById("uptime")}}function k(){t.statusDot.classList.toggle("offline",!c.isConnected)}function A(){let{projects:e,selectedProjectId:s}=c;if(!e||e.length===0){t.projectList.innerHTML='
  • No projects
  • ',document.getElementById("project-count").textContent="0";return}document.getElementById("project-count").textContent=String(e.length),t.projectList.innerHTML=e.map(n=>` -
  • +var r={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,connectionStart:null},g=[];function S(e){return g.push(e),()=>{let s=g.indexOf(e);s>-1&&g.splice(s,1)}}function u(){g.forEach(e=>{try{e()}catch(s){console.error("[bridge-state] Listener error:",s)}})}function L(e){r.projects=e,u()}function b(e){r.messages=e,u()}function E(e){r.selectedProjectId=e,u()}function v(e){r.isConnected=e,e&&!r.connectionStart&&(r.connectionStart=Date.now()),u()}function j(e){r.ws=e}function h(){let e=[];return r.projects.forEach(s=>{(s.agents||[]).forEach(n=>{e.push({name:n.name,projectId:s.id,projectName:s.name||s.id,cli:n.cli})})}),e}function M(){return r.projects.filter(e=>e.connected)}function y(e){return r.projects.find(s=>s.id===e)}function x(){if(!r.connectionStart)return"--";let e=Date.now()-r.connectionStart,s=Math.floor(e/1e3);if(s<60)return`${s}s`;let n=Math.floor(s/60);return n<60?`${n}m`:`${Math.floor(n/60)}h ${n%60}m`}function c(e){if(!e)return"";let s=document.createElement("div");return s.textContent=e,s.innerHTML}function P(e){return new Date(e).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}var t;function C(){return{statusDot:document.getElementById("status-dot"),projectList:document.getElementById("project-list"),cardsGrid:document.getElementById("cards-grid"),emptyState:document.getElementById("empty-state"),messagesList:document.getElementById("messages-list"),searchBar:document.getElementById("search-bar"),paletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteProjectsSection:document.getElementById("palette-projects-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),channelName:document.getElementById("channel-name"),statAgents:document.getElementById("stat-agents"),statMessages:document.getElementById("stat-messages"),composerProject:document.getElementById("composer-project"),composerAgent:document.getElementById("composer-agent"),composerMessage:document.getElementById("composer-message"),composerSend:document.getElementById("composer-send"),composerStatus:document.getElementById("composer-status"),uptime:document.getElementById("uptime")}}function k(){t.statusDot.classList.toggle("offline",!r.isConnected)}function H(){let{projects:e,selectedProjectId:s}=r;if(!e||e.length===0){t.projectList.innerHTML='
  • No projects
  • ',document.getElementById("project-count").textContent="0";return}document.getElementById("project-count").textContent=String(e.length),t.projectList.innerHTML=e.map(n=>` +
  • - ${r(n.name||n.id)} -
  • - `).join("")}function H(){let{projects:e,selectedProjectId:s}=c;if(!e||e.length===0){t.cardsGrid.innerHTML="",t.cardsGrid.appendChild(t.emptyState),t.emptyState.style.display="flex";return}t.emptyState.style.display="none",t.cardsGrid.innerHTML=e.map(n=>{let o=n.agents||[],i=o.length>0?o.map(m=>` + `).join("")}function A(){let{projects:e,selectedProjectId:s}=r;if(!e||e.length===0){t.cardsGrid.innerHTML="",t.cardsGrid.appendChild(t.emptyState),t.emptyState.style.display="flex";return}t.emptyState.style.display="none",t.cardsGrid.innerHTML=e.map(n=>{let o=n.agents||[],i=o.length>0?o.map(m=>`
    - ${r(m.name)} - ${r(m.cli||"")} + ${c(m.name)} + ${c(m.cli||"")}
    `).join(""):'
    No agents connected
    ',a=s===n.id;return` -
    +
    @@ -26,8 +26,8 @@ var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,con
    -
    ${r(n.name||n.id)}
    -
    ${r(n.path||"")}
    +
    ${c(n.name||n.id)}
    +
    ${c(n.path||"")}
    @@ -47,13 +47,13 @@ var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,con
    - -
    - `}).join("")}function D(){let{messages:e}=c;if(!e||e.length===0){t.messagesList.innerHTML='

    No messages yet

    ';return}t.messagesList.innerHTML=e.slice(-50).reverse().map(s=>` + `}).join("")}function D(){let{messages:e}=r;if(!e||e.length===0){t.messagesList.innerHTML='

    No messages yet

    ';return}t.messagesList.innerHTML=e.slice(-50).reverse().map(s=>`
    - ${r(s.sourceProject||"local")} - ${r(s.from)} + ${c(s.sourceProject||"local")} + ${c(s.from)} \u2192 - ${r(s.to||"*")} + ${c(s.to||"*")} ${P(s.timestamp)}
    -
    ${r(s.body||s.content||"")}
    +
    ${c(s.body||s.content||"")}
    - `).join("")}function O(){let e=h();t.statAgents.textContent=String(e.length),t.statMessages.textContent=String(c.messages.length)}function N(){let e=M(),s=t.composerProject.value;t.composerProject.innerHTML=''+e.map(n=>``).join(""),s&&e.some(n=>n.id===s)?t.composerProject.value=s:c.selectedProjectId&&e.some(n=>n.id===c.selectedProjectId)&&(t.composerProject.value=c.selectedProjectId,p())}function p(){let e=t.composerProject.value;if(!e){t.composerAgent.innerHTML='',t.composerAgent.disabled=!0,t.composerMessage.disabled=!0,t.composerSend.disabled=!0;return}let s=t.composerAgent.value,o=y(e)?.agents||[];t.composerAgent.innerHTML=''+o.map(i=>``).join(""),t.composerAgent.disabled=!1,s&&["*","lead",...o.map(a=>a.name)].includes(s)&&(t.composerAgent.value=s)}function l(){let e=!!t.composerProject.value,s=!!t.composerAgent.value,n=t.composerMessage.value.trim().length>0;t.composerMessage.disabled=!e||!s,t.composerSend.disabled=!e||!s||!n}async function w(){let e=t.composerProject.value,s=t.composerAgent.value,n=t.composerMessage.value.trim();if(!(!e||!s||!n)){t.composerSend.disabled=!0,t.composerStatus.textContent="Sending...",t.composerStatus.className="composer-status";try{let o=await fetch("/api/bridge/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({projectId:e,to:s,message:n})}),i=await o.json();if(o.ok&&i.success)t.composerStatus.textContent="Message sent!",t.composerStatus.className="composer-status success",t.composerMessage.value="",setTimeout(()=>{t.composerStatus.textContent="",t.composerStatus.className="composer-status"},2e3);else throw new Error(i.error||"Failed to send")}catch(o){t.composerStatus.textContent=o.message||"Failed to send message",t.composerStatus.className="composer-status error"}l()}}function R(){let{selectedProjectId:e}=c;if(e){let s=y(e);s&&(t.channelName.innerHTML=` + `).join("")}function O(){let e=h();t.statAgents.textContent=String(e.length),t.statMessages.textContent=String(r.messages.length)}function N(){let e=M(),s=t.composerProject.value;t.composerProject.innerHTML=''+e.map(n=>``).join(""),s&&e.some(n=>n.id===s)?t.composerProject.value=s:r.selectedProjectId&&e.some(n=>n.id===r.selectedProjectId)&&(t.composerProject.value=r.selectedProjectId,p())}function p(){let e=t.composerProject.value;if(!e){t.composerAgent.innerHTML='',t.composerAgent.disabled=!0,t.composerMessage.disabled=!0,t.composerSend.disabled=!0;return}let s=t.composerAgent.value,o=y(e)?.agents||[];t.composerAgent.innerHTML=''+o.map(i=>``).join(""),t.composerAgent.disabled=!1,s&&["*","lead",...o.map(a=>a.name)].includes(s)&&(t.composerAgent.value=s)}function l(){let e=!!t.composerProject.value,s=!!t.composerAgent.value,n=t.composerMessage.value.trim().length>0;t.composerMessage.disabled=!e||!s,t.composerSend.disabled=!e||!s||!n}async function w(){let e=t.composerProject.value,s=t.composerAgent.value,n=t.composerMessage.value.trim();if(!(!e||!s||!n)){t.composerSend.disabled=!0,t.composerStatus.textContent="Sending...",t.composerStatus.className="composer-status";try{let o=await fetch("/api/bridge/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({projectId:e,to:s,message:n})}),i=await o.json();if(o.ok&&i.success)t.composerStatus.textContent="Message sent!",t.composerStatus.className="composer-status success",t.composerMessage.value="",setTimeout(()=>{t.composerStatus.textContent="",t.composerStatus.className="composer-status"},2e3);else throw new Error(i.error||"Failed to send")}catch(o){t.composerStatus.textContent=o.message||"Failed to send message",t.composerStatus.className="composer-status error"}l()}}function R(){let{selectedProjectId:e}=r;if(e){let s=y(e);s&&(t.channelName.innerHTML=` \u2190 All Projects - ${r(s.name||s.id)} - `)}else t.channelName.textContent="All Projects"}function f(e){E(e),e&&(t.composerProject.value=e,p(),l()),document.querySelectorAll(".project-card").forEach(s=>{s.classList.toggle("selected",s.dataset.projectId===e)})}function $(){t.paletteOverlay.classList.add("visible"),t.paletteSearch.value="",t.paletteSearch.focus(),B()}function d(){t.paletteOverlay.classList.remove("visible")}function B(){let e=t.paletteSearch.value.toLowerCase(),{projects:s}=c,n=e?s.filter(a=>(a.name||a.id).toLowerCase().includes(e)):s;n.length>0?t.paletteProjectsSection.innerHTML=` + ${c(s.name||s.id)} + `)}else t.channelName.textContent="All Projects"}function f(e){E(e),e&&(t.composerProject.value=e,p(),l()),document.querySelectorAll(".project-card").forEach(s=>{s.classList.toggle("selected",s.dataset.projectId===e)})}function $(){t.paletteOverlay.classList.add("visible"),t.paletteSearch.value="",t.paletteSearch.focus(),B()}function d(){t.paletteOverlay.classList.remove("visible")}function B(){let e=t.paletteSearch.value.toLowerCase(),{projects:s}=r,n=e?s.filter(a=>(a.name||a.id).toLowerCase().includes(e)):s;n.length>0?t.paletteProjectsSection.innerHTML=`
    Open Project Dashboard
    ${n.map(a=>` -
    +
    @@ -89,7 +89,7 @@ var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,con
    -
    ${r(a.name||a.id)}
    +
    ${c(a.name||a.id)}
    ${a.connected?"Online":"Offline"} \xB7 ${(a.agents||[]).length} agents \xB7 Click to open dashboard
    @@ -100,7 +100,7 @@ var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,con `:t.paletteProjectsSection.innerHTML='
    Open Project Dashboard
    ';let o=h(),i=e?o.filter(a=>a.name.toLowerCase().includes(e)):o;i.length>0?t.paletteAgentsSection.innerHTML=`
    Message Agent
    ${i.map(a=>` -
    +
    @@ -108,10 +108,10 @@ var c={projects:[],messages:[],selectedProjectId:null,isConnected:!1,ws:null,con
    -
    ${r(a.name)}
    -
    ${r(a.projectName)} \xB7 ${r(a.cli||"unknown")}
    +
    ${c(a.name)}
    +
    ${c(a.projectName)} \xB7 ${c(a.cli||"unknown")}
    `).join("")} - `:t.paletteAgentsSection.innerHTML='
    Message Agent
    '}function U(){t.searchBar.addEventListener("click",$),t.paletteOverlay.addEventListener("click",e=>{e.target===t.paletteOverlay&&d()}),t.paletteSearch.addEventListener("input",B),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&e.key==="k"&&(e.preventDefault(),t.paletteOverlay.classList.contains("visible")?d():$()),e.key==="Escape"&&t.paletteOverlay.classList.contains("visible")&&d()}),t.paletteResults.addEventListener("click",e=>{let s=e.target.closest(".palette-item");if(!s)return;let n=s.dataset.command,o=s.dataset.project,i=s.dataset.agent,a=s.dataset.action;if(n==="broadcast")d(),t.composerMessage.focus(),t.composerStatus.textContent="Select a project and agent to send a message";else if(n==="refresh")d(),location.reload();else if(n==="go-dashboard")d(),window.location.href="/";else if(a==="open-dashboard"&&o)d(),window.location.href=`/project/${encodeURIComponent(o)}`;else if(i&&o)d(),t.composerProject.value=o,p(),setTimeout(()=>{t.composerAgent.value=i,l(),t.composerMessage.focus()},50);else if(o){d(),f(o);let m=document.querySelector(`.project-card[data-project-id="${o}"]`);m&&m.scrollIntoView({behavior:"smooth",block:"center"})}}),t.cardsGrid.addEventListener("click",e=>{let s=e.target,n=s.closest("[data-open-dashboard]");if(n){e.stopPropagation();let a=n.dataset.openDashboard;a&&(window.location.href=`/project/${encodeURIComponent(a)}`);return}let o=s.closest("[data-message-lead]");if(o&&!o.disabled){e.stopPropagation();let a=o.dataset.messageLead;a&&(t.composerProject.value=a,p(),setTimeout(()=>{t.composerAgent.value="lead",l(),t.composerMessage.focus()},50));return}let i=s.closest(".project-card");i&&f(i.dataset.projectId||null)}),t.projectList.addEventListener("click",e=>{let s=e.target,n=s.closest(".project-dashboard-btn");if(n){e.stopPropagation();let i=n.dataset.dashboardProject;i&&(window.location.href=`/project/${encodeURIComponent(i)}`);return}let o=s.closest(".project-item");o&&f(o.dataset.projectId||null)}),t.channelName.addEventListener("click",e=>{let s=e.target;(s.id==="back-to-all"||s.classList.contains("back-link"))&&f(null)}),t.composerProject.addEventListener("change",()=>{p(),l()}),t.composerAgent.addEventListener("change",l),t.composerMessage.addEventListener("input",l),t.composerSend.addEventListener("click",w),t.composerMessage.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!t.composerSend.disabled&&(e.preventDefault(),w())})}function T(){let e=window.location.protocol==="https:"?"wss:":"ws:",s=new WebSocket(`${e}//${window.location.host}/ws/bridge`);s.onopen=()=>{v(!0),j(s)},s.onclose=()=>{v(!1),j(null),setTimeout(T,3e3)},s.onerror=()=>{v(!1)},s.onmessage=n=>{try{let o=JSON.parse(n.data);L(o.projects||[]),b(o.messages||[])}catch(o){console.error("[bridge] Parse error:",o)}}}function I(){t=C(),S(()=>{k(),A(),H(),D(),O(),N(),R(),t.composerProject.value&&(p(),l())}),U(),T(),setInterval(()=>{t.uptime.textContent=`Uptime: ${x()}`},1e3)}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I):I());export{I as initBridgeApp}; + `:t.paletteAgentsSection.innerHTML='
    Message Agent
    '}function U(){t.searchBar.addEventListener("click",$),t.paletteOverlay.addEventListener("click",e=>{e.target===t.paletteOverlay&&d()}),t.paletteSearch.addEventListener("input",B),document.addEventListener("keydown",e=>{(e.metaKey||e.ctrlKey)&&e.key==="k"&&(e.preventDefault(),t.paletteOverlay.classList.contains("visible")?d():$()),e.key==="Escape"&&t.paletteOverlay.classList.contains("visible")&&d()}),t.paletteResults.addEventListener("click",e=>{let s=e.target.closest(".palette-item");if(!s)return;let n=s.dataset.command,o=s.dataset.project,i=s.dataset.agent,a=s.dataset.action;if(n==="broadcast")d(),t.composerMessage.focus(),t.composerStatus.textContent="Select a project and agent to send a message";else if(n==="refresh")d(),location.reload();else if(n==="go-dashboard")d(),window.location.href="/";else if(a==="open-dashboard"&&o)d(),window.location.href=`/project/${encodeURIComponent(o)}`;else if(i&&o)d(),t.composerProject.value=o,p(),setTimeout(()=>{t.composerAgent.value=i,l(),t.composerMessage.focus()},50);else if(o){d(),f(o);let m=document.querySelector(`.project-card[data-project-id="${o}"]`);m&&m.scrollIntoView({behavior:"smooth",block:"center"})}}),t.cardsGrid.addEventListener("click",e=>{let s=e.target,n=s.closest("[data-open-dashboard]");if(n){e.stopPropagation();let a=n.dataset.openDashboard;a&&(window.location.href=`/project/${encodeURIComponent(a)}`);return}let o=s.closest("[data-message-lead]");if(o&&!o.disabled){e.stopPropagation();let a=o.dataset.messageLead;a&&(t.composerProject.value=a,p(),setTimeout(()=>{t.composerAgent.value="lead",l(),t.composerMessage.focus()},50));return}let i=s.closest(".project-card");i&&f(i.dataset.projectId||null)}),t.projectList.addEventListener("click",e=>{let s=e.target,n=s.closest(".project-dashboard-btn");if(n){e.stopPropagation();let i=n.dataset.dashboardProject;i&&(window.location.href=`/project/${encodeURIComponent(i)}`);return}let o=s.closest(".project-item");o&&f(o.dataset.projectId||null)}),t.channelName.addEventListener("click",e=>{let s=e.target;(s.id==="back-to-all"||s.classList.contains("back-link"))&&f(null)}),t.composerProject.addEventListener("change",()=>{p(),l()}),t.composerAgent.addEventListener("change",l),t.composerMessage.addEventListener("input",l),t.composerSend.addEventListener("click",w),t.composerMessage.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&!t.composerSend.disabled&&(e.preventDefault(),w())})}function T(){let e=window.location.protocol==="https:"?"wss:":"ws:",s=new WebSocket(`${e}//${window.location.host}/ws/bridge`);s.onopen=()=>{v(!0),j(s)},s.onclose=()=>{v(!1),j(null),setTimeout(T,3e3)},s.onerror=()=>{v(!1)},s.onmessage=n=>{try{let o=JSON.parse(n.data);L(o.projects||[]),b(o.messages||[])}catch(o){console.error("[bridge] Parse error:",o)}}}function I(){t=C(),S(()=>{k(),H(),A(),D(),O(),N(),R(),t.composerProject.value&&(p(),l())}),U(),T(),setInterval(()=>{t.uptime.textContent=`Uptime: ${x()}`},1e3)}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",I):I());export{I as initBridgeApp}; //# sourceMappingURL=bridge.js.map diff --git a/src/dashboard/public/js/bridge.js.map b/src/dashboard/public/js/bridge.js.map index 006f06bfa..214d36282 100644 --- a/src/dashboard/public/js/bridge.js.map +++ b/src/dashboard/public/js/bridge.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../frontend/bridge/state.ts", "../../frontend/utils.ts", "../../frontend/bridge/app.ts"], - "sourcesContent": ["/**\n * Bridge State Management\n * Centralized state for the bridge dashboard\n */\n\nimport type { BridgeState, Project, BridgeMessage } from './types.js';\n\ntype StateListener = () => void;\n\n// Bridge state\nexport const state: BridgeState = {\n projects: [],\n messages: [],\n selectedProjectId: null,\n isConnected: false,\n ws: null,\n connectionStart: null,\n};\n\n// State subscribers\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => {\n try {\n listener();\n } catch (err) {\n console.error('[bridge-state] Listener error:', err);\n }\n });\n}\n\n/**\n * Update projects\n */\nexport function setProjects(projects: Project[]): void {\n state.projects = projects;\n notifyListeners();\n}\n\n/**\n * Update messages\n */\nexport function setMessages(messages: BridgeMessage[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set selected project\n */\nexport function setSelectedProject(projectId: string | null): void {\n state.selectedProjectId = projectId;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnected(connected: boolean): void {\n state.isConnected = connected;\n if (connected && !state.connectionStart) {\n state.connectionStart = Date.now();\n }\n notifyListeners();\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Get all agents across all projects\n */\nexport function getAllAgents(): { name: string; projectId: string; projectName: string; cli?: string }[] {\n const agents: { name: string; projectId: string; projectName: string; cli?: string }[] = [];\n\n state.projects.forEach((project) => {\n (project.agents || []).forEach((agent) => {\n agents.push({\n name: agent.name,\n projectId: project.id,\n projectName: project.name || project.id,\n cli: agent.cli,\n });\n });\n });\n\n return agents;\n}\n\n/**\n * Get connected projects\n */\nexport function getConnectedProjects(): Project[] {\n return state.projects.filter((p) => p.connected);\n}\n\n/**\n * Get project by ID\n */\nexport function getProject(projectId: string): Project | undefined {\n return state.projects.find((p) => p.id === projectId);\n}\n\n/**\n * Get uptime formatted string\n */\nexport function getUptimeString(): string {\n if (!state.connectionStart) return '--';\n\n const ms = Date.now() - state.connectionStart;\n const seconds = Math.floor(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m`;\n\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${minutes % 60}m`;\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Bridge Dashboard Application Entry Point\n */\n\nimport { subscribe, state, setProjects, setMessages, setConnected, setWebSocket, setSelectedProject, getUptimeString, getConnectedProjects, getAllAgents, getProject } from './state.js';\nimport type { BridgeDOMElements, Project, BridgeMessage } from './types.js';\nimport { escapeHtml, formatTime, getAvatarColor, getInitials } from '../utils.js';\n\nlet elements: BridgeDOMElements;\n\n/**\n * Initialize DOM element references\n */\nfunction initElements(): BridgeDOMElements {\n return {\n statusDot: document.getElementById('status-dot')!,\n projectList: document.getElementById('project-list')!,\n cardsGrid: document.getElementById('cards-grid')!,\n emptyState: document.getElementById('empty-state')!,\n messagesList: document.getElementById('messages-list')!,\n searchBar: document.getElementById('search-bar')!,\n paletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteProjectsSection: document.getElementById('palette-projects-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n channelName: document.getElementById('channel-name')!,\n statAgents: document.getElementById('stat-agents')!,\n statMessages: document.getElementById('stat-messages')!,\n composerProject: document.getElementById('composer-project') as HTMLSelectElement,\n composerAgent: document.getElementById('composer-agent') as HTMLSelectElement,\n composerMessage: document.getElementById('composer-message') as HTMLInputElement,\n composerSend: document.getElementById('composer-send') as HTMLButtonElement,\n composerStatus: document.getElementById('composer-status')!,\n uptime: document.getElementById('uptime')!,\n };\n}\n\n/**\n * Update connection status indicator\n */\nfunction updateConnectionStatus(): void {\n elements.statusDot.classList.toggle('offline', !state.isConnected);\n}\n\n/**\n * Render sidebar projects list\n */\nfunction renderSidebarProjects(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.projectList.innerHTML = '
  • No projects
  • ';\n document.getElementById('project-count')!.textContent = '0';\n return;\n }\n\n document.getElementById('project-count')!.textContent = String(projects.length);\n\n elements.projectList.innerHTML = projects.map((p) => `\n
  • \n \n ${escapeHtml(p.name || p.id)}\n \n
  • \n `).join('');\n}\n\n/**\n * Render project cards grid\n */\nfunction renderProjectCards(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.cardsGrid.innerHTML = '';\n elements.cardsGrid.appendChild(elements.emptyState);\n elements.emptyState.style.display = 'flex';\n return;\n }\n\n elements.emptyState.style.display = 'none';\n\n elements.cardsGrid.innerHTML = projects.map((p) => {\n const agents = p.agents || [];\n const agentsHtml = agents.length > 0\n ? agents.map((a) => `\n
    \n \n ${escapeHtml(a.name)}\n ${escapeHtml(a.cli || '')}\n
    \n `).join('')\n : '
    No agents connected
    ';\n\n const isSelected = selectedProjectId === p.id;\n return `\n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${escapeHtml(p.path || '')}
    \n
    \n
    \n
    \n \n ${p.connected ? 'Online' : p.reconnecting ? 'Reconnecting...' : 'Offline'}\n
    \n
    \n\n
    \n
    \n Agents\n ${agents.length} active\n
    \n
    \n ${agentsHtml}\n
    \n
    \n\n
    \n \n \n
    \n
    \n `;\n }).join('');\n}\n\n/**\n * Render messages list\n */\nfunction renderMessages(): void {\n const { messages } = state;\n\n if (!messages || messages.length === 0) {\n elements.messagesList.innerHTML = '

    No messages yet

    ';\n return;\n }\n\n elements.messagesList.innerHTML = messages.slice(-50).reverse().map((m) => `\n
    \n
    \n ${escapeHtml(m.sourceProject || 'local')}\n ${escapeHtml(m.from)}\n \u2192\n ${escapeHtml(m.to || '*')}\n ${formatTime(m.timestamp)}\n
    \n
    ${escapeHtml(m.body || m.content || '')}
    \n
    \n `).join('');\n}\n\n/**\n * Update stats display\n */\nfunction updateStats(): void {\n const allAgents = getAllAgents();\n elements.statAgents.textContent = String(allAgents.length);\n elements.statMessages.textContent = String(state.messages.length);\n}\n\n/**\n * Update composer project options\n */\nfunction updateComposerProjects(): void {\n const connectedProjects = getConnectedProjects();\n const currentValue = elements.composerProject.value;\n\n elements.composerProject.innerHTML = '' +\n connectedProjects.map((p) =>\n ``\n ).join('');\n\n // Restore selection if still valid\n if (currentValue && connectedProjects.some((p) => p.id === currentValue)) {\n elements.composerProject.value = currentValue;\n } else if (state.selectedProjectId && connectedProjects.some((p) => p.id === state.selectedProjectId)) {\n elements.composerProject.value = state.selectedProjectId;\n updateComposerAgents();\n }\n}\n\n/**\n * Update composer agent options\n */\nfunction updateComposerAgents(): void {\n const projectId = elements.composerProject.value;\n if (!projectId) {\n elements.composerAgent.innerHTML = '';\n elements.composerAgent.disabled = true;\n elements.composerMessage.disabled = true;\n elements.composerSend.disabled = true;\n return;\n }\n\n const currentAgent = elements.composerAgent.value;\n const project = getProject(projectId);\n const agents = project?.agents || [];\n\n elements.composerAgent.innerHTML = '' +\n '' +\n '' +\n agents.map((a) =>\n ``\n ).join('');\n\n elements.composerAgent.disabled = false;\n\n // Restore agent selection if still valid\n if (currentAgent) {\n const validAgents = ['*', 'lead', ...agents.map((a) => a.name)];\n if (validAgents.includes(currentAgent)) {\n elements.composerAgent.value = currentAgent;\n }\n }\n}\n\n/**\n * Update composer state based on selections\n */\nfunction updateComposerState(): void {\n const hasProject = !!elements.composerProject.value;\n const hasAgent = !!elements.composerAgent.value;\n const hasMessage = elements.composerMessage.value.trim().length > 0;\n\n elements.composerMessage.disabled = !hasProject || !hasAgent;\n elements.composerSend.disabled = !hasProject || !hasAgent || !hasMessage;\n}\n\n/**\n * Send message via bridge API\n */\nasync function sendBridgeMessage(): Promise {\n const projectId = elements.composerProject.value;\n const to = elements.composerAgent.value;\n const message = elements.composerMessage.value.trim();\n\n if (!projectId || !to || !message) return;\n\n elements.composerSend.disabled = true;\n elements.composerStatus.textContent = 'Sending...';\n elements.composerStatus.className = 'composer-status';\n\n try {\n const response = await fetch('/api/bridge/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ projectId, to, message }),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n elements.composerStatus.textContent = 'Message sent!';\n elements.composerStatus.className = 'composer-status success';\n elements.composerMessage.value = '';\n setTimeout(() => {\n elements.composerStatus.textContent = '';\n elements.composerStatus.className = 'composer-status';\n }, 2000);\n } else {\n throw new Error(result.error || 'Failed to send');\n }\n } catch (err) {\n elements.composerStatus.textContent = (err as Error).message || 'Failed to send message';\n elements.composerStatus.className = 'composer-status error';\n }\n\n updateComposerState();\n}\n\n/**\n * Update header for project selection\n */\nfunction updateHeader(): void {\n const { selectedProjectId } = state;\n\n if (selectedProjectId) {\n const project = getProject(selectedProjectId);\n if (project) {\n elements.channelName.innerHTML = `\n \u2190 All Projects\n ${escapeHtml(project.name || project.id)}\n `;\n }\n } else {\n elements.channelName.textContent = 'All Projects';\n }\n}\n\n/**\n * Select a project\n */\nfunction selectProject(projectId: string | null): void {\n setSelectedProject(projectId);\n\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n updateComposerState();\n }\n\n // Update card selection visually\n document.querySelectorAll('.project-card').forEach((card) => {\n card.classList.toggle('selected', (card as HTMLElement).dataset.projectId === projectId);\n });\n}\n\n/**\n * Open command palette\n */\nfunction openPalette(): void {\n elements.paletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n updatePaletteResults();\n}\n\n/**\n * Close command palette\n */\nfunction closePalette(): void {\n elements.paletteOverlay.classList.remove('visible');\n}\n\n/**\n * Update palette search results\n */\nfunction updatePaletteResults(): void {\n const query = elements.paletteSearch.value.toLowerCase();\n const { projects } = state;\n\n // Update projects section\n const filteredProjects = query\n ? projects.filter((p) => (p.name || p.id).toLowerCase().includes(query))\n : projects;\n\n if (filteredProjects.length > 0) {\n elements.paletteProjectsSection.innerHTML = `\n
    Open Project Dashboard
    \n ${filteredProjects.map((p) => `\n
    \n
    \n \n \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${p.connected ? 'Online' : 'Offline'} \u00B7 ${(p.agents || []).length} agents \u00B7 Click to open dashboard
    \n
    \n
    \n \u23CE\n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteProjectsSection.innerHTML = '
    Open Project Dashboard
    ';\n }\n\n // Update agents section\n const allAgents = getAllAgents();\n const filteredAgents = query\n ? allAgents.filter((a) => a.name.toLowerCase().includes(query))\n : allAgents;\n\n if (filteredAgents.length > 0) {\n elements.paletteAgentsSection.innerHTML = `\n
    Message Agent
    \n ${filteredAgents.map((a) => `\n
    \n
    \n \n \n \n \n
    \n
    \n
    ${escapeHtml(a.name)}
    \n
    ${escapeHtml(a.projectName)} \u00B7 ${escapeHtml(a.cli || 'unknown')}
    \n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteAgentsSection.innerHTML = '
    Message Agent
    ';\n }\n}\n\n/**\n * Set up event listeners\n */\nfunction setupEventListeners(): void {\n // Search bar opens palette\n elements.searchBar.addEventListener('click', openPalette);\n\n // Palette overlay click closes\n elements.paletteOverlay.addEventListener('click', (e) => {\n if (e.target === elements.paletteOverlay) closePalette();\n });\n\n // Palette search filtering\n elements.paletteSearch.addEventListener('input', updatePaletteResults);\n\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => {\n // Cmd/Ctrl + K to open palette\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n } else {\n openPalette();\n }\n }\n // Escape to close\n if (e.key === 'Escape' && elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n }\n });\n\n // Palette item clicks\n elements.paletteResults.addEventListener('click', (e) => {\n const item = (e.target as HTMLElement).closest('.palette-item') as HTMLElement | null;\n if (!item) return;\n\n const command = item.dataset.command;\n const projectId = item.dataset.project;\n const agentName = item.dataset.agent;\n const action = item.dataset.action;\n\n if (command === 'broadcast') {\n closePalette();\n elements.composerMessage.focus();\n elements.composerStatus.textContent = 'Select a project and agent to send a message';\n } else if (command === 'refresh') {\n closePalette();\n location.reload();\n } else if (command === 'go-dashboard') {\n closePalette();\n window.location.href = '/';\n } else if (action === 'open-dashboard' && projectId) {\n closePalette();\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n } else if (agentName && projectId) {\n closePalette();\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = agentName;\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n } else if (projectId) {\n closePalette();\n selectProject(projectId);\n const card = document.querySelector(`.project-card[data-project-id=\"${projectId}\"]`);\n if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });\n }\n });\n\n // Project card clicks\n elements.cardsGrid.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Handle \"Open Dashboard\" button\n const dashboardBtn = target.closest('[data-open-dashboard]') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.openDashboard;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n // Handle \"Message Lead\" button\n const messageLeadBtn = target.closest('[data-message-lead]') as HTMLButtonElement | null;\n if (messageLeadBtn && !messageLeadBtn.disabled) {\n e.stopPropagation();\n const projectId = messageLeadBtn.dataset.messageLead;\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = 'lead';\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n }\n return;\n }\n\n const card = target.closest('.project-card') as HTMLElement | null;\n if (card) {\n selectProject(card.dataset.projectId || null);\n }\n });\n\n // Sidebar project clicks\n elements.projectList.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Check if dashboard button was clicked\n const dashboardBtn = target.closest('.project-dashboard-btn') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.dashboardProject;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n const item = target.closest('.project-item') as HTMLElement | null;\n if (item) {\n selectProject(item.dataset.projectId || null);\n }\n });\n\n // Header back link\n elements.channelName.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (target.id === 'back-to-all' || target.classList.contains('back-link')) {\n selectProject(null);\n }\n });\n\n // Composer events\n elements.composerProject.addEventListener('change', () => {\n updateComposerAgents();\n updateComposerState();\n });\n\n elements.composerAgent.addEventListener('change', updateComposerState);\n elements.composerMessage.addEventListener('input', updateComposerState);\n\n elements.composerSend.addEventListener('click', sendBridgeMessage);\n elements.composerMessage.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !e.shiftKey && !elements.composerSend.disabled) {\n e.preventDefault();\n sendBridgeMessage();\n }\n });\n}\n\n/**\n * Connect to WebSocket\n */\nfunction connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws/bridge`);\n\n ws.onopen = () => {\n setConnected(true);\n setWebSocket(ws);\n };\n\n ws.onclose = () => {\n setConnected(false);\n setWebSocket(null);\n setTimeout(connect, 3000);\n };\n\n ws.onerror = () => {\n setConnected(false);\n };\n\n ws.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n setProjects(data.projects || []);\n setMessages(data.messages || []);\n } catch (err) {\n console.error('[bridge] Parse error:', err);\n }\n };\n}\n\n/**\n * Initialize the bridge application\n */\nexport function initBridgeApp(): void {\n elements = initElements();\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderSidebarProjects();\n renderProjectCards();\n renderMessages();\n updateStats();\n updateComposerProjects();\n updateHeader();\n if (elements.composerProject.value) {\n updateComposerAgents();\n updateComposerState();\n }\n });\n\n // Set up event listeners\n setupEventListeners();\n\n // Connect to WebSocket\n connect();\n\n // Update uptime periodically\n setInterval(() => {\n elements.uptime.textContent = `Uptime: ${getUptimeString()}`;\n }, 1000);\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initBridgeApp);\n } else {\n initBridgeApp();\n }\n}\n"], + "sourcesContent": ["/**\n * Bridge State Management\n * Centralized state for the bridge dashboard\n */\n\nimport type { BridgeState, Project, BridgeMessage } from './types.js';\n\ntype StateListener = () => void;\n\n// Bridge state\nexport const state: BridgeState = {\n projects: [],\n messages: [],\n selectedProjectId: null,\n isConnected: false,\n ws: null,\n connectionStart: null,\n};\n\n// State subscribers\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => {\n try {\n listener();\n } catch (err) {\n console.error('[bridge-state] Listener error:', err);\n }\n });\n}\n\n/**\n * Update projects\n */\nexport function setProjects(projects: Project[]): void {\n state.projects = projects;\n notifyListeners();\n}\n\n/**\n * Update messages\n */\nexport function setMessages(messages: BridgeMessage[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set selected project\n */\nexport function setSelectedProject(projectId: string | null): void {\n state.selectedProjectId = projectId;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnected(connected: boolean): void {\n state.isConnected = connected;\n if (connected && !state.connectionStart) {\n state.connectionStart = Date.now();\n }\n notifyListeners();\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Get all agents across all projects\n */\nexport function getAllAgents(): { name: string; projectId: string; projectName: string; cli?: string }[] {\n const agents: { name: string; projectId: string; projectName: string; cli?: string }[] = [];\n\n state.projects.forEach((project) => {\n (project.agents || []).forEach((agent) => {\n agents.push({\n name: agent.name,\n projectId: project.id,\n projectName: project.name || project.id,\n cli: agent.cli,\n });\n });\n });\n\n return agents;\n}\n\n/**\n * Get connected projects\n */\nexport function getConnectedProjects(): Project[] {\n return state.projects.filter((p) => p.connected);\n}\n\n/**\n * Get project by ID\n */\nexport function getProject(projectId: string): Project | undefined {\n return state.projects.find((p) => p.id === projectId);\n}\n\n/**\n * Get uptime formatted string\n */\nexport function getUptimeString(): string {\n if (!state.connectionStart) return '--';\n\n const ms = Date.now() - state.connectionStart;\n const seconds = Math.floor(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m`;\n\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${minutes % 60}m`;\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Bridge Dashboard Application Entry Point\n */\n\nimport { subscribe, state, setProjects, setMessages, setConnected, setWebSocket, setSelectedProject, getUptimeString, getConnectedProjects, getAllAgents, getProject } from './state.js';\nimport type { BridgeDOMElements } from './types.js';\nimport { escapeHtml, formatTime } from '../utils.js';\n\nlet elements: BridgeDOMElements;\n\n/**\n * Initialize DOM element references\n */\nfunction initElements(): BridgeDOMElements {\n return {\n statusDot: document.getElementById('status-dot')!,\n projectList: document.getElementById('project-list')!,\n cardsGrid: document.getElementById('cards-grid')!,\n emptyState: document.getElementById('empty-state')!,\n messagesList: document.getElementById('messages-list')!,\n searchBar: document.getElementById('search-bar')!,\n paletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteProjectsSection: document.getElementById('palette-projects-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n channelName: document.getElementById('channel-name')!,\n statAgents: document.getElementById('stat-agents')!,\n statMessages: document.getElementById('stat-messages')!,\n composerProject: document.getElementById('composer-project') as HTMLSelectElement,\n composerAgent: document.getElementById('composer-agent') as HTMLSelectElement,\n composerMessage: document.getElementById('composer-message') as HTMLInputElement,\n composerSend: document.getElementById('composer-send') as HTMLButtonElement,\n composerStatus: document.getElementById('composer-status')!,\n uptime: document.getElementById('uptime')!,\n };\n}\n\n/**\n * Update connection status indicator\n */\nfunction updateConnectionStatus(): void {\n elements.statusDot.classList.toggle('offline', !state.isConnected);\n}\n\n/**\n * Render sidebar projects list\n */\nfunction renderSidebarProjects(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.projectList.innerHTML = '
  • No projects
  • ';\n document.getElementById('project-count')!.textContent = '0';\n return;\n }\n\n document.getElementById('project-count')!.textContent = String(projects.length);\n\n elements.projectList.innerHTML = projects.map((p) => `\n
  • \n \n ${escapeHtml(p.name || p.id)}\n \n
  • \n `).join('');\n}\n\n/**\n * Render project cards grid\n */\nfunction renderProjectCards(): void {\n const { projects, selectedProjectId } = state;\n\n if (!projects || projects.length === 0) {\n elements.cardsGrid.innerHTML = '';\n elements.cardsGrid.appendChild(elements.emptyState);\n elements.emptyState.style.display = 'flex';\n return;\n }\n\n elements.emptyState.style.display = 'none';\n\n elements.cardsGrid.innerHTML = projects.map((p) => {\n const agents = p.agents || [];\n const agentsHtml = agents.length > 0\n ? agents.map((a) => `\n
    \n \n ${escapeHtml(a.name)}\n ${escapeHtml(a.cli || '')}\n
    \n `).join('')\n : '
    No agents connected
    ';\n\n const isSelected = selectedProjectId === p.id;\n return `\n
    \n
    \n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${escapeHtml(p.path || '')}
    \n
    \n
    \n
    \n \n ${p.connected ? 'Online' : p.reconnecting ? 'Reconnecting...' : 'Offline'}\n
    \n
    \n\n
    \n
    \n Agents\n ${agents.length} active\n
    \n
    \n ${agentsHtml}\n
    \n
    \n\n
    \n \n \n
    \n
    \n `;\n }).join('');\n}\n\n/**\n * Render messages list\n */\nfunction renderMessages(): void {\n const { messages } = state;\n\n if (!messages || messages.length === 0) {\n elements.messagesList.innerHTML = '

    No messages yet

    ';\n return;\n }\n\n elements.messagesList.innerHTML = messages.slice(-50).reverse().map((m) => `\n
    \n
    \n ${escapeHtml(m.sourceProject || 'local')}\n ${escapeHtml(m.from)}\n \u2192\n ${escapeHtml(m.to || '*')}\n ${formatTime(m.timestamp)}\n
    \n
    ${escapeHtml(m.body || m.content || '')}
    \n
    \n `).join('');\n}\n\n/**\n * Update stats display\n */\nfunction updateStats(): void {\n const allAgents = getAllAgents();\n elements.statAgents.textContent = String(allAgents.length);\n elements.statMessages.textContent = String(state.messages.length);\n}\n\n/**\n * Update composer project options\n */\nfunction updateComposerProjects(): void {\n const connectedProjects = getConnectedProjects();\n const currentValue = elements.composerProject.value;\n\n elements.composerProject.innerHTML = '' +\n connectedProjects.map((p) =>\n ``\n ).join('');\n\n // Restore selection if still valid\n if (currentValue && connectedProjects.some((p) => p.id === currentValue)) {\n elements.composerProject.value = currentValue;\n } else if (state.selectedProjectId && connectedProjects.some((p) => p.id === state.selectedProjectId)) {\n elements.composerProject.value = state.selectedProjectId;\n updateComposerAgents();\n }\n}\n\n/**\n * Update composer agent options\n */\nfunction updateComposerAgents(): void {\n const projectId = elements.composerProject.value;\n if (!projectId) {\n elements.composerAgent.innerHTML = '';\n elements.composerAgent.disabled = true;\n elements.composerMessage.disabled = true;\n elements.composerSend.disabled = true;\n return;\n }\n\n const currentAgent = elements.composerAgent.value;\n const project = getProject(projectId);\n const agents = project?.agents || [];\n\n elements.composerAgent.innerHTML = '' +\n '' +\n '' +\n agents.map((a) =>\n ``\n ).join('');\n\n elements.composerAgent.disabled = false;\n\n // Restore agent selection if still valid\n if (currentAgent) {\n const validAgents = ['*', 'lead', ...agents.map((a) => a.name)];\n if (validAgents.includes(currentAgent)) {\n elements.composerAgent.value = currentAgent;\n }\n }\n}\n\n/**\n * Update composer state based on selections\n */\nfunction updateComposerState(): void {\n const hasProject = !!elements.composerProject.value;\n const hasAgent = !!elements.composerAgent.value;\n const hasMessage = elements.composerMessage.value.trim().length > 0;\n\n elements.composerMessage.disabled = !hasProject || !hasAgent;\n elements.composerSend.disabled = !hasProject || !hasAgent || !hasMessage;\n}\n\n/**\n * Send message via bridge API\n */\nasync function sendBridgeMessage(): Promise {\n const projectId = elements.composerProject.value;\n const to = elements.composerAgent.value;\n const message = elements.composerMessage.value.trim();\n\n if (!projectId || !to || !message) return;\n\n elements.composerSend.disabled = true;\n elements.composerStatus.textContent = 'Sending...';\n elements.composerStatus.className = 'composer-status';\n\n try {\n const response = await fetch('/api/bridge/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ projectId, to, message }),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n elements.composerStatus.textContent = 'Message sent!';\n elements.composerStatus.className = 'composer-status success';\n elements.composerMessage.value = '';\n setTimeout(() => {\n elements.composerStatus.textContent = '';\n elements.composerStatus.className = 'composer-status';\n }, 2000);\n } else {\n throw new Error(result.error || 'Failed to send');\n }\n } catch (err) {\n elements.composerStatus.textContent = (err as Error).message || 'Failed to send message';\n elements.composerStatus.className = 'composer-status error';\n }\n\n updateComposerState();\n}\n\n/**\n * Update header for project selection\n */\nfunction updateHeader(): void {\n const { selectedProjectId } = state;\n\n if (selectedProjectId) {\n const project = getProject(selectedProjectId);\n if (project) {\n elements.channelName.innerHTML = `\n \u2190 All Projects\n ${escapeHtml(project.name || project.id)}\n `;\n }\n } else {\n elements.channelName.textContent = 'All Projects';\n }\n}\n\n/**\n * Select a project\n */\nfunction selectProject(projectId: string | null): void {\n setSelectedProject(projectId);\n\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n updateComposerState();\n }\n\n // Update card selection visually\n document.querySelectorAll('.project-card').forEach((card) => {\n card.classList.toggle('selected', (card as HTMLElement).dataset.projectId === projectId);\n });\n}\n\n/**\n * Open command palette\n */\nfunction openPalette(): void {\n elements.paletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n updatePaletteResults();\n}\n\n/**\n * Close command palette\n */\nfunction closePalette(): void {\n elements.paletteOverlay.classList.remove('visible');\n}\n\n/**\n * Update palette search results\n */\nfunction updatePaletteResults(): void {\n const query = elements.paletteSearch.value.toLowerCase();\n const { projects } = state;\n\n // Update projects section\n const filteredProjects = query\n ? projects.filter((p) => (p.name || p.id).toLowerCase().includes(query))\n : projects;\n\n if (filteredProjects.length > 0) {\n elements.paletteProjectsSection.innerHTML = `\n
    Open Project Dashboard
    \n ${filteredProjects.map((p) => `\n
    \n
    \n \n \n \n \n \n
    \n
    \n
    ${escapeHtml(p.name || p.id)}
    \n
    ${p.connected ? 'Online' : 'Offline'} \u00B7 ${(p.agents || []).length} agents \u00B7 Click to open dashboard
    \n
    \n
    \n \u23CE\n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteProjectsSection.innerHTML = '
    Open Project Dashboard
    ';\n }\n\n // Update agents section\n const allAgents = getAllAgents();\n const filteredAgents = query\n ? allAgents.filter((a) => a.name.toLowerCase().includes(query))\n : allAgents;\n\n if (filteredAgents.length > 0) {\n elements.paletteAgentsSection.innerHTML = `\n
    Message Agent
    \n ${filteredAgents.map((a) => `\n
    \n
    \n \n \n \n \n
    \n
    \n
    ${escapeHtml(a.name)}
    \n
    ${escapeHtml(a.projectName)} \u00B7 ${escapeHtml(a.cli || 'unknown')}
    \n
    \n
    \n `).join('')}\n `;\n } else {\n elements.paletteAgentsSection.innerHTML = '
    Message Agent
    ';\n }\n}\n\n/**\n * Set up event listeners\n */\nfunction setupEventListeners(): void {\n // Search bar opens palette\n elements.searchBar.addEventListener('click', openPalette);\n\n // Palette overlay click closes\n elements.paletteOverlay.addEventListener('click', (e) => {\n if (e.target === elements.paletteOverlay) closePalette();\n });\n\n // Palette search filtering\n elements.paletteSearch.addEventListener('input', updatePaletteResults);\n\n // Keyboard shortcuts\n document.addEventListener('keydown', (e) => {\n // Cmd/Ctrl + K to open palette\n if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n } else {\n openPalette();\n }\n }\n // Escape to close\n if (e.key === 'Escape' && elements.paletteOverlay.classList.contains('visible')) {\n closePalette();\n }\n });\n\n // Palette item clicks\n elements.paletteResults.addEventListener('click', (e) => {\n const item = (e.target as HTMLElement).closest('.palette-item') as HTMLElement | null;\n if (!item) return;\n\n const command = item.dataset.command;\n const projectId = item.dataset.project;\n const agentName = item.dataset.agent;\n const action = item.dataset.action;\n\n if (command === 'broadcast') {\n closePalette();\n elements.composerMessage.focus();\n elements.composerStatus.textContent = 'Select a project and agent to send a message';\n } else if (command === 'refresh') {\n closePalette();\n location.reload();\n } else if (command === 'go-dashboard') {\n closePalette();\n window.location.href = '/';\n } else if (action === 'open-dashboard' && projectId) {\n closePalette();\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n } else if (agentName && projectId) {\n closePalette();\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = agentName;\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n } else if (projectId) {\n closePalette();\n selectProject(projectId);\n const card = document.querySelector(`.project-card[data-project-id=\"${projectId}\"]`);\n if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });\n }\n });\n\n // Project card clicks\n elements.cardsGrid.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Handle \"Open Dashboard\" button\n const dashboardBtn = target.closest('[data-open-dashboard]') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.openDashboard;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n // Handle \"Message Lead\" button\n const messageLeadBtn = target.closest('[data-message-lead]') as HTMLButtonElement | null;\n if (messageLeadBtn && !messageLeadBtn.disabled) {\n e.stopPropagation();\n const projectId = messageLeadBtn.dataset.messageLead;\n if (projectId) {\n elements.composerProject.value = projectId;\n updateComposerAgents();\n setTimeout(() => {\n elements.composerAgent.value = 'lead';\n updateComposerState();\n elements.composerMessage.focus();\n }, 50);\n }\n return;\n }\n\n const card = target.closest('.project-card') as HTMLElement | null;\n if (card) {\n selectProject(card.dataset.projectId || null);\n }\n });\n\n // Sidebar project clicks\n elements.projectList.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n\n // Check if dashboard button was clicked\n const dashboardBtn = target.closest('.project-dashboard-btn') as HTMLElement | null;\n if (dashboardBtn) {\n e.stopPropagation();\n const projectId = dashboardBtn.dataset.dashboardProject;\n if (projectId) {\n window.location.href = `/project/${encodeURIComponent(projectId)}`;\n }\n return;\n }\n\n const item = target.closest('.project-item') as HTMLElement | null;\n if (item) {\n selectProject(item.dataset.projectId || null);\n }\n });\n\n // Header back link\n elements.channelName.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (target.id === 'back-to-all' || target.classList.contains('back-link')) {\n selectProject(null);\n }\n });\n\n // Composer events\n elements.composerProject.addEventListener('change', () => {\n updateComposerAgents();\n updateComposerState();\n });\n\n elements.composerAgent.addEventListener('change', updateComposerState);\n elements.composerMessage.addEventListener('input', updateComposerState);\n\n elements.composerSend.addEventListener('click', sendBridgeMessage);\n elements.composerMessage.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' && !e.shiftKey && !elements.composerSend.disabled) {\n e.preventDefault();\n sendBridgeMessage();\n }\n });\n}\n\n/**\n * Connect to WebSocket\n */\nfunction connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws/bridge`);\n\n ws.onopen = () => {\n setConnected(true);\n setWebSocket(ws);\n };\n\n ws.onclose = () => {\n setConnected(false);\n setWebSocket(null);\n setTimeout(connect, 3000);\n };\n\n ws.onerror = () => {\n setConnected(false);\n };\n\n ws.onmessage = (e) => {\n try {\n const data = JSON.parse(e.data);\n setProjects(data.projects || []);\n setMessages(data.messages || []);\n } catch (err) {\n console.error('[bridge] Parse error:', err);\n }\n };\n}\n\n/**\n * Initialize the bridge application\n */\nexport function initBridgeApp(): void {\n elements = initElements();\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderSidebarProjects();\n renderProjectCards();\n renderMessages();\n updateStats();\n updateComposerProjects();\n updateHeader();\n if (elements.composerProject.value) {\n updateComposerAgents();\n updateComposerState();\n }\n });\n\n // Set up event listeners\n setupEventListeners();\n\n // Connect to WebSocket\n connect();\n\n // Update uptime periodically\n setInterval(() => {\n elements.uptime.textContent = `Uptime: ${getUptimeString()}`;\n }, 1000);\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initBridgeApp);\n } else {\n initBridgeApp();\n }\n}\n"], "mappings": "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", "names": ["state", "listeners", "subscribe", "listener", "index", "notifyListeners", "err", "setProjects", "projects", "setMessages", "messages", "setSelectedProject", "projectId", "setConnected", "connected", "setWebSocket", "ws", "getAllAgents", "agents", "project", "agent", "getConnectedProjects", "p", "getProject", "getUptimeString", "ms", "seconds", "minutes", "escapeHtml", "text", "div", "formatTime", "timestamp", "elements", "initElements", "updateConnectionStatus", "state", "renderSidebarProjects", "projects", "selectedProjectId", "p", "escapeHtml", "renderProjectCards", "agents", "agentsHtml", "a", "isSelected", "renderMessages", "messages", "m", "formatTime", "updateStats", "allAgents", "getAllAgents", "updateComposerProjects", "connectedProjects", "getConnectedProjects", "currentValue", "updateComposerAgents", "projectId", "currentAgent", "getProject", "updateComposerState", "hasProject", "hasAgent", "hasMessage", "sendBridgeMessage", "to", "message", "response", "result", "err", "updateHeader", "project", "selectProject", "setSelectedProject", "card", "openPalette", "updatePaletteResults", "closePalette", "query", "filteredProjects", "filteredAgents", "setupEventListeners", "item", "command", "agentName", "action", "target", "dashboardBtn", "messageLeadBtn", "connect", "protocol", "ws", "setConnected", "setWebSocket", "e", "data", "setProjects", "setMessages", "initBridgeApp", "subscribe", "getUptimeString"] } From 7fef0373cb8f0c35909ca05ec78a4f9e17dfc470 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Dec 2025 08:24:50 +0000 Subject: [PATCH 3/4] feat: add spawn and kill agent functionality to dashboard and bridge - Add /api/agent/spawn and /api/agent/kill endpoints to dashboard server - Add spawn agent modal in dashboard with CLI selection (claude, codex, gemini) and optional model - Add kill button (X) that appears on hover for each agent in the sidebar - Add spawn/kill functionality to bridge view with project-aware spawning - Spawn button appears on project cards in bridge view - Kill button appears on each agent item in project cards - Uses AgentSpawner to create agents via tmux sessions --- src/dashboard/frontend/app.ts | 195 +++++++++++- src/dashboard/frontend/components.ts | 21 +- src/dashboard/public/bridge.html | 426 ++++++++++++++++++++++++++- src/dashboard/public/index.html | 306 +++++++++++++++++++ src/dashboard/public/js/app.js | 92 +++--- src/dashboard/public/js/app.js.map | 6 +- src/dashboard/server.ts | 155 ++++++++++ 7 files changed, 1152 insertions(+), 49 deletions(-) diff --git a/src/dashboard/frontend/app.ts b/src/dashboard/frontend/app.ts index e434a9f9c..6c20d8f31 100644 --- a/src/dashboard/frontend/app.ts +++ b/src/dashboard/frontend/app.ts @@ -392,11 +392,204 @@ async function handleThreadSend(): Promise { elements.threadSendBtn.disabled = false; } +/** + * Spawn Agent Modal Management + */ +interface SpawnModalElements { + overlay: HTMLElement | null; + closeBtn: HTMLElement | null; + cancelBtn: HTMLElement | null; + submitBtn: HTMLElement | null; + nameInput: HTMLInputElement | null; + cliSelect: HTMLSelectElement | null; + modelInput: HTMLInputElement | null; + taskInput: HTMLTextAreaElement | null; +} + +function getSpawnModalElements(): SpawnModalElements { + return { + overlay: document.getElementById('spawn-modal-overlay'), + closeBtn: document.getElementById('spawn-modal-close'), + cancelBtn: document.getElementById('spawn-modal-cancel'), + submitBtn: document.getElementById('spawn-modal-submit'), + nameInput: document.getElementById('spawn-agent-name') as HTMLInputElement, + cliSelect: document.getElementById('spawn-agent-cli') as HTMLSelectElement, + modelInput: document.getElementById('spawn-agent-model') as HTMLInputElement, + taskInput: document.getElementById('spawn-agent-task') as HTMLTextAreaElement, + }; +} + +function openSpawnModal(): void { + const modal = getSpawnModalElements(); + if (modal.overlay) { + modal.overlay.classList.add('visible'); + modal.nameInput?.focus(); + } +} + +function closeSpawnModal(): void { + const modal = getSpawnModalElements(); + if (modal.overlay) { + modal.overlay.classList.remove('visible'); + // Clear form + if (modal.nameInput) modal.nameInput.value = ''; + if (modal.cliSelect) modal.cliSelect.value = 'claude'; + if (modal.modelInput) modal.modelInput.value = ''; + if (modal.taskInput) modal.taskInput.value = ''; + } +} + +async function handleSpawnAgent(): Promise { + const modal = getSpawnModalElements(); + + const name = modal.nameInput?.value.trim(); + const cli = modal.cliSelect?.value; + const model = modal.modelInput?.value.trim(); + const task = modal.taskInput?.value.trim(); + + if (!name) { + alert('Please enter an agent name'); + return; + } + + if (!cli) { + alert('Please select a CLI tool'); + return; + } + + // Disable submit button + if (modal.submitBtn) { + modal.submitBtn.textContent = 'Spawning...'; + (modal.submitBtn as HTMLButtonElement).disabled = true; + } + + try { + const response = await fetch('/api/agent/spawn', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + name, + cli, + model: model || undefined, + task: task || undefined, + }), + }); + + const result = await response.json(); + + if (response.ok && result.success) { + closeSpawnModal(); + // Agent will appear in the list when it connects to the relay + } else { + alert(result.error || 'Failed to spawn agent'); + } + } catch (err) { + console.error('Failed to spawn agent:', err); + alert('Failed to spawn agent. Check console for details.'); + } finally { + if (modal.submitBtn) { + modal.submitBtn.textContent = 'Spawn Agent'; + (modal.submitBtn as HTMLButtonElement).disabled = false; + } + } +} + +/** + * Kill an agent + */ +async function killAgent(agentName: string): Promise { + if (!confirm(`Are you sure you want to kill agent "${agentName}"?`)) { + return; + } + + try { + const response = await fetch('/api/agent/kill', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: agentName }), + }); + + const result = await response.json(); + + if (!response.ok) { + alert(result.error || 'Failed to kill agent'); + } + // Agent will disappear from the list when it disconnects + } catch (err) { + console.error('Failed to kill agent:', err); + alert('Failed to kill agent. Check console for details.'); + } +} + +/** + * Set up spawn modal event listeners + */ +function setupSpawnModalListeners(): void { + const modal = getSpawnModalElements(); + + // Spawn button in sidebar + const spawnBtn = document.getElementById('spawn-agent-btn'); + if (spawnBtn) { + spawnBtn.addEventListener('click', (e) => { + e.stopPropagation(); + openSpawnModal(); + }); + } + + // Modal close buttons + modal.closeBtn?.addEventListener('click', closeSpawnModal); + modal.cancelBtn?.addEventListener('click', closeSpawnModal); + + // Modal submit + modal.submitBtn?.addEventListener('click', handleSpawnAgent); + + // Close on overlay click + modal.overlay?.addEventListener('click', (e) => { + if (e.target === modal.overlay) { + closeSpawnModal(); + } + }); + + // Close on Escape + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && modal.overlay?.classList.contains('visible')) { + closeSpawnModal(); + } + }); + + // Enter to submit (when name input is focused) + modal.nameInput?.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + handleSpawnAgent(); + } + }); +} + +/** + * Attach kill handlers to agent list items + * Called after agents are rendered + */ +export function attachKillHandlers(): void { + document.querySelectorAll('.agent-kill-btn').forEach((btn) => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const agentName = btn.dataset.agent; + if (agentName) { + killAgent(agentName); + } + }); + }); +} + // Auto-initialize when DOM is ready if (typeof document !== 'undefined') { if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initApp); + document.addEventListener('DOMContentLoaded', () => { + initApp(); + setupSpawnModalListeners(); + }); } else { initApp(); + setupSpawnModalListeners(); } } diff --git a/src/dashboard/frontend/components.ts b/src/dashboard/frontend/components.ts index 3b73401a9..2626d9fe0 100644 --- a/src/dashboard/frontend/components.ts +++ b/src/dashboard/frontend/components.ts @@ -91,6 +91,14 @@ export function renderAgents(): void {
    ${escapeHtml(agent.name)} ${agent.needsAttention ? 'Needs Input' : ''} +
    + +
    `; }) @@ -102,7 +110,11 @@ export function renderAgents(): void { // Add click handlers elements.agentsList.querySelectorAll('.channel-item[data-agent]').forEach((item) => { - item.addEventListener('click', () => { + item.addEventListener('click', (e) => { + // Don't navigate if clicking on action buttons + if ((e.target as HTMLElement).closest('.agent-actions')) { + return; + } const agentName = item.dataset.agent; if (agentName) { selectChannel(agentName); @@ -112,6 +124,13 @@ export function renderAgents(): void { // Update command palette agents updatePaletteAgents(); + + // Attach kill handlers (dynamically imported to avoid circular dependency) + import('./app.js').then(({ attachKillHandlers }) => { + attachKillHandlers(); + }).catch(() => { + // Ignore if app.js not yet ready + }); } /** diff --git a/src/dashboard/public/bridge.html b/src/dashboard/public/bridge.html index fea4a2f86..747cee17a 100644 --- a/src/dashboard/public/bridge.html +++ b/src/dashboard/public/bridge.html @@ -1070,6 +1070,239 @@ ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.2); } + + /* ======================================== + Spawn Agent Modal + ======================================== */ + .modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + align-items: center; + justify-content: center; + z-index: 1100; + } + + .modal-overlay.visible { + display: flex; + } + + .modal { + background: var(--bg-sidebar); + border-radius: 12px; + box-shadow: 0 18px 50px rgba(0, 0, 0, 0.6); + width: 440px; + max-width: 90vw; + animation: modalSlideIn 0.2s ease-out; + } + + @keyframes modalSlideIn { + from { opacity: 0; transform: scale(0.95) translateY(-10px); } + to { opacity: 1; transform: scale(1) translateY(0); } + } + + .modal-header { + padding: 20px 24px 16px; + border-bottom: 1px solid var(--border-divider); + display: flex; + align-items: center; + justify-content: space-between; + } + + .modal-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + } + + .modal-close { + width: 32px; + height: 32px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); + } + + .modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); + } + + .modal-body { + padding: 20px 24px; + } + + .form-group { + margin-bottom: 16px; + } + + .form-group:last-child { + margin-bottom: 0; + } + + .form-label { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 6px; + } + + .form-input { + width: 100%; + padding: 10px 12px; + background: var(--bg-main); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font-family); + font-size: 14px; + outline: none; + transition: border-color var(--transition-fast); + } + + .form-input:focus { + border-color: var(--accent-primary); + } + + .form-input::placeholder { + color: var(--text-muted); + } + + .form-select { + width: 100%; + padding: 10px 12px; + background: var(--bg-main); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font-family); + font-size: 14px; + outline: none; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ababad' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + } + + .form-select:focus { + border-color: var(--accent-primary); + } + + .form-hint { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + } + + .modal-footer { + padding: 16px 24px 20px; + display: flex; + justify-content: flex-end; + gap: 12px; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-family: var(--font-family); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); + } + + .btn-secondary { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-subtle); + } + + .btn-secondary:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary); + } + + .btn-primary { + background: var(--accent-primary); + color: white; + } + + .btn-primary:hover { + background: #0b5d99; + } + + .btn-primary:disabled { + background: var(--border-subtle); + cursor: not-allowed; + } + + /* Spawn button in card */ + .spawn-agent-btn { + padding: 8px 12px; + background: rgba(43, 172, 118, 0.15); + border: 1px solid rgba(43, 172, 118, 0.3); + border-radius: 6px; + color: var(--accent-green); + font-size: 13px; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all var(--transition-fast); + } + + .spawn-agent-btn:hover { + background: rgba(43, 172, 118, 0.25); + border-color: var(--accent-green); + } + + .spawn-agent-btn svg { + width: 14px; + height: 14px; + } + + /* Kill button in agent item */ + .agent-kill-btn { + width: 20px; + height: 20px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all var(--transition-fast); + margin-left: auto; + } + + .agent-item:hover .agent-kill-btn { + opacity: 1; + } + + .agent-kill-btn:hover { + background: rgba(224, 30, 90, 0.2); + color: var(--accent-red); + } + + .agent-kill-btn svg { + width: 12px; + height: 12px; + } @@ -1730,6 +1963,12 @@

    All Projects

    ${escapeHtml(a.name)} ${escapeHtml(a.cli || '')} +
    `).join('') : '
    No agents connected
    '; @@ -1766,11 +2005,18 @@

    All Projects

    +
    @@ -1875,6 +2121,182 @@

    All Projects

    connect(); + + + + + diff --git a/src/dashboard/public/index.html b/src/dashboard/public/index.html index f9e2804c1..b7238c31b 100644 --- a/src/dashboard/public/index.html +++ b/src/dashboard/public/index.html @@ -1419,6 +1419,264 @@ clip: rect(0, 0, 0, 0); border: 0; } + + /* ======================================== + Spawn Agent Modal + ======================================== */ + .modal-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.7); + display: none; + align-items: center; + justify-content: center; + z-index: 1100; + } + + .modal-overlay.visible { + display: flex; + } + + .modal { + background: var(--bg-modal); + border-radius: 12px; + box-shadow: var(--shadow-modal); + width: 440px; + max-width: 90vw; + animation: modalSlideIn 0.2s ease-out; + } + + @keyframes modalSlideIn { + from { + opacity: 0; + transform: scale(0.95) translateY(-10px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } + } + + .modal-header { + padding: 20px 24px 16px; + border-bottom: 1px solid var(--border-divider); + display: flex; + align-items: center; + justify-content: space-between; + } + + .modal-title { + font-size: 18px; + font-weight: 700; + color: var(--text-primary); + } + + .modal-close { + width: 32px; + height: 32px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); + } + + .modal-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); + } + + .modal-body { + padding: 20px 24px; + } + + .form-group { + margin-bottom: 16px; + } + + .form-group:last-child { + margin-bottom: 0; + } + + .form-label { + display: block; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary); + margin-bottom: 6px; + } + + .form-input { + width: 100%; + padding: 10px 12px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font-family); + font-size: 14px; + outline: none; + transition: border-color var(--transition-fast); + } + + .form-input:focus { + border-color: var(--accent-primary); + } + + .form-input::placeholder { + color: var(--text-muted); + } + + .form-select { + width: 100%; + padding: 10px 12px; + background: var(--bg-input); + border: 1px solid var(--border-subtle); + border-radius: 6px; + color: var(--text-primary); + font-family: var(--font-family); + font-size: 14px; + outline: none; + cursor: pointer; + transition: border-color var(--transition-fast); + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ababad' stroke-width='2'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 12px center; + } + + .form-select:focus { + border-color: var(--accent-primary); + } + + .form-select option { + background: var(--bg-modal); + color: var(--text-primary); + } + + .form-hint { + font-size: 12px; + color: var(--text-muted); + margin-top: 4px; + } + + .modal-footer { + padding: 16px 24px 20px; + display: flex; + justify-content: flex-end; + gap: 12px; + } + + .btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-family: var(--font-family); + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: all var(--transition-fast); + } + + .btn-secondary { + background: transparent; + color: var(--text-secondary); + border: 1px solid var(--border-subtle); + } + + .btn-secondary:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary); + } + + .btn-primary { + background: var(--accent-primary); + color: white; + } + + .btn-primary:hover { + background: #0b5d99; + } + + .btn-primary:disabled { + background: var(--border-subtle); + cursor: not-allowed; + } + + .btn-danger { + background: var(--accent-red); + color: white; + } + + .btn-danger:hover { + background: #c41a4f; + } + + /* Agent Kill Button */ + .agent-actions { + display: flex; + gap: 4px; + margin-left: auto; + opacity: 0; + transition: opacity var(--transition-fast); + } + + .channel-item:hover .agent-actions { + opacity: 1; + } + + .agent-action-btn { + width: 22px; + height: 22px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition-fast); + } + + .agent-action-btn:hover { + background: rgba(224, 30, 90, 0.2); + color: var(--accent-red); + } + + .agent-action-btn.kill-btn:hover { + background: rgba(224, 30, 90, 0.2); + color: var(--accent-red); + } + + /* Spawn Agent Button in Section Header */ + .spawn-agent-btn { + width: 24px; + height: 24px; + border: none; + background: transparent; + color: var(--text-muted); + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all var(--transition-fast); + } + + .section-header:hover .spawn-agent-btn { + opacity: 1; + } + + .spawn-agent-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--accent-green); + } @@ -1475,6 +1733,12 @@ Agents
    +
      @@ -1720,6 +1984,48 @@
    + + + diff --git a/src/dashboard/public/js/app.js b/src/dashboard/public/js/app.js index 03138d7dc..c12010475 100644 --- a/src/dashboard/public/js/app.js +++ b/src/dashboard/public/js/app.js @@ -1,50 +1,58 @@ -var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConnected:!1,ws:null,reconnectAttempts:0},E=[];function N(t){return E.push(t),()=>{let e=E.indexOf(t);e>-1&&E.splice(e,1)}}function L(){E.forEach(t=>t())}function q(t){r.agents=t,L()}function R(t){r.messages=t,L()}function K(t){r.currentChannel=t,L()}function x(t){r.isConnected=t,t&&(r.reconnectAttempts=0),L()}function V(){r.reconnectAttempts++}function z(t){r.ws=t}function W(){let{messages:t,currentChannel:e}=r;return e==="general"?t:t.filter(n=>n.from===e||n.to===e)}function S(t){r.currentThread=t}function U(t){return r.messages.filter(e=>e.thread===t)}function _(t){return r.messages.filter(e=>e.thread===t).length}var F=null;function T(){let t=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${t}//${window.location.host}/ws`);e.onopen=()=>{x(!0)},e.onclose=()=>{x(!1);let n=Math.min(1e3*Math.pow(2,r.reconnectAttempts),3e4);V(),setTimeout(T,n)},e.onerror=n=>{console.error("WebSocket error:",n)},e.onmessage=n=>{try{let s=JSON.parse(n.data);ce(s)}catch(s){console.error("Failed to parse message:",s)}},z(e)}function ce(t){console.log("[WS] Received data:",{agentCount:t.agents?.length,messageCount:t.messages?.length}),t.agents&&(console.log("[WS] Setting agents:",t.agents.map(e=>e.name)),q(t.agents)),t.messages&&R(t.messages),F&&F(t)}async function w(t,e,n){try{let s={to:t,message:e};n&&(s.thread=n);let o=await fetch("/api/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),i=await o.json();return o.ok&&i.success?{success:!0}:{success:!1,error:i.error||"Failed to send message"}}catch{return{success:!1,error:"Network error - could not send message"}}}function b(t){if(!t)return!1;let e=Date.parse(t);return Number.isNaN(e)?!1:Date.now()-e<3e4}function l(t){if(!t)return"";let e=document.createElement("div");return e.textContent=t,e.innerHTML}function C(t){return new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}function J(t){let e=new Date(t),n=new Date,s=new Date(n);return s.setDate(s.getDate()-1),e.toDateString()===n.toDateString()?"Today":e.toDateString()===s.toDateString()?"Yesterday":e.toLocaleDateString([],{weekday:"long",month:"long",day:"numeric"})}function h(t){let e=["#e01e5a","#2bac76","#e8a427","#1264a3","#7c3aed","#0d9488","#dc2626","#9333ea","#ea580c","#0891b2"],n=0;for(let s=0;s$1"),e=e.replace(/`([^`]+)`/g,"$1"),e=e.replace(/\n/g,"
    "),e}var a,c=-1;function X(){return a={connectionDot:document.getElementById("connection-dot"),channelsList:document.getElementById("channels-list"),agentsList:document.getElementById("agents-list"),messagesList:document.getElementById("messages-list"),currentChannelName:document.getElementById("current-channel-name"),channelTopic:document.getElementById("channel-topic"),onlineCount:document.getElementById("online-count"),messageInput:document.getElementById("message-input"),sendBtn:document.getElementById("send-btn"),boldBtn:document.getElementById("bold-btn"),emojiBtn:document.getElementById("emoji-btn"),searchTrigger:document.getElementById("search-trigger"),commandPaletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteChannelsSection:document.getElementById("palette-channels-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),paletteMessagesSection:document.getElementById("palette-messages-section"),typingIndicator:document.getElementById("typing-indicator"),threadPanelOverlay:document.getElementById("thread-panel-overlay"),threadPanelId:document.getElementById("thread-panel-id"),threadPanelClose:document.getElementById("thread-panel-close"),threadMessages:document.getElementById("thread-messages"),threadMessageInput:document.getElementById("thread-message-input"),threadSendBtn:document.getElementById("thread-send-btn"),mentionAutocomplete:document.getElementById("mention-autocomplete"),mentionAutocompleteList:document.getElementById("mention-autocomplete-list")},a}function k(){return a}function Y(){r.isConnected?a.connectionDot.classList.remove("offline"):a.connectionDot.classList.add("offline")}function G(){console.log("[UI] renderAgents called, agents:",r.agents.length,r.agents.map(e=>e.name));let t=r.agents.map(e=>{let s=b(e.lastSeen||e.lastActive)?"online":"",o=r.currentChannel===e.name,i=e.needsAttention?"needs-attention":"";return` -
  • -
    - ${f(e.name)} +var Le=Object.defineProperty;var E=(t,e)=>()=>(t&&(e=t(t=0)),e);var be=(t,e)=>{for(var n in e)Le(t,n,{get:e[n],enumerable:!0})};function V(t){return M.push(t),()=>{let e=M.indexOf(t);e>-1&&M.splice(e,1)}}function w(){M.forEach(t=>t())}function z(t){l.agents=t,w()}function W(t){l.messages=t,w()}function U(t){l.currentChannel=t,w()}function I(t){l.isConnected=t,t&&(l.reconnectAttempts=0),w()}function _(){l.reconnectAttempts++}function J(t){l.ws=t}function Q(){let{messages:t,currentChannel:e}=l;return e==="general"?t:t.filter(n=>n.from===e||n.to===e)}function k(t){l.currentThread=t}function X(t){return l.messages.filter(e=>e.thread===t)}function Y(t){return l.messages.filter(e=>e.thread===t).length}var l,M,L=E(()=>{"use strict";l={agents:[],messages:[],currentChannel:"general",currentThread:null,isConnected:!1,ws:null,reconnectAttempts:0},M=[]});function C(){let t=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${t}//${window.location.host}/ws`);e.onopen=()=>{I(!0)},e.onclose=()=>{I(!1);let n=Math.min(1e3*Math.pow(2,l.reconnectAttempts),3e4);_(),setTimeout(C,n)},e.onerror=n=>{console.error("WebSocket error:",n)},e.onmessage=n=>{try{let s=JSON.parse(n.data);Me(s)}catch(s){console.error("Failed to parse message:",s)}},J(e)}function Me(t){console.log("[WS] Received data:",{agentCount:t.agents?.length,messageCount:t.messages?.length}),t.agents&&(console.log("[WS] Setting agents:",t.agents.map(e=>e.name)),z(t.agents)),t.messages&&W(t.messages),G&&G(t)}async function A(t,e,n){try{let s={to:t,message:e};n&&(s.thread=n);let o=await fetch("/api/send",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)}),i=await o.json();return o.ok&&i.success?{success:!0}:{success:!1,error:i.error||"Failed to send message"}}catch{return{success:!1,error:"Network error - could not send message"}}}var G,Z=E(()=>{"use strict";L();G=null});function S(t){if(!t)return!1;let e=Date.parse(t);return Number.isNaN(e)?!1:Date.now()-e<3e4}function r(t){if(!t)return"";let e=document.createElement("div");return e.textContent=t,e.innerHTML}function B(t){return new Date(t).toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})}function ee(t){let e=new Date(t),n=new Date,s=new Date(n);return s.setDate(s.getDate()-1),e.toDateString()===n.toDateString()?"Today":e.toDateString()===s.toDateString()?"Yesterday":e.toLocaleDateString([],{weekday:"long",month:"long",day:"numeric"})}function f(t){let e=["#e01e5a","#2bac76","#e8a427","#1264a3","#7c3aed","#0d9488","#dc2626","#9333ea","#ea580c","#0891b2"],n=0;for(let s=0;s$1"),e=e.replace(/`([^`]+)`/g,"$1"),e=e.replace(/\n/g,"
    "),e}var te=E(()=>{"use strict"});function se(){return a={connectionDot:document.getElementById("connection-dot"),channelsList:document.getElementById("channels-list"),agentsList:document.getElementById("agents-list"),messagesList:document.getElementById("messages-list"),currentChannelName:document.getElementById("current-channel-name"),channelTopic:document.getElementById("channel-topic"),onlineCount:document.getElementById("online-count"),messageInput:document.getElementById("message-input"),sendBtn:document.getElementById("send-btn"),boldBtn:document.getElementById("bold-btn"),emojiBtn:document.getElementById("emoji-btn"),searchTrigger:document.getElementById("search-trigger"),commandPaletteOverlay:document.getElementById("command-palette-overlay"),paletteSearch:document.getElementById("palette-search"),paletteResults:document.getElementById("palette-results"),paletteChannelsSection:document.getElementById("palette-channels-section"),paletteAgentsSection:document.getElementById("palette-agents-section"),paletteMessagesSection:document.getElementById("palette-messages-section"),typingIndicator:document.getElementById("typing-indicator"),threadPanelOverlay:document.getElementById("thread-panel-overlay"),threadPanelId:document.getElementById("thread-panel-id"),threadPanelClose:document.getElementById("thread-panel-close"),threadMessages:document.getElementById("thread-messages"),threadMessageInput:document.getElementById("thread-message-input"),threadSendBtn:document.getElementById("thread-send-btn"),mentionAutocomplete:document.getElementById("mention-autocomplete"),mentionAutocompleteList:document.getElementById("mention-autocomplete-list")},a}function D(){return a}function ae(){l.isConnected?a.connectionDot.classList.remove("offline"):a.connectionDot.classList.add("offline")}function oe(){console.log("[UI] renderAgents called, agents:",l.agents.length,l.agents.map(e=>e.name));let t=l.agents.map(e=>{let s=S(e.lastSeen||e.lastActive)?"online":"",o=l.currentChannel===e.name,i=e.needsAttention?"needs-attention":"";return` +
  • +
    + ${h(e.name)}
    - ${l(e.name)} + ${r(e.name)} ${e.needsAttention?'Needs Input':""} +
    + +
  • - `}).join("");a.agentsList.innerHTML=t||'
  • No agents connected
  • ',a.agentsList.querySelectorAll(".channel-item[data-agent]").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.agent;n&&p(n)})}),de()}function B(){let t=W();if(t.length===0){a.messagesList.innerHTML=` + `}).join("");a.agentsList.innerHTML=t||'
  • No agents connected
  • ',a.agentsList.querySelectorAll(".channel-item[data-agent]").forEach(e=>{e.addEventListener("click",n=>{if(n.target.closest(".agent-actions"))return;let s=e.dataset.agent;s&&p(s)})}),we(),Promise.resolve().then(()=>(fe(),ge)).then(({attachKillHandlers:e})=>{e()}).catch(()=>{})}function P(){let t=Q();if(t.length===0){a.messagesList.innerHTML=`
    No messages yet
    - ${r.currentChannel==="general"?"Messages between agents will appear here":`Messages with ${r.currentChannel} will appear here`} + ${l.currentChannel==="general"?"Messages between agents will appear here":`Messages with ${l.currentChannel} will appear here`}
    `;return}let e="",n=null;t.forEach(s=>{let o=new Date(s.timestamp).toDateString();o!==n&&(e+=`
    - ${J(s.timestamp)} + ${ee(s.timestamp)}
    - `,n=o);let i=s.to==="*",d=h(s.from),g=_(s.id),y=i?"@everyone":s.project?`${l(s.project)}@${l(s.to)}`:`@${l(s.to)}`;e+=` -
    -
    - ${f(s.from)} + `,n=o);let i=s.to==="*",c=f(s.from),g=Y(s.id),y=i?"@everyone":s.project?`${r(s.project)}@${r(s.to)}`:`@${r(s.to)}`;e+=` +
    +
    + ${h(s.from)}
    - @${l(s.from)} + @${r(s.from)} \u2192 ${y} - ${C(s.timestamp)} + ${B(s.timestamp)}
    -
    ${A(s.content)}
    +
    ${H(s.content)}
    ${s.thread?` -
    +
    - Thread: ${l(s.thread)} + Thread: ${r(s.thread)}
    `:""} ${g>0?` -
    +
    @@ -68,32 +76,32 @@ var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConne
    - `}),a.messagesList.innerHTML=e,ue()}function p(t){K(t),a.channelsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.channel===t)}),a.agentsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.agent===t)});let e=document.querySelector(".channel-header-name .prefix");if(t==="general")a.currentChannelName.innerHTML="general",a.channelTopic.textContent="All agent communications",e&&(e.textContent="#");else{a.currentChannelName.innerHTML=l(t);let n=r.agents.find(s=>s.name===t);a.channelTopic.textContent=n?.status||"Direct messages",e&&(e.textContent="@")}a.messageInput.placeholder=t==="general"?"@AgentName message... (or @* to broadcast)":`@${t} your message here...`,B()}function Z(){let t=r.agents.filter(e=>b(e.lastSeen||e.lastActive)).length;a.onlineCount.textContent=`${t} online`}function de(){let t=r.agents.map(s=>{let o=b(s.lastSeen||s.lastActive);return` -
    + `}),a.messagesList.innerHTML=e,xe()}function p(t){U(t),a.channelsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.channel===t)}),a.agentsList.querySelectorAll(".channel-item").forEach(n=>{n.classList.toggle("active",n.dataset.agent===t)});let e=document.querySelector(".channel-header-name .prefix");if(t==="general")a.currentChannelName.innerHTML="general",a.channelTopic.textContent="All agent communications",e&&(e.textContent="#");else{a.currentChannelName.innerHTML=r(t);let n=l.agents.find(s=>s.name===t);a.channelTopic.textContent=n?.status||"Direct messages",e&&(e.textContent="@")}a.messageInput.placeholder=t==="general"?"@AgentName message... (or @* to broadcast)":`@${t} your message here...`,P()}function ie(){let t=l.agents.filter(e=>S(e.lastSeen||e.lastActive)).length;a.onlineCount.textContent=`${t} online`}function we(){let t=l.agents.map(s=>{let o=S(s.lastSeen||s.lastActive);return` +
    -
    - ${f(s.name)} +
    + ${h(s.name)}
    -
    ${l(s.name)}
    +
    ${r(s.name)}
    ${o?"Online":"Offline"}
    - `}).join(""),e=a.paletteAgentsSection;e.querySelectorAll(".palette-item").forEach(s=>s.remove()),e.insertAdjacentHTML("beforeend",t),e.querySelectorAll(".palette-item[data-jump-agent]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.jumpAgent;o&&(p(o),u())})})}function ee(){a.paletteChannelsSection.querySelectorAll(".palette-item[data-jump-channel]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.jumpChannel;e&&(p(e),u())})})}function $(){a.commandPaletteOverlay.classList.add("visible"),a.paletteSearch.value="",a.paletteSearch.focus(),c=-1,D("")}function te(){return Array.from(a.paletteResults.querySelectorAll(".palette-item")).filter(e=>e.style.display!=="none")}function Q(){let t=te();if(t.forEach(e=>e.classList.remove("selected")),c>=0&&c0?c-1:e.length-1,Q();break;case"Enter":t.preventDefault(),c>=0&&ci.classList.remove("highlighted"),2e3)),u();return}}function u(){a.commandPaletteOverlay.classList.remove("visible")}function D(t){let e=t.toLowerCase();if(c=-1,document.querySelectorAll(".palette-item[data-command]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-channel]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-agent]").forEach(n=>{let s=n.dataset.jumpAgent?.toLowerCase()||"";n.style.display=s.includes(e)?"flex":"none"}),e.length>=2){let n=r.messages.filter(s=>s.content.toLowerCase().includes(e)).slice(0,5);if(n.length>0){a.paletteMessagesSection.style.display="block";let s=n.map(i=>` -
    + `}).join(""),e=a.paletteAgentsSection;e.querySelectorAll(".palette-item").forEach(s=>s.remove()),e.insertAdjacentHTML("beforeend",t),e.querySelectorAll(".palette-item[data-jump-agent]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.jumpAgent;o&&(p(o),u())})})}function le(){a.paletteChannelsSection.querySelectorAll(".palette-item[data-jump-channel]").forEach(t=>{t.addEventListener("click",()=>{let e=t.dataset.jumpChannel;e&&(p(e),u())})})}function j(){a.commandPaletteOverlay.classList.add("visible"),a.paletteSearch.value="",a.paletteSearch.focus(),d=-1,O("")}function re(){return Array.from(a.paletteResults.querySelectorAll(".palette-item")).filter(e=>e.style.display!=="none")}function ne(){let t=re();if(t.forEach(e=>e.classList.remove("selected")),d>=0&&d0?d-1:e.length-1,ne();break;case"Enter":t.preventDefault(),d>=0&&di.classList.remove("highlighted"),2e3)),u();return}}function u(){a.commandPaletteOverlay.classList.remove("visible")}function O(t){let e=t.toLowerCase();if(d=-1,document.querySelectorAll(".palette-item[data-command]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-channel]").forEach(n=>{let o=n.querySelector(".palette-item-title")?.textContent?.toLowerCase()||"";n.style.display=o.includes(e)?"flex":"none"}),document.querySelectorAll(".palette-item[data-jump-agent]").forEach(n=>{let s=n.dataset.jumpAgent?.toLowerCase()||"";n.style.display=s.includes(e)?"flex":"none"}),e.length>=2){let n=l.messages.filter(s=>s.content.toLowerCase().includes(e)).slice(0,5);if(n.length>0){a.paletteMessagesSection.style.display="block";let s=n.map(i=>` +
    -
    ${l(i.from)}
    -
    ${l(i.content.substring(0,60))}${i.content.length>60?"...":""}
    +
    ${r(i.from)}
    +
    ${r(i.content.substring(0,60))}${i.content.length>60?"...":""}
    - `).join("");a.paletteMessagesSection.querySelectorAll(".palette-item").forEach(i=>i.remove()),a.paletteMessagesSection.insertAdjacentHTML("beforeend",s)}else a.paletteMessagesSection.style.display="none"}else a.paletteMessagesSection.style.display="none"}function I(t){S(t),a.threadPanelId.textContent=t,a.threadPanelOverlay.classList.add("visible"),a.threadMessageInput.value="",P(t),a.threadMessageInput.focus()}function H(){S(null),a.threadPanelOverlay.classList.remove("visible")}function P(t){let e=U(t);if(e.length===0){a.threadMessages.innerHTML=` + `).join("");a.paletteMessagesSection.querySelectorAll(".palette-item").forEach(i=>i.remove()),a.paletteMessagesSection.insertAdjacentHTML("beforeend",s)}else a.paletteMessagesSection.style.display="none"}else a.paletteMessagesSection.style.display="none"}function $(t){k(t),a.threadPanelId.textContent=t,a.threadPanelOverlay.classList.add("visible"),a.threadMessageInput.value="",q(t),a.threadMessageInput.focus()}function N(){k(null),a.threadPanelOverlay.classList.remove("visible")}function q(t){let e=X(t);if(e.length===0){a.threadMessages.innerHTML=`

    No messages in this thread yet.

    Start the conversation below!

    @@ -101,27 +109,27 @@ var r={agents:[],messages:[],currentChannel:"general",currentThread:null,isConne `;return}let n=e.map(s=>`
    -
    - ${f(s.from)} +
    + ${h(s.from)}
    - ${l(s.from)} - ${C(s.timestamp)} + ${r(s.from)} + ${B(s.timestamp)}
    -
    ${A(s.content)}
    +
    ${H(s.content)}
    - `).join("");a.threadMessages.innerHTML=n,a.threadMessages.scrollTop=a.threadMessages.scrollHeight}function ue(){a.messagesList.querySelectorAll(".thread-indicator").forEach(t=>{t.style.cursor="pointer",t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&I(n)})}),a.messagesList.querySelectorAll(".reply-count-badge").forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&I(n)})}),a.messagesList.querySelectorAll('.message-action-btn[data-action="reply"]').forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.closest(".message")?.getAttribute("data-id");n&&I(n)})})}var m=0,M=[];function se(t){let e=t.toLowerCase();M=r.agents.filter(s=>s.name.toLowerCase().includes(e)),m=0;let n="";("*".includes(e)||"everyone".includes(e)||"all".includes(e)||"broadcast".includes(e))&&(n+=` -
    + `).join("");a.threadMessages.innerHTML=n,a.threadMessages.scrollTop=a.threadMessages.scrollHeight}function xe(){a.messagesList.querySelectorAll(".thread-indicator").forEach(t=>{t.style.cursor="pointer",t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&$(n)})}),a.messagesList.querySelectorAll(".reply-count-badge").forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.thread;n&&$(n)})}),a.messagesList.querySelectorAll('.message-action-btn[data-action="reply"]').forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.closest(".message")?.getAttribute("data-id");n&&$(n)})})}function de(t){let e=t.toLowerCase();x=l.agents.filter(s=>s.name.toLowerCase().includes(e)),m=0;let n="";("*".includes(e)||"everyone".includes(e)||"all".includes(e)||"broadcast".includes(e))&&(n+=` +
    *
    @everyone Broadcast to all
    - `),M.forEach((s,o)=>{n+=` -
    -
    - ${f(s.name)} + `),x.forEach((s,o)=>{n+=` +
    +
    + ${h(s.name)}
    - @${l(s.name)} - ${l(s.role||"Agent")} + @${r(s.name)} + ${r(s.role||"Agent")}
    - `}),n===""&&(n='
    No matching agents
    '),a.mentionAutocompleteList.innerHTML=n,a.mentionAutocomplete.classList.add("visible"),a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.mention;o&&O(o)})})}function v(){a.mentionAutocomplete.classList.remove("visible"),M=[],m=0}function ae(){return a.mentionAutocomplete.classList.contains("visible")}function j(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]");e.length!==0&&(e[m]?.classList.remove("selected"),t==="down"?m=(m+1)%e.length:m=(m-1+e.length)%e.length,e[m]?.classList.add("selected"),e[m]?.scrollIntoView({block:"nearest"}))}function O(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]"),n=t;if(!n&&e.length>0&&(n=e[m]?.dataset.mention),!n){v();return}let s=a.messageInput,o=s.value,i=o.match(/^@\S*/);if(i){let d=`@${n} `;s.value=d+o.substring(i[0].length),s.selectionStart=s.selectionEnd=d.length}v(),s.focus()}function oe(){let t=a.messageInput,e=t.value,n=t.selectionStart,s=e.match(/^@(\S*)/);return s&&n<=s[0].length?s[1]:null}function pe(){let e=window.location.pathname.match(/^\/project\/([^/]+)$/);return e?{projectId:decodeURIComponent(e[1]),fromBridge:!0}:{projectId:null,fromBridge:!1}}async function ge(t){let e=document.querySelector(".workspace-name");if(e)try{let o=await fetch(`/api/project/${encodeURIComponent(t)}`);if(o.ok){let i=await o.json(),d=e.querySelector(":not(.status-dot)");d&&d.nodeType===Node.TEXT_NODE?d.textContent=i.name||t:(Array.from(e.childNodes).filter(y=>y.nodeType===Node.TEXT_NODE).forEach(y=>y.textContent=""),e.appendChild(document.createTextNode(" "+(i.name||t))))}}catch{}let n=document.getElementById("bridge-link-text"),s=document.getElementById("bridge-nav-link");n&&(n.textContent="\u2190 Back to Bridge"),s&&s.classList.add("back-to-bridge"),document.body.classList.add("project-view")}function ie(){let t=X(),{projectId:e,fromBridge:n}=pe();n&&e&&ge(e),N(()=>{Y(),G(),B(),Z()}),he(t),T()}function he(t){t.channelsList.querySelectorAll(".channel-item").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.channel;n&&p(n)})}),t.sendBtn.addEventListener("click",re),t.messageInput.addEventListener("keydown",e=>{if(ae()){if(e.key==="Tab"||e.key==="Enter"){e.preventDefault(),O();return}if(e.key==="ArrowUp"){e.preventDefault(),j("up");return}if(e.key==="ArrowDown"){e.preventDefault(),j("down");return}if(e.key==="Escape"){e.preventDefault(),v();return}}e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),re())}),t.messageInput.addEventListener("input",()=>{t.messageInput.style.height="auto",t.messageInput.style.height=Math.min(t.messageInput.scrollHeight,200)+"px";let e=oe();e!==null?se(e):v()}),t.messageInput.addEventListener("blur",()=>{setTimeout(()=>{v()},150)}),t.boldBtn.addEventListener("click",()=>{let e=t.messageInput,n=e.selectionStart,s=e.selectionEnd,o=e.value;if(n===s){let i=o.substring(0,n),d=o.substring(s);e.value=i+"**bold**"+d,e.selectionStart=n+2,e.selectionEnd=n+6}else{let i=o.substring(0,n),d=o.substring(n,s),g=o.substring(s);e.value=i+"**"+d+"**"+g,e.selectionStart=n,e.selectionEnd=s+4}e.focus()}),t.emojiBtn.addEventListener("click",()=>{let e=["\u{1F44D}","\u{1F44E}","\u2705","\u274C","\u{1F389}","\u{1F525}","\u{1F4A1}","\u26A0\uFE0F","\u{1F4DD}","\u{1F680}"],n=e[Math.floor(Math.random()*e.length)],s=t.messageInput,o=s.selectionStart,i=s.value;s.value=i.substring(0,o)+n+i.substring(o),s.selectionStart=s.selectionEnd=o+n.length,s.focus()}),t.searchTrigger.addEventListener("click",$),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="k"&&(e.preventDefault(),t.commandPaletteOverlay.classList.contains("visible")?u():$()),e.key==="Escape"&&u()}),t.commandPaletteOverlay.addEventListener("click",e=>{e.target===t.commandPaletteOverlay&&u()}),t.paletteSearch.addEventListener("input",e=>{let n=e.target;D(n.value)}),t.paletteSearch.addEventListener("keydown",ne),document.querySelectorAll(".palette-item[data-command]").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.command;n==="bridge"?window.location.href="/bridge":n==="broadcast"?(t.messageInput.value="@* ",t.messageInput.focus()):n==="clear"&&(t.messagesList.innerHTML=""),u()})}),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="b"&&(e.preventDefault(),window.location.href="/bridge")}),ee(),t.threadPanelClose.addEventListener("click",H),t.threadSendBtn.addEventListener("click",le),t.threadMessageInput.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),le())}),document.addEventListener("keydown",e=>{e.key==="Escape"&&t.threadPanelOverlay.classList.contains("visible")&&H()})}function fe(t){let n=t.trim().match(/^@(\*|[^\s]+)\s+(.+)$/s);return n?{to:n[1],message:n[2].trim()}:null}async function re(){let t=k(),e=t.messageInput.value.trim();if(!e)return;let n=fe(e);if(!n){alert('Message must start with @recipient (e.g., "@Lead hello" or "@* broadcast")');return}let{to:s,message:o}=n;t.sendBtn.disabled=!0;let i=await w(s,o);i.success?(t.messageInput.value="",t.messageInput.style.height="auto"):alert(i.error),t.sendBtn.disabled=!1}async function le(){let t=k(),e=t.threadMessageInput.value.trim(),n=r.currentThread;if(!e||!n)return;t.threadSendBtn.disabled=!0;let s=await w("*",e,n);s.success?(t.threadMessageInput.value="",P(n)):alert(s.error),t.threadSendBtn.disabled=!1}typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",ie):ie());export{ie as initApp}; + `}),n===""&&(n='
    No matching agents
    '),a.mentionAutocompleteList.innerHTML=n,a.mentionAutocomplete.classList.add("visible"),a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]").forEach(s=>{s.addEventListener("click",()=>{let o=s.dataset.mention;o&&R(o)})})}function v(){a.mentionAutocomplete.classList.remove("visible"),x=[],m=0}function me(){return a.mentionAutocomplete.classList.contains("visible")}function K(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]");e.length!==0&&(e[m]?.classList.remove("selected"),t==="down"?m=(m+1)%e.length:m=(m-1+e.length)%e.length,e[m]?.classList.add("selected"),e[m]?.scrollIntoView({block:"nearest"}))}function R(t){let e=a.mentionAutocompleteList.querySelectorAll(".mention-autocomplete-item[data-mention]"),n=t;if(!n&&e.length>0&&(n=e[m]?.dataset.mention),!n){v();return}let s=a.messageInput,o=s.value,i=o.match(/^@\S*/);if(i){let c=`@${n} `;s.value=c+o.substring(i[0].length),s.selectionStart=s.selectionEnd=c.length}v(),s.focus()}function ue(){let t=a.messageInput,e=t.value,n=t.selectionStart,s=e.match(/^@(\S*)/);return s&&n<=s[0].length?s[1]:null}var a,d,m,x,pe=E(()=>{"use strict";L();te();d=-1;m=0,x=[]});var ge={};be(ge,{attachKillHandlers:()=>He,initApp:()=>F});function Te(){let e=window.location.pathname.match(/^\/project\/([^/]+)$/);return e?{projectId:decodeURIComponent(e[1]),fromBridge:!0}:{projectId:null,fromBridge:!1}}async function Ie(t){let e=document.querySelector(".workspace-name");if(e)try{let o=await fetch(`/api/project/${encodeURIComponent(t)}`);if(o.ok){let i=await o.json(),c=e.querySelector(":not(.status-dot)");c&&c.nodeType===Node.TEXT_NODE?c.textContent=i.name||t:(Array.from(e.childNodes).filter(y=>y.nodeType===Node.TEXT_NODE).forEach(y=>y.textContent=""),e.appendChild(document.createTextNode(" "+(i.name||t))))}}catch{}let n=document.getElementById("bridge-link-text"),s=document.getElementById("bridge-nav-link");n&&(n.textContent="\u2190 Back to Bridge"),s&&s.classList.add("back-to-bridge"),document.body.classList.add("project-view")}function F(){let t=se(),{projectId:e,fromBridge:n}=Te();n&&e&&Ie(e),V(()=>{ae(),oe(),P(),ie()}),ke(t),C()}function ke(t){t.channelsList.querySelectorAll(".channel-item").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.channel;n&&p(n)})}),t.sendBtn.addEventListener("click",he),t.messageInput.addEventListener("keydown",e=>{if(me()){if(e.key==="Tab"||e.key==="Enter"){e.preventDefault(),R();return}if(e.key==="ArrowUp"){e.preventDefault(),K("up");return}if(e.key==="ArrowDown"){e.preventDefault(),K("down");return}if(e.key==="Escape"){e.preventDefault(),v();return}}e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),he())}),t.messageInput.addEventListener("input",()=>{t.messageInput.style.height="auto",t.messageInput.style.height=Math.min(t.messageInput.scrollHeight,200)+"px";let e=ue();e!==null?de(e):v()}),t.messageInput.addEventListener("blur",()=>{setTimeout(()=>{v()},150)}),t.boldBtn.addEventListener("click",()=>{let e=t.messageInput,n=e.selectionStart,s=e.selectionEnd,o=e.value;if(n===s){let i=o.substring(0,n),c=o.substring(s);e.value=i+"**bold**"+c,e.selectionStart=n+2,e.selectionEnd=n+6}else{let i=o.substring(0,n),c=o.substring(n,s),g=o.substring(s);e.value=i+"**"+c+"**"+g,e.selectionStart=n,e.selectionEnd=s+4}e.focus()}),t.emojiBtn.addEventListener("click",()=>{let e=["\u{1F44D}","\u{1F44E}","\u2705","\u274C","\u{1F389}","\u{1F525}","\u{1F4A1}","\u26A0\uFE0F","\u{1F4DD}","\u{1F680}"],n=e[Math.floor(Math.random()*e.length)],s=t.messageInput,o=s.selectionStart,i=s.value;s.value=i.substring(0,o)+n+i.substring(o),s.selectionStart=s.selectionEnd=o+n.length,s.focus()}),t.searchTrigger.addEventListener("click",j),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="k"&&(e.preventDefault(),t.commandPaletteOverlay.classList.contains("visible")?u():j()),e.key==="Escape"&&u()}),t.commandPaletteOverlay.addEventListener("click",e=>{e.target===t.commandPaletteOverlay&&u()}),t.paletteSearch.addEventListener("input",e=>{let n=e.target;O(n.value)}),t.paletteSearch.addEventListener("keydown",ce),document.querySelectorAll(".palette-item[data-command]").forEach(e=>{e.addEventListener("click",()=>{let n=e.dataset.command;n==="bridge"?window.location.href="/bridge":n==="broadcast"?(t.messageInput.value="@* ",t.messageInput.focus()):n==="clear"&&(t.messagesList.innerHTML=""),u()})}),document.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="b"&&(e.preventDefault(),window.location.href="/bridge")}),le(),t.threadPanelClose.addEventListener("click",N),t.threadSendBtn.addEventListener("click",ve),t.threadMessageInput.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),ve())}),document.addEventListener("keydown",e=>{e.key==="Escape"&&t.threadPanelOverlay.classList.contains("visible")&&N()})}function Ce(t){let n=t.trim().match(/^@(\*|[^\s]+)\s+(.+)$/s);return n?{to:n[1],message:n[2].trim()}:null}async function he(){let t=D(),e=t.messageInput.value.trim();if(!e)return;let n=Ce(e);if(!n){alert('Message must start with @recipient (e.g., "@Lead hello" or "@* broadcast")');return}let{to:s,message:o}=n;t.sendBtn.disabled=!0;let i=await A(s,o);i.success?(t.messageInput.value="",t.messageInput.style.height="auto"):alert(i.error),t.sendBtn.disabled=!1}async function ve(){let t=D(),e=t.threadMessageInput.value.trim(),n=l.currentThread;if(!e||!n)return;t.threadSendBtn.disabled=!0;let s=await A("*",e,n);s.success?(t.threadMessageInput.value="",q(n)):alert(s.error),t.threadSendBtn.disabled=!1}function T(){return{overlay:document.getElementById("spawn-modal-overlay"),closeBtn:document.getElementById("spawn-modal-close"),cancelBtn:document.getElementById("spawn-modal-cancel"),submitBtn:document.getElementById("spawn-modal-submit"),nameInput:document.getElementById("spawn-agent-name"),cliSelect:document.getElementById("spawn-agent-cli"),modelInput:document.getElementById("spawn-agent-model"),taskInput:document.getElementById("spawn-agent-task")}}function Ae(){let t=T();t.overlay&&(t.overlay.classList.add("visible"),t.nameInput?.focus())}function b(){let t=T();t.overlay&&(t.overlay.classList.remove("visible"),t.nameInput&&(t.nameInput.value=""),t.cliSelect&&(t.cliSelect.value="claude"),t.modelInput&&(t.modelInput.value=""),t.taskInput&&(t.taskInput.value=""))}async function ye(){let t=T(),e=t.nameInput?.value.trim(),n=t.cliSelect?.value,s=t.modelInput?.value.trim(),o=t.taskInput?.value.trim();if(!e){alert("Please enter an agent name");return}if(!n){alert("Please select a CLI tool");return}t.submitBtn&&(t.submitBtn.textContent="Spawning...",t.submitBtn.disabled=!0);try{let i=await fetch("/api/agent/spawn",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,cli:n,model:s||void 0,task:o||void 0})}),c=await i.json();i.ok&&c.success?b():alert(c.error||"Failed to spawn agent")}catch(i){console.error("Failed to spawn agent:",i),alert("Failed to spawn agent. Check console for details.")}finally{t.submitBtn&&(t.submitBtn.textContent="Spawn Agent",t.submitBtn.disabled=!1)}}async function Be(t){if(confirm(`Are you sure you want to kill agent "${t}"?`))try{let e=await fetch("/api/agent/kill",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:t})}),n=await e.json();e.ok||alert(n.error||"Failed to kill agent")}catch(e){console.error("Failed to kill agent:",e),alert("Failed to kill agent. Check console for details.")}}function Ee(){let t=T(),e=document.getElementById("spawn-agent-btn");e&&e.addEventListener("click",n=>{n.stopPropagation(),Ae()}),t.closeBtn?.addEventListener("click",b),t.cancelBtn?.addEventListener("click",b),t.submitBtn?.addEventListener("click",ye),t.overlay?.addEventListener("click",n=>{n.target===t.overlay&&b()}),document.addEventListener("keydown",n=>{n.key==="Escape"&&t.overlay?.classList.contains("visible")&&b()}),t.nameInput?.addEventListener("keydown",n=>{n.key==="Enter"&&ye()})}function He(){document.querySelectorAll(".agent-kill-btn").forEach(t=>{t.addEventListener("click",e=>{e.stopPropagation();let n=t.dataset.agent;n&&Be(n)})})}var fe=E(()=>{L();Z();pe();L();typeof document<"u"&&(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>{F(),Ee()}):(F(),Ee()))});fe();export{He as attachKillHandlers,F as initApp}; //# sourceMappingURL=app.js.map diff --git a/src/dashboard/public/js/app.js.map b/src/dashboard/public/js/app.js.map index 9f0b95867..a37838160 100644 --- a/src/dashboard/public/js/app.js.map +++ b/src/dashboard/public/js/app.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../../frontend/state.ts", "../../frontend/websocket.ts", "../../frontend/utils.ts", "../../frontend/components.ts", "../../frontend/app.ts"], - "sourcesContent": ["/**\n * Dashboard State Management\n */\n\nimport type { Agent, Message, AppState, ChannelType } from './types.js';\n\n/**\n * Global application state\n */\nexport const state: AppState = {\n agents: [],\n messages: [],\n currentChannel: 'general',\n currentThread: null,\n isConnected: false,\n ws: null,\n reconnectAttempts: 0,\n};\n\n/**\n * State update callbacks\n */\ntype StateListener = () => void;\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => listener());\n}\n\n/**\n * Update agents in state\n */\nexport function setAgents(agents: Agent[]): void {\n state.agents = agents;\n notifyListeners();\n}\n\n/**\n * Update messages in state\n */\nexport function setMessages(messages: Message[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set current channel/conversation\n */\nexport function setCurrentChannel(channel: ChannelType): void {\n state.currentChannel = channel;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnectionStatus(connected: boolean): void {\n state.isConnected = connected;\n if (connected) {\n state.reconnectAttempts = 0;\n }\n notifyListeners();\n}\n\n/**\n * Increment reconnect attempts\n */\nexport function incrementReconnectAttempts(): void {\n state.reconnectAttempts++;\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Filter messages based on current channel\n */\nexport function getFilteredMessages(): Message[] {\n const { messages, currentChannel } = state;\n\n if (currentChannel === 'general') {\n return messages;\n }\n\n // Filter for specific agent - show messages to/from that agent\n return messages.filter(\n (m) => m.from === currentChannel || m.to === currentChannel\n );\n}\n\n/**\n * Set current thread for thread panel\n */\nexport function setCurrentThread(thread: string | null): void {\n state.currentThread = thread;\n}\n\n/**\n * Get messages for a specific thread\n */\nexport function getThreadMessages(threadId: string): Message[] {\n return state.messages.filter((m) => m.thread === threadId);\n}\n\n/**\n * Get reply count for a thread\n */\nexport function getThreadReplyCount(threadId: string): number {\n return state.messages.filter((m) => m.thread === threadId).length;\n}\n", "/**\n * WebSocket Connection Handler\n */\n\nimport type { DashboardData } from './types.js';\nimport {\n state,\n setAgents,\n setMessages,\n setConnectionStatus,\n setWebSocket,\n incrementReconnectAttempts,\n} from './state.js';\n\ntype DataHandler = (data: DashboardData) => void;\n\nlet dataHandler: DataHandler | null = null;\n\n/**\n * Set the handler for incoming data\n */\nexport function onData(handler: DataHandler): void {\n dataHandler = handler;\n}\n\n/**\n * Connect to the WebSocket server\n */\nexport function connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);\n\n ws.onopen = (): void => {\n setConnectionStatus(true);\n };\n\n ws.onclose = (): void => {\n setConnectionStatus(false);\n // Reconnect with exponential backoff\n const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);\n incrementReconnectAttempts();\n setTimeout(connect, delay);\n };\n\n ws.onerror = (error): void => {\n console.error('WebSocket error:', error);\n };\n\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const data: DashboardData = JSON.parse(event.data as string);\n handleData(data);\n } catch (e) {\n console.error('Failed to parse message:', e);\n }\n };\n\n setWebSocket(ws);\n}\n\n/**\n * Handle incoming dashboard data\n */\nfunction handleData(data: DashboardData): void {\n console.log('[WS] Received data:', { agentCount: data.agents?.length, messageCount: data.messages?.length });\n\n if (data.agents) {\n console.log('[WS] Setting agents:', data.agents.map(a => a.name));\n setAgents(data.agents);\n }\n\n if (data.messages) {\n setMessages(data.messages);\n }\n\n if (dataHandler) {\n dataHandler(data);\n }\n}\n\n/**\n * Send a message via the REST API\n */\nexport async function sendMessage(\n to: string,\n message: string,\n thread?: string\n): Promise<{ success: boolean; error?: string }> {\n try {\n const body: { to: string; message: string; thread?: string } = { to, message };\n if (thread) {\n body.thread = thread;\n }\n\n const response = await fetch('/api/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n return { success: true };\n } else {\n return { success: false, error: result.error || 'Failed to send message' };\n }\n } catch (err) {\n return { success: false, error: 'Network error - could not send message' };\n }\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Dashboard UI Components\n */\n\nimport type { Agent, Message, DOMElements, ChannelType } from './types.js';\nimport { state, getFilteredMessages, setCurrentChannel, setCurrentThread, getThreadMessages, getThreadReplyCount } from './state.js';\nimport {\n escapeHtml,\n formatTime,\n formatDate,\n getAvatarColor,\n getInitials,\n formatMessageBody,\n isAgentOnline,\n} from './utils.js';\n\nlet elements: DOMElements;\nlet paletteSelectedIndex = -1;\n\n/**\n * Initialize DOM element references\n */\nexport function initElements(): DOMElements {\n elements = {\n connectionDot: document.getElementById('connection-dot')!,\n channelsList: document.getElementById('channels-list')!,\n agentsList: document.getElementById('agents-list')!,\n messagesList: document.getElementById('messages-list')!,\n currentChannelName: document.getElementById('current-channel-name')!,\n channelTopic: document.getElementById('channel-topic')!,\n onlineCount: document.getElementById('online-count')!,\n messageInput: document.getElementById('message-input') as HTMLTextAreaElement,\n sendBtn: document.getElementById('send-btn') as HTMLButtonElement,\n boldBtn: document.getElementById('bold-btn') as HTMLButtonElement,\n emojiBtn: document.getElementById('emoji-btn') as HTMLButtonElement,\n searchTrigger: document.getElementById('search-trigger')!,\n commandPaletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteChannelsSection: document.getElementById('palette-channels-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n paletteMessagesSection: document.getElementById('palette-messages-section')!,\n typingIndicator: document.getElementById('typing-indicator')!,\n threadPanelOverlay: document.getElementById('thread-panel-overlay')!,\n threadPanelId: document.getElementById('thread-panel-id')!,\n threadPanelClose: document.getElementById('thread-panel-close') as HTMLButtonElement,\n threadMessages: document.getElementById('thread-messages')!,\n threadMessageInput: document.getElementById('thread-message-input') as HTMLTextAreaElement,\n threadSendBtn: document.getElementById('thread-send-btn') as HTMLButtonElement,\n mentionAutocomplete: document.getElementById('mention-autocomplete')!,\n mentionAutocompleteList: document.getElementById('mention-autocomplete-list')!,\n };\n return elements;\n}\n\n/**\n * Get DOM elements\n */\nexport function getElements(): DOMElements {\n return elements;\n}\n\n/**\n * Update connection status indicator\n */\nexport function updateConnectionStatus(): void {\n if (state.isConnected) {\n elements.connectionDot.classList.remove('offline');\n } else {\n elements.connectionDot.classList.add('offline');\n }\n}\n\n/**\n * Render agents list in sidebar\n */\nexport function renderAgents(): void {\n console.log('[UI] renderAgents called, agents:', state.agents.length, state.agents.map(a => a.name));\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n const presenceClass = online ? 'online' : '';\n const isActive = state.currentChannel === agent.name;\n const needsAttentionClass = agent.needsAttention ? 'needs-attention' : '';\n\n return `\n
  • \n
    \n ${getInitials(agent.name)}\n \n
    \n ${escapeHtml(agent.name)}\n ${agent.needsAttention ? 'Needs Input' : ''}\n
  • \n `;\n })\n .join('');\n\n elements.agentsList.innerHTML =\n html ||\n '
  • No agents connected
  • ';\n\n // Add click handlers\n elements.agentsList.querySelectorAll('.channel-item[data-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.agent;\n if (agentName) {\n selectChannel(agentName);\n }\n });\n });\n\n // Update command palette agents\n updatePaletteAgents();\n}\n\n/**\n * Render messages list\n */\nexport function renderMessages(): void {\n const filtered = getFilteredMessages();\n\n if (filtered.length === 0) {\n elements.messagesList.innerHTML = `\n
    \n \n \n \n
    No messages yet
    \n
    \n ${\n state.currentChannel === 'general'\n ? 'Messages between agents will appear here'\n : `Messages with ${state.currentChannel} will appear here`\n }\n
    \n
    \n `;\n return;\n }\n\n let html = '';\n let lastDate: string | null = null;\n\n filtered.forEach((msg) => {\n const msgDate = new Date(msg.timestamp).toDateString();\n\n // Add date divider if needed\n if (msgDate !== lastDate) {\n html += `\n
    \n ${formatDate(msg.timestamp)}\n
    \n `;\n lastDate = msgDate;\n }\n\n const isBroadcast = msg.to === '*';\n const avatarColor = getAvatarColor(msg.from);\n const replyCount = getThreadReplyCount(msg.id);\n\n // Format: @From \u2192 @To: message (like Slack)\n // For cross-project messages, show project badge before agent name\n const recipientDisplay = isBroadcast\n ? '@everyone'\n : msg.project\n ? `${escapeHtml(msg.project)}@${escapeHtml(msg.to)}`\n : `@${escapeHtml(msg.to)}`;\n\n html += `\n
    \n
    \n ${getInitials(msg.from)}\n
    \n
    \n
    \n @${escapeHtml(msg.from)}\n \n \u2192 ${recipientDisplay}\n \n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n ${\n msg.thread\n ? `\n
    \n \n \n \n Thread: ${escapeHtml(msg.thread)}\n
    \n `\n : ''\n }\n ${\n replyCount > 0\n ? `\n
    \n \n \n \n ${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}\n
    \n `\n : ''\n }\n
    \n
    \n \n \n
    \n
    \n `;\n });\n\n elements.messagesList.innerHTML = html;\n\n // Note: Auto-scroll removed - interferes with manual scrolling through history\n\n // Attach thread click handlers\n attachThreadHandlers();\n}\n\n/**\n * Select a channel and update UI\n */\nexport function selectChannel(channel: ChannelType): void {\n setCurrentChannel(channel);\n\n // Update sidebar active states\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.channel === channel);\n });\n elements.agentsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.agent === channel);\n });\n\n // Update header\n const prefixEl = document.querySelector('.channel-header-name .prefix');\n if (channel === 'general') {\n elements.currentChannelName.innerHTML = 'general';\n elements.channelTopic.textContent = 'All agent communications';\n if (prefixEl) prefixEl.textContent = '#';\n } else {\n elements.currentChannelName.innerHTML = escapeHtml(channel);\n const agent = state.agents.find((a) => a.name === channel);\n elements.channelTopic.textContent = agent?.status || 'Direct messages';\n if (prefixEl) prefixEl.textContent = '@';\n }\n\n // Update composer placeholder with @mention format\n elements.messageInput.placeholder =\n channel === 'general'\n ? '@AgentName message... (or @* to broadcast)'\n : `@${channel} your message here...`;\n\n // Re-render messages\n renderMessages();\n}\n\n/**\n * Update online count display\n */\nexport function updateOnlineCount(): void {\n const online = state.agents.filter((a) => isAgentOnline(a.lastSeen || a.lastActive)).length;\n elements.onlineCount.textContent = `${online} online`;\n}\n\n/**\n * Update agents in command palette\n */\nexport function updatePaletteAgents(): void {\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n return `\n
    \n
    \n
    \n ${getInitials(agent.name)}\n \n
    \n
    \n
    \n
    ${escapeHtml(agent.name)}
    \n
    ${online ? 'Online' : 'Offline'}
    \n
    \n
    \n `;\n })\n .join('');\n\n const section = elements.paletteAgentsSection;\n const items = section.querySelectorAll('.palette-item');\n items.forEach((item) => item.remove());\n section.insertAdjacentHTML('beforeend', html);\n\n // Add click handlers\n section.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.jumpAgent;\n if (agentName) {\n selectChannel(agentName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Initialize channel click handlers in command palette\n */\nexport function initPaletteChannels(): void {\n elements.paletteChannelsSection\n .querySelectorAll('.palette-item[data-jump-channel]')\n .forEach((item) => {\n item.addEventListener('click', () => {\n const channelName = item.dataset.jumpChannel;\n if (channelName) {\n selectChannel(channelName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Open command palette\n */\nexport function openCommandPalette(): void {\n elements.commandPaletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n paletteSelectedIndex = -1;\n filterPaletteResults('');\n}\n\n/**\n * Get all visible palette items\n */\nexport function getVisiblePaletteItems(): HTMLElement[] {\n const allItems = Array.from(\n elements.paletteResults.querySelectorAll('.palette-item')\n );\n return allItems.filter((item) => item.style.display !== 'none');\n}\n\n/**\n * Update the selected palette item visually\n */\nexport function updatePaletteSelection(): void {\n const items = getVisiblePaletteItems();\n\n // Remove selection from all items\n items.forEach((item) => item.classList.remove('selected'));\n\n // Add selection to current item\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n const selectedItem = items[paletteSelectedIndex];\n selectedItem.classList.add('selected');\n\n // Scroll into view if needed\n selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }\n}\n\n/**\n * Handle keyboard navigation in command palette\n */\nexport function handlePaletteKeydown(e: KeyboardEvent): void {\n const items = getVisiblePaletteItems();\n\n if (items.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex < items.length - 1\n ? paletteSelectedIndex + 1\n : 0;\n updatePaletteSelection();\n break;\n\n case 'ArrowUp':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex > 0\n ? paletteSelectedIndex - 1\n : items.length - 1;\n updatePaletteSelection();\n break;\n\n case 'Enter':\n e.preventDefault();\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n executePaletteItem(items[paletteSelectedIndex]);\n }\n break;\n }\n}\n\n/**\n * Execute the action for a palette item\n */\nexport function executePaletteItem(item: HTMLElement): void {\n // Check for command\n const command = item.dataset.command;\n if (command) {\n if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n closeCommandPalette();\n return;\n }\n\n // Check for channel jump\n const channel = item.dataset.jumpChannel;\n if (channel) {\n selectChannel(channel);\n closeCommandPalette();\n return;\n }\n\n // Check for agent jump\n const agent = item.dataset.jumpAgent;\n if (agent) {\n selectChannel(agent);\n closeCommandPalette();\n return;\n }\n\n // Check for message jump\n const messageId = item.dataset.jumpMessage;\n if (messageId) {\n // Find and scroll to the message\n const messageEl = elements.messagesList.querySelector(`[data-id=\"${messageId}\"]`);\n if (messageEl) {\n messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n messageEl.classList.add('highlighted');\n setTimeout(() => messageEl.classList.remove('highlighted'), 2000);\n }\n closeCommandPalette();\n return;\n }\n}\n\n/**\n * Close command palette\n */\nexport function closeCommandPalette(): void {\n elements.commandPaletteOverlay.classList.remove('visible');\n}\n\n/**\n * Filter command palette results based on query\n */\nexport function filterPaletteResults(query: string): void {\n const q = query.toLowerCase();\n\n // Reset selection when filtering\n paletteSelectedIndex = -1;\n\n // Filter command items\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter channel items\n document.querySelectorAll('.palette-item[data-jump-channel]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter agent items\n document.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n const name = item.dataset.jumpAgent?.toLowerCase() || '';\n item.style.display = name.includes(q) ? 'flex' : 'none';\n });\n\n // Show message search if query is long enough\n if (q.length >= 2) {\n const matches = state.messages.filter((m) => m.content.toLowerCase().includes(q)).slice(0, 5);\n\n if (matches.length > 0) {\n elements.paletteMessagesSection.style.display = 'block';\n const items = matches\n .map(\n (m) => `\n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(m.from)}
    \n
    ${escapeHtml(m.content.substring(0, 60))}${m.content.length > 60 ? '...' : ''}
    \n
    \n
    \n `\n )\n .join('');\n\n const existingItems = elements.paletteMessagesSection.querySelectorAll('.palette-item');\n existingItems.forEach((item) => item.remove());\n elements.paletteMessagesSection.insertAdjacentHTML('beforeend', items);\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n}\n\n/**\n * Open thread panel for a specific thread\n */\nexport function openThreadPanel(threadId: string): void {\n setCurrentThread(threadId);\n elements.threadPanelId.textContent = threadId;\n elements.threadPanelOverlay.classList.add('visible');\n elements.threadMessageInput.value = '';\n renderThreadMessages(threadId);\n elements.threadMessageInput.focus();\n}\n\n/**\n * Close thread panel\n */\nexport function closeThreadPanel(): void {\n setCurrentThread(null);\n elements.threadPanelOverlay.classList.remove('visible');\n}\n\n/**\n * Render messages in thread panel\n */\nexport function renderThreadMessages(threadId: string): void {\n const messages = getThreadMessages(threadId);\n\n if (messages.length === 0) {\n elements.threadMessages.innerHTML = `\n
    \n

    No messages in this thread yet.

    \n

    Start the conversation below!

    \n
    \n `;\n return;\n }\n\n const html = messages\n .map((msg) => `\n
    \n
    \n
    \n ${getInitials(msg.from)}\n
    \n ${escapeHtml(msg.from)}\n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n
    \n `)\n .join('');\n\n elements.threadMessages.innerHTML = html;\n\n // Scroll to bottom\n elements.threadMessages.scrollTop = elements.threadMessages.scrollHeight;\n}\n\n/**\n * Attach thread click handlers to messages (call after renderMessages)\n */\nexport function attachThreadHandlers(): void {\n // Thread indicator clicks\n elements.messagesList.querySelectorAll('.thread-indicator').forEach((el) => {\n el.style.cursor = 'pointer';\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply count badge clicks\n elements.messagesList.querySelectorAll('.reply-count-badge').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply in thread button clicks\n elements.messagesList.querySelectorAll('.message-action-btn[data-action=\"reply\"]').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const messageId = el.closest('.message')?.getAttribute('data-id');\n if (messageId) {\n // Use message ID as thread ID for new threads\n openThreadPanel(messageId);\n }\n });\n });\n}\n\n/**\n * @-Mention Autocomplete State\n */\nlet mentionSelectedIndex = 0;\nlet mentionFilteredAgents: typeof state.agents = [];\n\n/**\n * Show mention autocomplete dropdown with filtered agents\n */\nexport function showMentionAutocomplete(filter: string): void {\n const filterLower = filter.toLowerCase();\n\n // Filter agents by name, include broadcast option\n mentionFilteredAgents = state.agents.filter(agent =>\n agent.name.toLowerCase().includes(filterLower)\n );\n\n // Reset selection\n mentionSelectedIndex = 0;\n\n // Build HTML for agent list\n let html = '';\n\n // Add broadcast option if filter matches\n if ('*'.includes(filterLower) || 'everyone'.includes(filterLower) || 'all'.includes(filterLower) || 'broadcast'.includes(filterLower)) {\n html += `\n
    \n
    *
    \n @everyone\n Broadcast to all\n
    \n `;\n }\n\n // Add agents\n mentionFilteredAgents.forEach((agent, index) => {\n const isSelected = index === mentionSelectedIndex;\n html += `\n
    \n
    \n ${getInitials(agent.name)}\n
    \n @${escapeHtml(agent.name)}\n ${escapeHtml(agent.role || 'Agent')}\n
    \n `;\n });\n\n if (html === '') {\n html = '
    No matching agents
    ';\n }\n\n elements.mentionAutocompleteList.innerHTML = html;\n elements.mentionAutocomplete.classList.add('visible');\n\n // Add click handlers to items\n elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]').forEach((item) => {\n item.addEventListener('click', () => {\n const mention = item.dataset.mention;\n if (mention) {\n completeMention(mention);\n }\n });\n });\n}\n\n/**\n * Hide mention autocomplete dropdown\n */\nexport function hideMentionAutocomplete(): void {\n elements.mentionAutocomplete.classList.remove('visible');\n mentionFilteredAgents = [];\n mentionSelectedIndex = 0;\n}\n\n/**\n * Check if mention autocomplete is visible\n */\nexport function isMentionAutocompleteVisible(): boolean {\n return elements.mentionAutocomplete.classList.contains('visible');\n}\n\n/**\n * Navigate mention autocomplete selection\n */\nexport function navigateMentionAutocomplete(direction: 'up' | 'down'): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n if (items.length === 0) return;\n\n // Remove current selection\n items[mentionSelectedIndex]?.classList.remove('selected');\n\n // Update index\n if (direction === 'down') {\n mentionSelectedIndex = (mentionSelectedIndex + 1) % items.length;\n } else {\n mentionSelectedIndex = (mentionSelectedIndex - 1 + items.length) % items.length;\n }\n\n // Add new selection\n items[mentionSelectedIndex]?.classList.add('selected');\n items[mentionSelectedIndex]?.scrollIntoView({ block: 'nearest' });\n}\n\n/**\n * Complete the current mention selection\n */\nexport function completeMention(mention?: string): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n\n // Use provided mention or get from selected item\n let selectedMention = mention;\n if (!selectedMention && items.length > 0) {\n selectedMention = items[mentionSelectedIndex]?.dataset.mention;\n }\n\n if (!selectedMention) {\n hideMentionAutocomplete();\n return;\n }\n\n // Replace the @... text with the completed mention\n const input = elements.messageInput;\n const value = input.value;\n\n // Find the @ position (should be at start or after whitespace)\n const atMatch = value.match(/^@\\S*/);\n if (atMatch) {\n // Replace the @partial with @CompletedName\n const completedText = `@${selectedMention} `;\n input.value = completedText + value.substring(atMatch[0].length);\n input.selectionStart = input.selectionEnd = completedText.length;\n }\n\n hideMentionAutocomplete();\n input.focus();\n}\n\n/**\n * Get the current @mention being typed (if any)\n */\nexport function getCurrentMentionQuery(): string | null {\n const input = elements.messageInput;\n const value = input.value;\n const cursorPos = input.selectionStart;\n\n // Check if cursor is within an @mention at the start\n const atMatch = value.match(/^@(\\S*)/);\n if (atMatch && cursorPos <= atMatch[0].length) {\n return atMatch[1]; // Return the text after @\n }\n\n return null;\n}\n", "/**\n * Dashboard Application Entry Point\n */\n\nimport { subscribe } from './state.js';\nimport { connect, sendMessage } from './websocket.js';\nimport {\n initElements,\n getElements,\n updateConnectionStatus,\n renderAgents,\n renderMessages,\n selectChannel,\n updateOnlineCount,\n openCommandPalette,\n closeCommandPalette,\n filterPaletteResults,\n handlePaletteKeydown,\n initPaletteChannels,\n closeThreadPanel,\n renderThreadMessages,\n showMentionAutocomplete,\n hideMentionAutocomplete,\n isMentionAutocompleteVisible,\n navigateMentionAutocomplete,\n completeMention,\n getCurrentMentionQuery,\n} from './components.js';\nimport { state } from './state.js';\n\n/**\n * Detect if we're viewing a project dashboard from bridge context\n */\nfunction detectProjectContext(): { projectId: string | null; fromBridge: boolean } {\n const pathname = window.location.pathname;\n const match = pathname.match(/^\\/project\\/([^/]+)$/);\n\n if (match) {\n return { projectId: decodeURIComponent(match[1]), fromBridge: true };\n }\n\n return { projectId: null, fromBridge: false };\n}\n\n/**\n * Update the UI for project context (when accessed from bridge)\n */\nasync function setupProjectContext(projectId: string): Promise {\n // Update workspace name to show project\n const workspaceName = document.querySelector('.workspace-name');\n if (workspaceName) {\n // Fetch project info\n try {\n const response = await fetch(`/api/project/${encodeURIComponent(projectId)}`);\n if (response.ok) {\n const project = await response.json();\n const nameSpan = workspaceName.querySelector(':not(.status-dot)');\n if (nameSpan && nameSpan.nodeType === Node.TEXT_NODE) {\n nameSpan.textContent = project.name || projectId;\n } else {\n // Replace text content after status-dot\n const textNodes = Array.from(workspaceName.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);\n textNodes.forEach(n => n.textContent = '');\n workspaceName.appendChild(document.createTextNode(' ' + (project.name || projectId)));\n }\n }\n } catch {\n // Fallback - just show project ID\n }\n }\n\n // Update bridge nav link to show \"Back to Bridge\" with back arrow\n const bridgeLinkText = document.getElementById('bridge-link-text');\n const bridgeNavLink = document.getElementById('bridge-nav-link');\n if (bridgeLinkText) {\n bridgeLinkText.textContent = '\u2190 Back to Bridge';\n }\n if (bridgeNavLink) {\n bridgeNavLink.classList.add('back-to-bridge');\n }\n\n // Add a subtle indicator that we're in project view\n document.body.classList.add('project-view');\n}\n\n/**\n * Initialize the dashboard application\n */\nexport function initApp(): void {\n const elements = initElements();\n\n // Check if we're in project context (from bridge)\n const { projectId, fromBridge } = detectProjectContext();\n if (fromBridge && projectId) {\n setupProjectContext(projectId);\n }\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderAgents();\n renderMessages();\n updateOnlineCount();\n });\n\n // Set up event listeners\n setupEventListeners(elements);\n\n // Connect to WebSocket\n connect();\n}\n\n/**\n * Set up all event listeners\n */\nfunction setupEventListeners(elements: ReturnType): void {\n // Channel clicks\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.addEventListener('click', () => {\n const channel = item.dataset.channel;\n if (channel) {\n selectChannel(channel);\n }\n });\n });\n\n // Send button\n elements.sendBtn.addEventListener('click', handleSend);\n\n // Keyboard shortcuts for composer\n elements.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n // Handle mention autocomplete keys first\n if (isMentionAutocompleteVisible()) {\n if (e.key === 'Tab' || e.key === 'Enter') {\n e.preventDefault();\n completeMention();\n return;\n }\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n navigateMentionAutocomplete('up');\n return;\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n navigateMentionAutocomplete('down');\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n hideMentionAutocomplete();\n return;\n }\n }\n\n // Enter to send (Slack-style), Shift+Enter for newline\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Auto-resize textarea and handle @-mention autocomplete\n elements.messageInput.addEventListener('input', () => {\n elements.messageInput.style.height = 'auto';\n elements.messageInput.style.height =\n Math.min(elements.messageInput.scrollHeight, 200) + 'px';\n\n // Check for @-mention at start of input\n const query = getCurrentMentionQuery();\n if (query !== null) {\n showMentionAutocomplete(query);\n } else {\n hideMentionAutocomplete();\n }\n });\n\n // Hide mention autocomplete when input loses focus (with delay to allow clicks)\n elements.messageInput.addEventListener('blur', () => {\n setTimeout(() => {\n hideMentionAutocomplete();\n }, 150);\n });\n\n // Bold button - wrap selected text with ** or insert **bold**\n elements.boldBtn.addEventListener('click', () => {\n const input = elements.messageInput;\n const start = input.selectionStart;\n const end = input.selectionEnd;\n const text = input.value;\n\n if (start === end) {\n // No selection - insert **bold** placeholder\n const before = text.substring(0, start);\n const after = text.substring(end);\n input.value = before + '**bold**' + after;\n input.selectionStart = start + 2;\n input.selectionEnd = start + 6;\n } else {\n // Wrap selection with **\n const before = text.substring(0, start);\n const selected = text.substring(start, end);\n const after = text.substring(end);\n input.value = before + '**' + selected + '**' + after;\n input.selectionStart = start;\n input.selectionEnd = end + 4;\n }\n input.focus();\n });\n\n // Emoji button - insert common emojis via simple picker\n elements.emojiBtn.addEventListener('click', () => {\n const emojis = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2705', '\u274C', '\uD83C\uDF89', '\uD83D\uDD25', '\uD83D\uDCA1', '\u26A0\uFE0F', '\uD83D\uDCDD', '\uD83D\uDE80'];\n const emoji = emojis[Math.floor(Math.random() * emojis.length)];\n const input = elements.messageInput;\n const start = input.selectionStart;\n const text = input.value;\n input.value = text.substring(0, start) + emoji + text.substring(start);\n input.selectionStart = input.selectionEnd = start + emoji.length;\n input.focus();\n });\n\n // Command palette\n elements.searchTrigger.addEventListener('click', openCommandPalette);\n\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.commandPaletteOverlay.classList.contains('visible')) {\n closeCommandPalette();\n } else {\n openCommandPalette();\n }\n }\n\n if (e.key === 'Escape') {\n closeCommandPalette();\n }\n });\n\n elements.commandPaletteOverlay.addEventListener('click', (e: MouseEvent) => {\n if (e.target === elements.commandPaletteOverlay) {\n closeCommandPalette();\n }\n });\n\n elements.paletteSearch.addEventListener('input', (e: Event) => {\n const target = e.target as HTMLInputElement;\n filterPaletteResults(target.value);\n });\n\n elements.paletteSearch.addEventListener('keydown', handlePaletteKeydown);\n\n // Command execution\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n item.addEventListener('click', () => {\n const command = item.dataset.command;\n\n if (command === 'bridge') {\n // Navigate to bridge view\n window.location.href = '/bridge';\n } else if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n\n closeCommandPalette();\n });\n });\n\n // Add Cmd/Ctrl+B shortcut for bridge navigation\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'b') {\n e.preventDefault();\n window.location.href = '/bridge';\n }\n });\n\n // Initialize palette channel click handlers\n initPaletteChannels();\n\n // Thread panel close button\n elements.threadPanelClose.addEventListener('click', closeThreadPanel);\n\n // Thread panel send button\n elements.threadSendBtn.addEventListener('click', handleThreadSend);\n\n // Thread message input keyboard shortcuts (Slack-style)\n elements.threadMessageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleThreadSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Close thread panel on Escape\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape' && elements.threadPanelOverlay.classList.contains('visible')) {\n closeThreadPanel();\n }\n });\n}\n\n/**\n * Parse @mention from message text\n * Formats: \"@AgentName message\" or \"@* message\" for broadcast\n * Returns { to, message } or null if no valid mention found\n */\nfunction parseMention(text: string): { to: string; message: string } | null {\n const trimmed = text.trim();\n\n // Match @mention at the start of the message\n // @* for broadcast, @AgentName for direct message\n const match = trimmed.match(/^@(\\*|[^\\s]+)\\s+(.+)$/s);\n\n if (!match) {\n return null;\n }\n\n return {\n to: match[1],\n message: match[2].trim(),\n };\n}\n\n/**\n * Handle send button click\n */\nasync function handleSend(): Promise {\n const elements = getElements();\n const rawMessage = elements.messageInput.value.trim();\n\n if (!rawMessage) {\n return;\n }\n\n // Parse @mention from the message\n const parsed = parseMention(rawMessage);\n\n if (!parsed) {\n alert('Message must start with @recipient (e.g., \"@Lead hello\" or \"@* broadcast\")');\n return;\n }\n\n const { to, message } = parsed;\n\n elements.sendBtn.disabled = true;\n\n const result = await sendMessage(to, message);\n\n if (result.success) {\n elements.messageInput.value = '';\n elements.messageInput.style.height = 'auto';\n } else {\n alert(result.error);\n }\n\n elements.sendBtn.disabled = false;\n}\n\n/**\n * Handle thread panel send button click\n */\nasync function handleThreadSend(): Promise {\n const elements = getElements();\n const message = elements.threadMessageInput.value.trim();\n const threadId = state.currentThread;\n\n if (!message || !threadId) {\n return;\n }\n\n // For thread replies, send to broadcast or use original recipient\n // For now, send as broadcast with thread ID\n elements.threadSendBtn.disabled = true;\n\n const result = await sendMessage('*', message, threadId);\n\n if (result.success) {\n elements.threadMessageInput.value = '';\n // Re-render thread messages to show the new message\n renderThreadMessages(threadId);\n } else {\n alert(result.error);\n }\n\n elements.threadSendBtn.disabled = false;\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', initApp);\n } else {\n initApp();\n }\n}\n"], - "mappings": "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", - "names": ["state", "listeners", "subscribe", "listener", "index", "notifyListeners", "setAgents", "agents", "setMessages", "messages", "setCurrentChannel", "channel", "setConnectionStatus", "connected", "incrementReconnectAttempts", "setWebSocket", "ws", "getFilteredMessages", "currentChannel", "m", "setCurrentThread", "thread", "getThreadMessages", "threadId", "getThreadReplyCount", "dataHandler", "connect", "protocol", "ws", "setConnectionStatus", "delay", "state", "incrementReconnectAttempts", "error", "event", "data", "handleData", "e", "setWebSocket", "a", "setAgents", "setMessages", "dataHandler", "sendMessage", "to", "message", "thread", "body", "response", "result", "isAgentOnline", "lastSeen", "ts", "escapeHtml", "text", "div", "formatTime", "timestamp", "formatDate", "date", "today", "yesterday", "getAvatarColor", "name", "colors", "hash", "i", "getInitials", "formatMessageBody", "content", "escaped", "elements", "paletteSelectedIndex", "initElements", "getElements", "updateConnectionStatus", "state", "renderAgents", "a", "html", "agent", "presenceClass", "isAgentOnline", "isActive", "needsAttentionClass", "escapeHtml", "getAvatarColor", "getInitials", "item", "agentName", "selectChannel", "updatePaletteAgents", "renderMessages", "filtered", "getFilteredMessages", "lastDate", "msg", "msgDate", "formatDate", "isBroadcast", "avatarColor", "replyCount", "getThreadReplyCount", "recipientDisplay", "formatTime", "formatMessageBody", "attachThreadHandlers", "channel", "setCurrentChannel", "prefixEl", "updateOnlineCount", "online", "section", "closeCommandPalette", "initPaletteChannels", "channelName", "openCommandPalette", "filterPaletteResults", "getVisiblePaletteItems", "updatePaletteSelection", "items", "selectedItem", "handlePaletteKeydown", "e", "executePaletteItem", "command", "messageId", "messageEl", "query", "q", "title", "name", "matches", "m", "openThreadPanel", "threadId", "setCurrentThread", "renderThreadMessages", "closeThreadPanel", "messages", "getThreadMessages", "el", "mentionSelectedIndex", "mentionFilteredAgents", "showMentionAutocomplete", "filter", "filterLower", "index", "mention", "completeMention", "hideMentionAutocomplete", "isMentionAutocompleteVisible", "navigateMentionAutocomplete", "direction", "selectedMention", "input", "value", "atMatch", "completedText", "getCurrentMentionQuery", "cursorPos", "detectProjectContext", "match", "setupProjectContext", "projectId", "workspaceName", "response", "project", "nameSpan", "n", "bridgeLinkText", "bridgeNavLink", "initApp", "elements", "initElements", "fromBridge", "subscribe", "updateConnectionStatus", "renderAgents", "renderMessages", "updateOnlineCount", "setupEventListeners", "connect", "item", "channel", "selectChannel", "handleSend", "isMentionAutocompleteVisible", "completeMention", "navigateMentionAutocomplete", "hideMentionAutocomplete", "query", "getCurrentMentionQuery", "showMentionAutocomplete", "input", "start", "end", "text", "before", "after", "selected", "emojis", "emoji", "openCommandPalette", "closeCommandPalette", "target", "filterPaletteResults", "handlePaletteKeydown", "command", "initPaletteChannels", "closeThreadPanel", "handleThreadSend", "parseMention", "getElements", "rawMessage", "parsed", "to", "message", "result", "sendMessage", "threadId", "state", "renderThreadMessages"] + "sourcesContent": ["/**\n * Dashboard State Management\n */\n\nimport type { Agent, Message, AppState, ChannelType } from './types.js';\n\n/**\n * Global application state\n */\nexport const state: AppState = {\n agents: [],\n messages: [],\n currentChannel: 'general',\n currentThread: null,\n isConnected: false,\n ws: null,\n reconnectAttempts: 0,\n};\n\n/**\n * State update callbacks\n */\ntype StateListener = () => void;\nconst listeners: StateListener[] = [];\n\n/**\n * Subscribe to state changes\n */\nexport function subscribe(listener: StateListener): () => void {\n listeners.push(listener);\n return () => {\n const index = listeners.indexOf(listener);\n if (index > -1) {\n listeners.splice(index, 1);\n }\n };\n}\n\n/**\n * Notify all listeners of state change\n */\nfunction notifyListeners(): void {\n listeners.forEach((listener) => listener());\n}\n\n/**\n * Update agents in state\n */\nexport function setAgents(agents: Agent[]): void {\n state.agents = agents;\n notifyListeners();\n}\n\n/**\n * Update messages in state\n */\nexport function setMessages(messages: Message[]): void {\n state.messages = messages;\n notifyListeners();\n}\n\n/**\n * Set current channel/conversation\n */\nexport function setCurrentChannel(channel: ChannelType): void {\n state.currentChannel = channel;\n notifyListeners();\n}\n\n/**\n * Update connection status\n */\nexport function setConnectionStatus(connected: boolean): void {\n state.isConnected = connected;\n if (connected) {\n state.reconnectAttempts = 0;\n }\n notifyListeners();\n}\n\n/**\n * Increment reconnect attempts\n */\nexport function incrementReconnectAttempts(): void {\n state.reconnectAttempts++;\n}\n\n/**\n * Set WebSocket instance\n */\nexport function setWebSocket(ws: WebSocket | null): void {\n state.ws = ws;\n}\n\n/**\n * Filter messages based on current channel\n */\nexport function getFilteredMessages(): Message[] {\n const { messages, currentChannel } = state;\n\n if (currentChannel === 'general') {\n return messages;\n }\n\n // Filter for specific agent - show messages to/from that agent\n return messages.filter(\n (m) => m.from === currentChannel || m.to === currentChannel\n );\n}\n\n/**\n * Set current thread for thread panel\n */\nexport function setCurrentThread(thread: string | null): void {\n state.currentThread = thread;\n}\n\n/**\n * Get messages for a specific thread\n */\nexport function getThreadMessages(threadId: string): Message[] {\n return state.messages.filter((m) => m.thread === threadId);\n}\n\n/**\n * Get reply count for a thread\n */\nexport function getThreadReplyCount(threadId: string): number {\n return state.messages.filter((m) => m.thread === threadId).length;\n}\n", "/**\n * WebSocket Connection Handler\n */\n\nimport type { DashboardData } from './types.js';\nimport {\n state,\n setAgents,\n setMessages,\n setConnectionStatus,\n setWebSocket,\n incrementReconnectAttempts,\n} from './state.js';\n\ntype DataHandler = (data: DashboardData) => void;\n\nlet dataHandler: DataHandler | null = null;\n\n/**\n * Set the handler for incoming data\n */\nexport function onData(handler: DataHandler): void {\n dataHandler = handler;\n}\n\n/**\n * Connect to the WebSocket server\n */\nexport function connect(): void {\n const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';\n const ws = new WebSocket(`${protocol}//${window.location.host}/ws`);\n\n ws.onopen = (): void => {\n setConnectionStatus(true);\n };\n\n ws.onclose = (): void => {\n setConnectionStatus(false);\n // Reconnect with exponential backoff\n const delay = Math.min(1000 * Math.pow(2, state.reconnectAttempts), 30000);\n incrementReconnectAttempts();\n setTimeout(connect, delay);\n };\n\n ws.onerror = (error): void => {\n console.error('WebSocket error:', error);\n };\n\n ws.onmessage = (event: MessageEvent): void => {\n try {\n const data: DashboardData = JSON.parse(event.data as string);\n handleData(data);\n } catch (e) {\n console.error('Failed to parse message:', e);\n }\n };\n\n setWebSocket(ws);\n}\n\n/**\n * Handle incoming dashboard data\n */\nfunction handleData(data: DashboardData): void {\n console.log('[WS] Received data:', { agentCount: data.agents?.length, messageCount: data.messages?.length });\n\n if (data.agents) {\n console.log('[WS] Setting agents:', data.agents.map(a => a.name));\n setAgents(data.agents);\n }\n\n if (data.messages) {\n setMessages(data.messages);\n }\n\n if (dataHandler) {\n dataHandler(data);\n }\n}\n\n/**\n * Send a message via the REST API\n */\nexport async function sendMessage(\n to: string,\n message: string,\n thread?: string\n): Promise<{ success: boolean; error?: string }> {\n try {\n const body: { to: string; message: string; thread?: string } = { to, message };\n if (thread) {\n body.thread = thread;\n }\n\n const response = await fetch('/api/send', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n return { success: true };\n } else {\n return { success: false, error: result.error || 'Failed to send message' };\n }\n } catch (err) {\n return { success: false, error: 'Network error - could not send message' };\n }\n}\n", "/**\n * Dashboard Utility Functions\n */\n\n/** Threshold for considering an agent offline (30 seconds) */\nexport const STALE_THRESHOLD_MS = 30000;\n\n/**\n * Check if an agent is online based on last seen timestamp\n */\nexport function isAgentOnline(lastSeen: string | undefined): boolean {\n if (!lastSeen) return false;\n const ts = Date.parse(lastSeen);\n if (Number.isNaN(ts)) return false;\n return Date.now() - ts < STALE_THRESHOLD_MS;\n}\n\n/**\n * Escape HTML to prevent XSS\n */\nexport function escapeHtml(text: string | undefined): string {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n}\n\n/**\n * Format timestamp to locale time string\n */\nexport function formatTime(timestamp: string): string {\n const date = new Date(timestamp);\n return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });\n}\n\n/**\n * Format timestamp to human-readable date\n */\nexport function formatDate(timestamp: string): string {\n const date = new Date(timestamp);\n const today = new Date();\n const yesterday = new Date(today);\n yesterday.setDate(yesterday.getDate() - 1);\n\n if (date.toDateString() === today.toDateString()) {\n return 'Today';\n } else if (date.toDateString() === yesterday.toDateString()) {\n return 'Yesterday';\n } else {\n return date.toLocaleDateString([], {\n weekday: 'long',\n month: 'long',\n day: 'numeric',\n });\n }\n}\n\n/**\n * Generate a consistent color for an agent based on their name\n */\nexport function getAvatarColor(name: string): string {\n const colors = [\n '#e01e5a',\n '#2bac76',\n '#e8a427',\n '#1264a3',\n '#7c3aed',\n '#0d9488',\n '#dc2626',\n '#9333ea',\n '#ea580c',\n '#0891b2',\n ];\n let hash = 0;\n for (let i = 0; i < name.length; i++) {\n hash = name.charCodeAt(i) + ((hash << 5) - hash);\n }\n return colors[Math.abs(hash) % colors.length];\n}\n\n/**\n * Get initials from a name (first 2 characters, uppercase)\n */\nexport function getInitials(name: string): string {\n return name.substring(0, 2).toUpperCase();\n}\n\n/**\n * Format message body with basic markdown-like formatting\n */\nexport function formatMessageBody(content: string | undefined): string {\n if (!content) return '';\n\n let escaped = escapeHtml(content);\n\n // Simple code block detection\n escaped = escaped.replace(/```([\\s\\S]*?)```/g, '
    $1
    ');\n escaped = escaped.replace(/`([^`]+)`/g, '$1');\n\n // Convert newlines to
    for proper multi-line display\n escaped = escaped.replace(/\\n/g, '
    ');\n\n return escaped;\n}\n", "/**\n * Dashboard UI Components\n */\n\nimport type { Agent, Message, DOMElements, ChannelType } from './types.js';\nimport { state, getFilteredMessages, setCurrentChannel, setCurrentThread, getThreadMessages, getThreadReplyCount } from './state.js';\nimport {\n escapeHtml,\n formatTime,\n formatDate,\n getAvatarColor,\n getInitials,\n formatMessageBody,\n isAgentOnline,\n} from './utils.js';\n\nlet elements: DOMElements;\nlet paletteSelectedIndex = -1;\n\n/**\n * Initialize DOM element references\n */\nexport function initElements(): DOMElements {\n elements = {\n connectionDot: document.getElementById('connection-dot')!,\n channelsList: document.getElementById('channels-list')!,\n agentsList: document.getElementById('agents-list')!,\n messagesList: document.getElementById('messages-list')!,\n currentChannelName: document.getElementById('current-channel-name')!,\n channelTopic: document.getElementById('channel-topic')!,\n onlineCount: document.getElementById('online-count')!,\n messageInput: document.getElementById('message-input') as HTMLTextAreaElement,\n sendBtn: document.getElementById('send-btn') as HTMLButtonElement,\n boldBtn: document.getElementById('bold-btn') as HTMLButtonElement,\n emojiBtn: document.getElementById('emoji-btn') as HTMLButtonElement,\n searchTrigger: document.getElementById('search-trigger')!,\n commandPaletteOverlay: document.getElementById('command-palette-overlay')!,\n paletteSearch: document.getElementById('palette-search') as HTMLInputElement,\n paletteResults: document.getElementById('palette-results')!,\n paletteChannelsSection: document.getElementById('palette-channels-section')!,\n paletteAgentsSection: document.getElementById('palette-agents-section')!,\n paletteMessagesSection: document.getElementById('palette-messages-section')!,\n typingIndicator: document.getElementById('typing-indicator')!,\n threadPanelOverlay: document.getElementById('thread-panel-overlay')!,\n threadPanelId: document.getElementById('thread-panel-id')!,\n threadPanelClose: document.getElementById('thread-panel-close') as HTMLButtonElement,\n threadMessages: document.getElementById('thread-messages')!,\n threadMessageInput: document.getElementById('thread-message-input') as HTMLTextAreaElement,\n threadSendBtn: document.getElementById('thread-send-btn') as HTMLButtonElement,\n mentionAutocomplete: document.getElementById('mention-autocomplete')!,\n mentionAutocompleteList: document.getElementById('mention-autocomplete-list')!,\n };\n return elements;\n}\n\n/**\n * Get DOM elements\n */\nexport function getElements(): DOMElements {\n return elements;\n}\n\n/**\n * Update connection status indicator\n */\nexport function updateConnectionStatus(): void {\n if (state.isConnected) {\n elements.connectionDot.classList.remove('offline');\n } else {\n elements.connectionDot.classList.add('offline');\n }\n}\n\n/**\n * Render agents list in sidebar\n */\nexport function renderAgents(): void {\n console.log('[UI] renderAgents called, agents:', state.agents.length, state.agents.map(a => a.name));\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n const presenceClass = online ? 'online' : '';\n const isActive = state.currentChannel === agent.name;\n const needsAttentionClass = agent.needsAttention ? 'needs-attention' : '';\n\n return `\n
  • \n
    \n ${getInitials(agent.name)}\n \n
    \n ${escapeHtml(agent.name)}\n ${agent.needsAttention ? 'Needs Input' : ''}\n
    \n \n
    \n
  • \n `;\n })\n .join('');\n\n elements.agentsList.innerHTML =\n html ||\n '
  • No agents connected
  • ';\n\n // Add click handlers\n elements.agentsList.querySelectorAll('.channel-item[data-agent]').forEach((item) => {\n item.addEventListener('click', (e) => {\n // Don't navigate if clicking on action buttons\n if ((e.target as HTMLElement).closest('.agent-actions')) {\n return;\n }\n const agentName = item.dataset.agent;\n if (agentName) {\n selectChannel(agentName);\n }\n });\n });\n\n // Update command palette agents\n updatePaletteAgents();\n\n // Attach kill handlers (dynamically imported to avoid circular dependency)\n import('./app.js').then(({ attachKillHandlers }) => {\n attachKillHandlers();\n }).catch(() => {\n // Ignore if app.js not yet ready\n });\n}\n\n/**\n * Render messages list\n */\nexport function renderMessages(): void {\n const filtered = getFilteredMessages();\n\n if (filtered.length === 0) {\n elements.messagesList.innerHTML = `\n
    \n \n \n \n
    No messages yet
    \n
    \n ${\n state.currentChannel === 'general'\n ? 'Messages between agents will appear here'\n : `Messages with ${state.currentChannel} will appear here`\n }\n
    \n
    \n `;\n return;\n }\n\n let html = '';\n let lastDate: string | null = null;\n\n filtered.forEach((msg) => {\n const msgDate = new Date(msg.timestamp).toDateString();\n\n // Add date divider if needed\n if (msgDate !== lastDate) {\n html += `\n
    \n ${formatDate(msg.timestamp)}\n
    \n `;\n lastDate = msgDate;\n }\n\n const isBroadcast = msg.to === '*';\n const avatarColor = getAvatarColor(msg.from);\n const replyCount = getThreadReplyCount(msg.id);\n\n // Format: @From \u2192 @To: message (like Slack)\n // For cross-project messages, show project badge before agent name\n const recipientDisplay = isBroadcast\n ? '@everyone'\n : msg.project\n ? `${escapeHtml(msg.project)}@${escapeHtml(msg.to)}`\n : `@${escapeHtml(msg.to)}`;\n\n html += `\n
    \n
    \n ${getInitials(msg.from)}\n
    \n
    \n
    \n @${escapeHtml(msg.from)}\n \n \u2192 ${recipientDisplay}\n \n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n ${\n msg.thread\n ? `\n
    \n \n \n \n Thread: ${escapeHtml(msg.thread)}\n
    \n `\n : ''\n }\n ${\n replyCount > 0\n ? `\n
    \n \n \n \n ${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}\n
    \n `\n : ''\n }\n
    \n
    \n \n \n
    \n
    \n `;\n });\n\n elements.messagesList.innerHTML = html;\n\n // Note: Auto-scroll removed - interferes with manual scrolling through history\n\n // Attach thread click handlers\n attachThreadHandlers();\n}\n\n/**\n * Select a channel and update UI\n */\nexport function selectChannel(channel: ChannelType): void {\n setCurrentChannel(channel);\n\n // Update sidebar active states\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.channel === channel);\n });\n elements.agentsList.querySelectorAll('.channel-item').forEach((item) => {\n item.classList.toggle('active', item.dataset.agent === channel);\n });\n\n // Update header\n const prefixEl = document.querySelector('.channel-header-name .prefix');\n if (channel === 'general') {\n elements.currentChannelName.innerHTML = 'general';\n elements.channelTopic.textContent = 'All agent communications';\n if (prefixEl) prefixEl.textContent = '#';\n } else {\n elements.currentChannelName.innerHTML = escapeHtml(channel);\n const agent = state.agents.find((a) => a.name === channel);\n elements.channelTopic.textContent = agent?.status || 'Direct messages';\n if (prefixEl) prefixEl.textContent = '@';\n }\n\n // Update composer placeholder with @mention format\n elements.messageInput.placeholder =\n channel === 'general'\n ? '@AgentName message... (or @* to broadcast)'\n : `@${channel} your message here...`;\n\n // Re-render messages\n renderMessages();\n}\n\n/**\n * Update online count display\n */\nexport function updateOnlineCount(): void {\n const online = state.agents.filter((a) => isAgentOnline(a.lastSeen || a.lastActive)).length;\n elements.onlineCount.textContent = `${online} online`;\n}\n\n/**\n * Update agents in command palette\n */\nexport function updatePaletteAgents(): void {\n const html = state.agents\n .map((agent) => {\n const online = isAgentOnline(agent.lastSeen || agent.lastActive);\n return `\n
    \n
    \n
    \n ${getInitials(agent.name)}\n \n
    \n
    \n
    \n
    ${escapeHtml(agent.name)}
    \n
    ${online ? 'Online' : 'Offline'}
    \n
    \n
    \n `;\n })\n .join('');\n\n const section = elements.paletteAgentsSection;\n const items = section.querySelectorAll('.palette-item');\n items.forEach((item) => item.remove());\n section.insertAdjacentHTML('beforeend', html);\n\n // Add click handlers\n section.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n item.addEventListener('click', () => {\n const agentName = item.dataset.jumpAgent;\n if (agentName) {\n selectChannel(agentName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Initialize channel click handlers in command palette\n */\nexport function initPaletteChannels(): void {\n elements.paletteChannelsSection\n .querySelectorAll('.palette-item[data-jump-channel]')\n .forEach((item) => {\n item.addEventListener('click', () => {\n const channelName = item.dataset.jumpChannel;\n if (channelName) {\n selectChannel(channelName);\n closeCommandPalette();\n }\n });\n });\n}\n\n/**\n * Open command palette\n */\nexport function openCommandPalette(): void {\n elements.commandPaletteOverlay.classList.add('visible');\n elements.paletteSearch.value = '';\n elements.paletteSearch.focus();\n paletteSelectedIndex = -1;\n filterPaletteResults('');\n}\n\n/**\n * Get all visible palette items\n */\nexport function getVisiblePaletteItems(): HTMLElement[] {\n const allItems = Array.from(\n elements.paletteResults.querySelectorAll('.palette-item')\n );\n return allItems.filter((item) => item.style.display !== 'none');\n}\n\n/**\n * Update the selected palette item visually\n */\nexport function updatePaletteSelection(): void {\n const items = getVisiblePaletteItems();\n\n // Remove selection from all items\n items.forEach((item) => item.classList.remove('selected'));\n\n // Add selection to current item\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n const selectedItem = items[paletteSelectedIndex];\n selectedItem.classList.add('selected');\n\n // Scroll into view if needed\n selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });\n }\n}\n\n/**\n * Handle keyboard navigation in command palette\n */\nexport function handlePaletteKeydown(e: KeyboardEvent): void {\n const items = getVisiblePaletteItems();\n\n if (items.length === 0) return;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex < items.length - 1\n ? paletteSelectedIndex + 1\n : 0;\n updatePaletteSelection();\n break;\n\n case 'ArrowUp':\n e.preventDefault();\n paletteSelectedIndex = paletteSelectedIndex > 0\n ? paletteSelectedIndex - 1\n : items.length - 1;\n updatePaletteSelection();\n break;\n\n case 'Enter':\n e.preventDefault();\n if (paletteSelectedIndex >= 0 && paletteSelectedIndex < items.length) {\n executePaletteItem(items[paletteSelectedIndex]);\n }\n break;\n }\n}\n\n/**\n * Execute the action for a palette item\n */\nexport function executePaletteItem(item: HTMLElement): void {\n // Check for command\n const command = item.dataset.command;\n if (command) {\n if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n closeCommandPalette();\n return;\n }\n\n // Check for channel jump\n const channel = item.dataset.jumpChannel;\n if (channel) {\n selectChannel(channel);\n closeCommandPalette();\n return;\n }\n\n // Check for agent jump\n const agent = item.dataset.jumpAgent;\n if (agent) {\n selectChannel(agent);\n closeCommandPalette();\n return;\n }\n\n // Check for message jump\n const messageId = item.dataset.jumpMessage;\n if (messageId) {\n // Find and scroll to the message\n const messageEl = elements.messagesList.querySelector(`[data-id=\"${messageId}\"]`);\n if (messageEl) {\n messageEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n messageEl.classList.add('highlighted');\n setTimeout(() => messageEl.classList.remove('highlighted'), 2000);\n }\n closeCommandPalette();\n return;\n }\n}\n\n/**\n * Close command palette\n */\nexport function closeCommandPalette(): void {\n elements.commandPaletteOverlay.classList.remove('visible');\n}\n\n/**\n * Filter command palette results based on query\n */\nexport function filterPaletteResults(query: string): void {\n const q = query.toLowerCase();\n\n // Reset selection when filtering\n paletteSelectedIndex = -1;\n\n // Filter command items\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter channel items\n document.querySelectorAll('.palette-item[data-jump-channel]').forEach((item) => {\n const titleEl = item.querySelector('.palette-item-title');\n const title = titleEl?.textContent?.toLowerCase() || '';\n item.style.display = title.includes(q) ? 'flex' : 'none';\n });\n\n // Filter agent items\n document.querySelectorAll('.palette-item[data-jump-agent]').forEach((item) => {\n const name = item.dataset.jumpAgent?.toLowerCase() || '';\n item.style.display = name.includes(q) ? 'flex' : 'none';\n });\n\n // Show message search if query is long enough\n if (q.length >= 2) {\n const matches = state.messages.filter((m) => m.content.toLowerCase().includes(q)).slice(0, 5);\n\n if (matches.length > 0) {\n elements.paletteMessagesSection.style.display = 'block';\n const items = matches\n .map(\n (m) => `\n
    \n
    \n \n \n \n
    \n
    \n
    ${escapeHtml(m.from)}
    \n
    ${escapeHtml(m.content.substring(0, 60))}${m.content.length > 60 ? '...' : ''}
    \n
    \n
    \n `\n )\n .join('');\n\n const existingItems = elements.paletteMessagesSection.querySelectorAll('.palette-item');\n existingItems.forEach((item) => item.remove());\n elements.paletteMessagesSection.insertAdjacentHTML('beforeend', items);\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n } else {\n elements.paletteMessagesSection.style.display = 'none';\n }\n}\n\n/**\n * Open thread panel for a specific thread\n */\nexport function openThreadPanel(threadId: string): void {\n setCurrentThread(threadId);\n elements.threadPanelId.textContent = threadId;\n elements.threadPanelOverlay.classList.add('visible');\n elements.threadMessageInput.value = '';\n renderThreadMessages(threadId);\n elements.threadMessageInput.focus();\n}\n\n/**\n * Close thread panel\n */\nexport function closeThreadPanel(): void {\n setCurrentThread(null);\n elements.threadPanelOverlay.classList.remove('visible');\n}\n\n/**\n * Render messages in thread panel\n */\nexport function renderThreadMessages(threadId: string): void {\n const messages = getThreadMessages(threadId);\n\n if (messages.length === 0) {\n elements.threadMessages.innerHTML = `\n
    \n

    No messages in this thread yet.

    \n

    Start the conversation below!

    \n
    \n `;\n return;\n }\n\n const html = messages\n .map((msg) => `\n
    \n
    \n
    \n ${getInitials(msg.from)}\n
    \n ${escapeHtml(msg.from)}\n ${formatTime(msg.timestamp)}\n
    \n
    ${formatMessageBody(msg.content)}
    \n
    \n `)\n .join('');\n\n elements.threadMessages.innerHTML = html;\n\n // Scroll to bottom\n elements.threadMessages.scrollTop = elements.threadMessages.scrollHeight;\n}\n\n/**\n * Attach thread click handlers to messages (call after renderMessages)\n */\nexport function attachThreadHandlers(): void {\n // Thread indicator clicks\n elements.messagesList.querySelectorAll('.thread-indicator').forEach((el) => {\n el.style.cursor = 'pointer';\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply count badge clicks\n elements.messagesList.querySelectorAll('.reply-count-badge').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const threadId = el.dataset.thread;\n if (threadId) {\n openThreadPanel(threadId);\n }\n });\n });\n\n // Reply in thread button clicks\n elements.messagesList.querySelectorAll('.message-action-btn[data-action=\"reply\"]').forEach((el) => {\n el.addEventListener('click', (e) => {\n e.stopPropagation();\n const messageId = el.closest('.message')?.getAttribute('data-id');\n if (messageId) {\n // Use message ID as thread ID for new threads\n openThreadPanel(messageId);\n }\n });\n });\n}\n\n/**\n * @-Mention Autocomplete State\n */\nlet mentionSelectedIndex = 0;\nlet mentionFilteredAgents: typeof state.agents = [];\n\n/**\n * Show mention autocomplete dropdown with filtered agents\n */\nexport function showMentionAutocomplete(filter: string): void {\n const filterLower = filter.toLowerCase();\n\n // Filter agents by name, include broadcast option\n mentionFilteredAgents = state.agents.filter(agent =>\n agent.name.toLowerCase().includes(filterLower)\n );\n\n // Reset selection\n mentionSelectedIndex = 0;\n\n // Build HTML for agent list\n let html = '';\n\n // Add broadcast option if filter matches\n if ('*'.includes(filterLower) || 'everyone'.includes(filterLower) || 'all'.includes(filterLower) || 'broadcast'.includes(filterLower)) {\n html += `\n
    \n
    *
    \n @everyone\n Broadcast to all\n
    \n `;\n }\n\n // Add agents\n mentionFilteredAgents.forEach((agent, index) => {\n const isSelected = index === mentionSelectedIndex;\n html += `\n
    \n
    \n ${getInitials(agent.name)}\n
    \n @${escapeHtml(agent.name)}\n ${escapeHtml(agent.role || 'Agent')}\n
    \n `;\n });\n\n if (html === '') {\n html = '
    No matching agents
    ';\n }\n\n elements.mentionAutocompleteList.innerHTML = html;\n elements.mentionAutocomplete.classList.add('visible');\n\n // Add click handlers to items\n elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]').forEach((item) => {\n item.addEventListener('click', () => {\n const mention = item.dataset.mention;\n if (mention) {\n completeMention(mention);\n }\n });\n });\n}\n\n/**\n * Hide mention autocomplete dropdown\n */\nexport function hideMentionAutocomplete(): void {\n elements.mentionAutocomplete.classList.remove('visible');\n mentionFilteredAgents = [];\n mentionSelectedIndex = 0;\n}\n\n/**\n * Check if mention autocomplete is visible\n */\nexport function isMentionAutocompleteVisible(): boolean {\n return elements.mentionAutocomplete.classList.contains('visible');\n}\n\n/**\n * Navigate mention autocomplete selection\n */\nexport function navigateMentionAutocomplete(direction: 'up' | 'down'): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n if (items.length === 0) return;\n\n // Remove current selection\n items[mentionSelectedIndex]?.classList.remove('selected');\n\n // Update index\n if (direction === 'down') {\n mentionSelectedIndex = (mentionSelectedIndex + 1) % items.length;\n } else {\n mentionSelectedIndex = (mentionSelectedIndex - 1 + items.length) % items.length;\n }\n\n // Add new selection\n items[mentionSelectedIndex]?.classList.add('selected');\n items[mentionSelectedIndex]?.scrollIntoView({ block: 'nearest' });\n}\n\n/**\n * Complete the current mention selection\n */\nexport function completeMention(mention?: string): void {\n const items = elements.mentionAutocompleteList.querySelectorAll('.mention-autocomplete-item[data-mention]');\n\n // Use provided mention or get from selected item\n let selectedMention = mention;\n if (!selectedMention && items.length > 0) {\n selectedMention = items[mentionSelectedIndex]?.dataset.mention;\n }\n\n if (!selectedMention) {\n hideMentionAutocomplete();\n return;\n }\n\n // Replace the @... text with the completed mention\n const input = elements.messageInput;\n const value = input.value;\n\n // Find the @ position (should be at start or after whitespace)\n const atMatch = value.match(/^@\\S*/);\n if (atMatch) {\n // Replace the @partial with @CompletedName\n const completedText = `@${selectedMention} `;\n input.value = completedText + value.substring(atMatch[0].length);\n input.selectionStart = input.selectionEnd = completedText.length;\n }\n\n hideMentionAutocomplete();\n input.focus();\n}\n\n/**\n * Get the current @mention being typed (if any)\n */\nexport function getCurrentMentionQuery(): string | null {\n const input = elements.messageInput;\n const value = input.value;\n const cursorPos = input.selectionStart;\n\n // Check if cursor is within an @mention at the start\n const atMatch = value.match(/^@(\\S*)/);\n if (atMatch && cursorPos <= atMatch[0].length) {\n return atMatch[1]; // Return the text after @\n }\n\n return null;\n}\n", "/**\n * Dashboard Application Entry Point\n */\n\nimport { subscribe } from './state.js';\nimport { connect, sendMessage } from './websocket.js';\nimport {\n initElements,\n getElements,\n updateConnectionStatus,\n renderAgents,\n renderMessages,\n selectChannel,\n updateOnlineCount,\n openCommandPalette,\n closeCommandPalette,\n filterPaletteResults,\n handlePaletteKeydown,\n initPaletteChannels,\n closeThreadPanel,\n renderThreadMessages,\n showMentionAutocomplete,\n hideMentionAutocomplete,\n isMentionAutocompleteVisible,\n navigateMentionAutocomplete,\n completeMention,\n getCurrentMentionQuery,\n} from './components.js';\nimport { state } from './state.js';\n\n/**\n * Detect if we're viewing a project dashboard from bridge context\n */\nfunction detectProjectContext(): { projectId: string | null; fromBridge: boolean } {\n const pathname = window.location.pathname;\n const match = pathname.match(/^\\/project\\/([^/]+)$/);\n\n if (match) {\n return { projectId: decodeURIComponent(match[1]), fromBridge: true };\n }\n\n return { projectId: null, fromBridge: false };\n}\n\n/**\n * Update the UI for project context (when accessed from bridge)\n */\nasync function setupProjectContext(projectId: string): Promise {\n // Update workspace name to show project\n const workspaceName = document.querySelector('.workspace-name');\n if (workspaceName) {\n // Fetch project info\n try {\n const response = await fetch(`/api/project/${encodeURIComponent(projectId)}`);\n if (response.ok) {\n const project = await response.json();\n const nameSpan = workspaceName.querySelector(':not(.status-dot)');\n if (nameSpan && nameSpan.nodeType === Node.TEXT_NODE) {\n nameSpan.textContent = project.name || projectId;\n } else {\n // Replace text content after status-dot\n const textNodes = Array.from(workspaceName.childNodes).filter(n => n.nodeType === Node.TEXT_NODE);\n textNodes.forEach(n => n.textContent = '');\n workspaceName.appendChild(document.createTextNode(' ' + (project.name || projectId)));\n }\n }\n } catch {\n // Fallback - just show project ID\n }\n }\n\n // Update bridge nav link to show \"Back to Bridge\" with back arrow\n const bridgeLinkText = document.getElementById('bridge-link-text');\n const bridgeNavLink = document.getElementById('bridge-nav-link');\n if (bridgeLinkText) {\n bridgeLinkText.textContent = '\u2190 Back to Bridge';\n }\n if (bridgeNavLink) {\n bridgeNavLink.classList.add('back-to-bridge');\n }\n\n // Add a subtle indicator that we're in project view\n document.body.classList.add('project-view');\n}\n\n/**\n * Initialize the dashboard application\n */\nexport function initApp(): void {\n const elements = initElements();\n\n // Check if we're in project context (from bridge)\n const { projectId, fromBridge } = detectProjectContext();\n if (fromBridge && projectId) {\n setupProjectContext(projectId);\n }\n\n // Subscribe to state changes\n subscribe(() => {\n updateConnectionStatus();\n renderAgents();\n renderMessages();\n updateOnlineCount();\n });\n\n // Set up event listeners\n setupEventListeners(elements);\n\n // Connect to WebSocket\n connect();\n}\n\n/**\n * Set up all event listeners\n */\nfunction setupEventListeners(elements: ReturnType): void {\n // Channel clicks\n elements.channelsList.querySelectorAll('.channel-item').forEach((item) => {\n item.addEventListener('click', () => {\n const channel = item.dataset.channel;\n if (channel) {\n selectChannel(channel);\n }\n });\n });\n\n // Send button\n elements.sendBtn.addEventListener('click', handleSend);\n\n // Keyboard shortcuts for composer\n elements.messageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n // Handle mention autocomplete keys first\n if (isMentionAutocompleteVisible()) {\n if (e.key === 'Tab' || e.key === 'Enter') {\n e.preventDefault();\n completeMention();\n return;\n }\n if (e.key === 'ArrowUp') {\n e.preventDefault();\n navigateMentionAutocomplete('up');\n return;\n }\n if (e.key === 'ArrowDown') {\n e.preventDefault();\n navigateMentionAutocomplete('down');\n return;\n }\n if (e.key === 'Escape') {\n e.preventDefault();\n hideMentionAutocomplete();\n return;\n }\n }\n\n // Enter to send (Slack-style), Shift+Enter for newline\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Auto-resize textarea and handle @-mention autocomplete\n elements.messageInput.addEventListener('input', () => {\n elements.messageInput.style.height = 'auto';\n elements.messageInput.style.height =\n Math.min(elements.messageInput.scrollHeight, 200) + 'px';\n\n // Check for @-mention at start of input\n const query = getCurrentMentionQuery();\n if (query !== null) {\n showMentionAutocomplete(query);\n } else {\n hideMentionAutocomplete();\n }\n });\n\n // Hide mention autocomplete when input loses focus (with delay to allow clicks)\n elements.messageInput.addEventListener('blur', () => {\n setTimeout(() => {\n hideMentionAutocomplete();\n }, 150);\n });\n\n // Bold button - wrap selected text with ** or insert **bold**\n elements.boldBtn.addEventListener('click', () => {\n const input = elements.messageInput;\n const start = input.selectionStart;\n const end = input.selectionEnd;\n const text = input.value;\n\n if (start === end) {\n // No selection - insert **bold** placeholder\n const before = text.substring(0, start);\n const after = text.substring(end);\n input.value = before + '**bold**' + after;\n input.selectionStart = start + 2;\n input.selectionEnd = start + 6;\n } else {\n // Wrap selection with **\n const before = text.substring(0, start);\n const selected = text.substring(start, end);\n const after = text.substring(end);\n input.value = before + '**' + selected + '**' + after;\n input.selectionStart = start;\n input.selectionEnd = end + 4;\n }\n input.focus();\n });\n\n // Emoji button - insert common emojis via simple picker\n elements.emojiBtn.addEventListener('click', () => {\n const emojis = ['\uD83D\uDC4D', '\uD83D\uDC4E', '\u2705', '\u274C', '\uD83C\uDF89', '\uD83D\uDD25', '\uD83D\uDCA1', '\u26A0\uFE0F', '\uD83D\uDCDD', '\uD83D\uDE80'];\n const emoji = emojis[Math.floor(Math.random() * emojis.length)];\n const input = elements.messageInput;\n const start = input.selectionStart;\n const text = input.value;\n input.value = text.substring(0, start) + emoji + text.substring(start);\n input.selectionStart = input.selectionEnd = start + emoji.length;\n input.focus();\n });\n\n // Command palette\n elements.searchTrigger.addEventListener('click', openCommandPalette);\n\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\n e.preventDefault();\n if (elements.commandPaletteOverlay.classList.contains('visible')) {\n closeCommandPalette();\n } else {\n openCommandPalette();\n }\n }\n\n if (e.key === 'Escape') {\n closeCommandPalette();\n }\n });\n\n elements.commandPaletteOverlay.addEventListener('click', (e: MouseEvent) => {\n if (e.target === elements.commandPaletteOverlay) {\n closeCommandPalette();\n }\n });\n\n elements.paletteSearch.addEventListener('input', (e: Event) => {\n const target = e.target as HTMLInputElement;\n filterPaletteResults(target.value);\n });\n\n elements.paletteSearch.addEventListener('keydown', handlePaletteKeydown);\n\n // Command execution\n document.querySelectorAll('.palette-item[data-command]').forEach((item) => {\n item.addEventListener('click', () => {\n const command = item.dataset.command;\n\n if (command === 'bridge') {\n // Navigate to bridge view\n window.location.href = '/bridge';\n } else if (command === 'broadcast') {\n // Pre-fill message input with @* for broadcast\n elements.messageInput.value = '@* ';\n elements.messageInput.focus();\n } else if (command === 'clear') {\n elements.messagesList.innerHTML = '';\n }\n\n closeCommandPalette();\n });\n });\n\n // Add Cmd/Ctrl+B shortcut for bridge navigation\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if ((e.ctrlKey || e.metaKey) && e.key === 'b') {\n e.preventDefault();\n window.location.href = '/bridge';\n }\n });\n\n // Initialize palette channel click handlers\n initPaletteChannels();\n\n // Thread panel close button\n elements.threadPanelClose.addEventListener('click', closeThreadPanel);\n\n // Thread panel send button\n elements.threadSendBtn.addEventListener('click', handleThreadSend);\n\n // Thread message input keyboard shortcuts (Slack-style)\n elements.threadMessageInput.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n handleThreadSend();\n }\n // Shift+Enter allows default behavior (inserts newline)\n });\n\n // Close thread panel on Escape\n document.addEventListener('keydown', (e: KeyboardEvent) => {\n if (e.key === 'Escape' && elements.threadPanelOverlay.classList.contains('visible')) {\n closeThreadPanel();\n }\n });\n}\n\n/**\n * Parse @mention from message text\n * Formats: \"@AgentName message\" or \"@* message\" for broadcast\n * Returns { to, message } or null if no valid mention found\n */\nfunction parseMention(text: string): { to: string; message: string } | null {\n const trimmed = text.trim();\n\n // Match @mention at the start of the message\n // @* for broadcast, @AgentName for direct message\n const match = trimmed.match(/^@(\\*|[^\\s]+)\\s+(.+)$/s);\n\n if (!match) {\n return null;\n }\n\n return {\n to: match[1],\n message: match[2].trim(),\n };\n}\n\n/**\n * Handle send button click\n */\nasync function handleSend(): Promise {\n const elements = getElements();\n const rawMessage = elements.messageInput.value.trim();\n\n if (!rawMessage) {\n return;\n }\n\n // Parse @mention from the message\n const parsed = parseMention(rawMessage);\n\n if (!parsed) {\n alert('Message must start with @recipient (e.g., \"@Lead hello\" or \"@* broadcast\")');\n return;\n }\n\n const { to, message } = parsed;\n\n elements.sendBtn.disabled = true;\n\n const result = await sendMessage(to, message);\n\n if (result.success) {\n elements.messageInput.value = '';\n elements.messageInput.style.height = 'auto';\n } else {\n alert(result.error);\n }\n\n elements.sendBtn.disabled = false;\n}\n\n/**\n * Handle thread panel send button click\n */\nasync function handleThreadSend(): Promise {\n const elements = getElements();\n const message = elements.threadMessageInput.value.trim();\n const threadId = state.currentThread;\n\n if (!message || !threadId) {\n return;\n }\n\n // For thread replies, send to broadcast or use original recipient\n // For now, send as broadcast with thread ID\n elements.threadSendBtn.disabled = true;\n\n const result = await sendMessage('*', message, threadId);\n\n if (result.success) {\n elements.threadMessageInput.value = '';\n // Re-render thread messages to show the new message\n renderThreadMessages(threadId);\n } else {\n alert(result.error);\n }\n\n elements.threadSendBtn.disabled = false;\n}\n\n/**\n * Spawn Agent Modal Management\n */\ninterface SpawnModalElements {\n overlay: HTMLElement | null;\n closeBtn: HTMLElement | null;\n cancelBtn: HTMLElement | null;\n submitBtn: HTMLElement | null;\n nameInput: HTMLInputElement | null;\n cliSelect: HTMLSelectElement | null;\n modelInput: HTMLInputElement | null;\n taskInput: HTMLTextAreaElement | null;\n}\n\nfunction getSpawnModalElements(): SpawnModalElements {\n return {\n overlay: document.getElementById('spawn-modal-overlay'),\n closeBtn: document.getElementById('spawn-modal-close'),\n cancelBtn: document.getElementById('spawn-modal-cancel'),\n submitBtn: document.getElementById('spawn-modal-submit'),\n nameInput: document.getElementById('spawn-agent-name') as HTMLInputElement,\n cliSelect: document.getElementById('spawn-agent-cli') as HTMLSelectElement,\n modelInput: document.getElementById('spawn-agent-model') as HTMLInputElement,\n taskInput: document.getElementById('spawn-agent-task') as HTMLTextAreaElement,\n };\n}\n\nfunction openSpawnModal(): void {\n const modal = getSpawnModalElements();\n if (modal.overlay) {\n modal.overlay.classList.add('visible');\n modal.nameInput?.focus();\n }\n}\n\nfunction closeSpawnModal(): void {\n const modal = getSpawnModalElements();\n if (modal.overlay) {\n modal.overlay.classList.remove('visible');\n // Clear form\n if (modal.nameInput) modal.nameInput.value = '';\n if (modal.cliSelect) modal.cliSelect.value = 'claude';\n if (modal.modelInput) modal.modelInput.value = '';\n if (modal.taskInput) modal.taskInput.value = '';\n }\n}\n\nasync function handleSpawnAgent(): Promise {\n const modal = getSpawnModalElements();\n\n const name = modal.nameInput?.value.trim();\n const cli = modal.cliSelect?.value;\n const model = modal.modelInput?.value.trim();\n const task = modal.taskInput?.value.trim();\n\n if (!name) {\n alert('Please enter an agent name');\n return;\n }\n\n if (!cli) {\n alert('Please select a CLI tool');\n return;\n }\n\n // Disable submit button\n if (modal.submitBtn) {\n modal.submitBtn.textContent = 'Spawning...';\n (modal.submitBtn as HTMLButtonElement).disabled = true;\n }\n\n try {\n const response = await fetch('/api/agent/spawn', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({\n name,\n cli,\n model: model || undefined,\n task: task || undefined,\n }),\n });\n\n const result = await response.json();\n\n if (response.ok && result.success) {\n closeSpawnModal();\n // Agent will appear in the list when it connects to the relay\n } else {\n alert(result.error || 'Failed to spawn agent');\n }\n } catch (err) {\n console.error('Failed to spawn agent:', err);\n alert('Failed to spawn agent. Check console for details.');\n } finally {\n if (modal.submitBtn) {\n modal.submitBtn.textContent = 'Spawn Agent';\n (modal.submitBtn as HTMLButtonElement).disabled = false;\n }\n }\n}\n\n/**\n * Kill an agent\n */\nasync function killAgent(agentName: string): Promise {\n if (!confirm(`Are you sure you want to kill agent \"${agentName}\"?`)) {\n return;\n }\n\n try {\n const response = await fetch('/api/agent/kill', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ name: agentName }),\n });\n\n const result = await response.json();\n\n if (!response.ok) {\n alert(result.error || 'Failed to kill agent');\n }\n // Agent will disappear from the list when it disconnects\n } catch (err) {\n console.error('Failed to kill agent:', err);\n alert('Failed to kill agent. Check console for details.');\n }\n}\n\n/**\n * Set up spawn modal event listeners\n */\nfunction setupSpawnModalListeners(): void {\n const modal = getSpawnModalElements();\n\n // Spawn button in sidebar\n const spawnBtn = document.getElementById('spawn-agent-btn');\n if (spawnBtn) {\n spawnBtn.addEventListener('click', (e) => {\n e.stopPropagation();\n openSpawnModal();\n });\n }\n\n // Modal close buttons\n modal.closeBtn?.addEventListener('click', closeSpawnModal);\n modal.cancelBtn?.addEventListener('click', closeSpawnModal);\n\n // Modal submit\n modal.submitBtn?.addEventListener('click', handleSpawnAgent);\n\n // Close on overlay click\n modal.overlay?.addEventListener('click', (e) => {\n if (e.target === modal.overlay) {\n closeSpawnModal();\n }\n });\n\n // Close on Escape\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && modal.overlay?.classList.contains('visible')) {\n closeSpawnModal();\n }\n });\n\n // Enter to submit (when name input is focused)\n modal.nameInput?.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n handleSpawnAgent();\n }\n });\n}\n\n/**\n * Attach kill handlers to agent list items\n * Called after agents are rendered\n */\nexport function attachKillHandlers(): void {\n document.querySelectorAll('.agent-kill-btn').forEach((btn) => {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n const agentName = btn.dataset.agent;\n if (agentName) {\n killAgent(agentName);\n }\n });\n });\n}\n\n// Auto-initialize when DOM is ready\nif (typeof document !== 'undefined') {\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', () => {\n initApp();\n setupSpawnModalListeners();\n });\n } else {\n initApp();\n setupSpawnModalListeners();\n }\n}\n"], + "mappings": "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", + "names": ["subscribe", "listener", "listeners", "index", "notifyListeners", "setAgents", "agents", "state", "setMessages", "messages", "setCurrentChannel", "channel", "setConnectionStatus", "connected", "incrementReconnectAttempts", "setWebSocket", "ws", "getFilteredMessages", "currentChannel", "m", "setCurrentThread", "thread", "getThreadMessages", "threadId", "getThreadReplyCount", "init_state", "__esmMin", "connect", "protocol", "ws", "setConnectionStatus", "delay", "state", "incrementReconnectAttempts", "error", "event", "data", "handleData", "e", "setWebSocket", "a", "setAgents", "setMessages", "dataHandler", "sendMessage", "to", "message", "thread", "body", "response", "result", "init_websocket", "__esmMin", "init_state", "isAgentOnline", "lastSeen", "ts", "escapeHtml", "text", "div", "formatTime", "timestamp", "formatDate", "date", "today", "yesterday", "getAvatarColor", "name", "colors", "hash", "i", "getInitials", "formatMessageBody", "content", "escaped", "init_utils", "__esmMin", "initElements", "elements", "getElements", "updateConnectionStatus", "state", "renderAgents", "a", "html", "agent", "presenceClass", "isAgentOnline", "isActive", "needsAttentionClass", "escapeHtml", "getAvatarColor", "getInitials", "item", "e", "agentName", "selectChannel", "updatePaletteAgents", "attachKillHandlers", "renderMessages", "filtered", "getFilteredMessages", "lastDate", "msg", "msgDate", "formatDate", "isBroadcast", "avatarColor", "replyCount", "getThreadReplyCount", "recipientDisplay", "formatTime", "formatMessageBody", "attachThreadHandlers", "channel", "setCurrentChannel", "prefixEl", "updateOnlineCount", "online", "section", "closeCommandPalette", "initPaletteChannels", "channelName", "openCommandPalette", "paletteSelectedIndex", "filterPaletteResults", "getVisiblePaletteItems", "updatePaletteSelection", "items", "selectedItem", "handlePaletteKeydown", "executePaletteItem", "command", "messageId", "messageEl", "query", "q", "title", "name", "matches", "m", "openThreadPanel", "threadId", "setCurrentThread", "renderThreadMessages", "closeThreadPanel", "messages", "getThreadMessages", "el", "showMentionAutocomplete", "filter", "filterLower", "mentionFilteredAgents", "mentionSelectedIndex", "index", "mention", "completeMention", "hideMentionAutocomplete", "isMentionAutocompleteVisible", "navigateMentionAutocomplete", "direction", "selectedMention", "input", "value", "atMatch", "completedText", "getCurrentMentionQuery", "cursorPos", "init_components", "__esmMin", "init_state", "init_utils", "app_exports", "__export", "attachKillHandlers", "initApp", "detectProjectContext", "match", "setupProjectContext", "projectId", "workspaceName", "response", "project", "nameSpan", "n", "bridgeLinkText", "bridgeNavLink", "elements", "initElements", "fromBridge", "subscribe", "updateConnectionStatus", "renderAgents", "renderMessages", "updateOnlineCount", "setupEventListeners", "connect", "item", "channel", "selectChannel", "handleSend", "isMentionAutocompleteVisible", "completeMention", "navigateMentionAutocomplete", "hideMentionAutocomplete", "query", "getCurrentMentionQuery", "showMentionAutocomplete", "input", "start", "end", "text", "before", "after", "selected", "emojis", "emoji", "openCommandPalette", "closeCommandPalette", "target", "filterPaletteResults", "handlePaletteKeydown", "command", "initPaletteChannels", "closeThreadPanel", "handleThreadSend", "parseMention", "getElements", "rawMessage", "parsed", "to", "message", "result", "sendMessage", "threadId", "state", "renderThreadMessages", "getSpawnModalElements", "openSpawnModal", "modal", "closeSpawnModal", "handleSpawnAgent", "name", "cli", "model", "task", "err", "killAgent", "agentName", "setupSpawnModalListeners", "spawnBtn", "e", "btn", "init_app", "__esmMin", "init_state", "init_websocket", "init_components"] } diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index ff68b811f..9ecfb5e3a 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -11,6 +11,7 @@ import { RelayClient } from '../wrapper/client.js'; import { computeNeedsAttention } from './needs-attention.js'; import { MultiProjectClient } from '../bridge/multi-project-client.js'; import type { ProjectConfig } from '../bridge/types.js'; +import { AgentSpawner } from '../bridge/spawner.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -239,6 +240,160 @@ export async function startDashboard(port: number, dataDir: string, teamDir: str // Start bridge client connection (non-blocking) connectBridgeClient().catch(() => {}); + // Agent spawners by project path + const spawners = new Map(); + + /** + * Get or create a spawner for a project + */ + const getSpawner = (projectPath: string): AgentSpawner => { + let spawner = spawners.get(projectPath); + if (!spawner) { + spawner = new AgentSpawner(projectPath); + spawners.set(projectPath, spawner); + } + return spawner; + }; + + /** + * Get the current project root from the data directory + * (For single-project dashboard, the project is the parent of the data dir) + */ + const getCurrentProjectRoot = (): string => { + // The dataDir is typically ~/.agent-relay// + // For bridge mode, projects have their own paths + // For now, try to detect from cwd or use parent of dataDir + return process.cwd(); + }; + + // Supported CLI options + const SUPPORTED_CLIS = ['claude', 'codex', 'gemini'] as const; + + // API endpoint to spawn a new agent + app.post('/api/agent/spawn', async (req, res) => { + const { name, cli, model, task, projectId, projectPath } = req.body; + + if (!name || !cli) { + return res.status(400).json({ error: 'Missing "name" or "cli" field' }); + } + + // Validate CLI + if (!SUPPORTED_CLIS.includes(cli)) { + return res.status(400).json({ + error: `Invalid CLI "${cli}". Supported: ${SUPPORTED_CLIS.join(', ')}` + }); + } + + // Determine the project root + let targetProjectPath = projectPath; + if (!targetProjectPath && projectId) { + // Look up project path from bridge state + const bridgeStatePath = path.join(dataDir, 'bridge-state.json'); + if (fs.existsSync(bridgeStatePath)) { + try { + const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8')); + const project = (bridgeState.projects || []).find((p: { id: string }) => p.id === projectId); + if (project) { + targetProjectPath = project.path; + } + } catch { + // Ignore + } + } + } + if (!targetProjectPath) { + targetProjectPath = getCurrentProjectRoot(); + } + + // Build CLI string with optional model + let cliString = cli; + if (model) { + cliString = `${cli}:${model}`; + } + + const spawner = getSpawner(targetProjectPath); + + try { + const result = await spawner.spawn({ + name, + cli: cliString, + task: task || `You are ${name}, an AI agent working on this project.`, + requestedBy: 'Dashboard', + }); + + if (result.success) { + res.json({ + success: true, + name: result.name, + window: result.window, + projectPath: targetProjectPath, + }); + } else { + res.status(400).json({ error: result.error || 'Failed to spawn agent' }); + } + } catch (err: any) { + console.error('[dashboard] Failed to spawn agent:', err); + res.status(500).json({ error: err.message || 'Failed to spawn agent' }); + } + }); + + // API endpoint to kill an agent + app.post('/api/agent/kill', async (req, res) => { + const { name, projectId, projectPath } = req.body; + + if (!name) { + return res.status(400).json({ error: 'Missing "name" field' }); + } + + // Determine the project root + let targetProjectPath = projectPath; + if (!targetProjectPath && projectId) { + // Look up project path from bridge state + const bridgeStatePath = path.join(dataDir, 'bridge-state.json'); + if (fs.existsSync(bridgeStatePath)) { + try { + const bridgeState = JSON.parse(fs.readFileSync(bridgeStatePath, 'utf-8')); + const project = (bridgeState.projects || []).find((p: { id: string }) => p.id === projectId); + if (project) { + targetProjectPath = project.path; + } + } catch { + // Ignore + } + } + } + if (!targetProjectPath) { + targetProjectPath = getCurrentProjectRoot(); + } + + const spawner = getSpawner(targetProjectPath); + + try { + const released = await spawner.release(name); + if (released) { + res.json({ success: true, name }); + } else { + res.status(404).json({ error: `Agent "${name}" not found or already released` }); + } + } catch (err: any) { + console.error('[dashboard] Failed to kill agent:', err); + res.status(500).json({ error: err.message || 'Failed to kill agent' }); + } + }); + + // API endpoint to get spawned agents + app.get('/api/agents/spawned', (req, res) => { + const projectPath = req.query.projectPath as string || getCurrentProjectRoot(); + const spawner = spawners.get(projectPath); + + if (!spawner) { + res.json({ agents: [] }); + return; + } + + res.json({ agents: spawner.getActiveWorkers() }); + }); + // API endpoint to send messages app.post('/api/send', async (req, res) => { const { to, message, thread } = req.body; From 5dce5e093cc82724b1befca89bc73568beb04466 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 25 Dec 2025 17:18:03 +0000 Subject: [PATCH 4/4] Fix TypeScript compilation errors after merge - Update startDashboard to accept options object instead of positional args - Add spawn modal element references to types.ts and components.ts - Add spawn modal functions (openSpawnModal, closeSpawnModal, spawnAgent, fetchSpawnedAgents) - Align element IDs with HTML (spawn-agent-name, spawn-agent-cli, etc.) --- src/dashboard/frontend/components.ts | 101 +++++++++++++++++++++++++++ src/dashboard/frontend/types.ts | 13 ++-- src/dashboard/server.ts | 12 +++- src/dashboard/start.ts | 7 +- 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/dashboard/frontend/components.ts b/src/dashboard/frontend/components.ts index 2626d9fe0..0749e4879 100644 --- a/src/dashboard/frontend/components.ts +++ b/src/dashboard/frontend/components.ts @@ -49,6 +49,16 @@ export function initElements(): DOMElements { threadSendBtn: document.getElementById('thread-send-btn') as HTMLButtonElement, mentionAutocomplete: document.getElementById('mention-autocomplete')!, mentionAutocompleteList: document.getElementById('mention-autocomplete-list')!, + // Spawn modal elements + spawnAgentBtn: document.getElementById('spawn-agent-btn') as HTMLButtonElement, + spawnModalOverlay: document.getElementById('spawn-modal-overlay')!, + spawnModalClose: document.getElementById('spawn-modal-close') as HTMLButtonElement, + spawnAgentName: document.getElementById('spawn-agent-name') as HTMLInputElement, + spawnAgentCli: document.getElementById('spawn-agent-cli') as HTMLSelectElement, + spawnAgentModel: document.getElementById('spawn-agent-model') as HTMLInputElement, + spawnAgentTask: document.getElementById('spawn-agent-task') as HTMLTextAreaElement, + spawnModalCancel: document.getElementById('spawn-modal-cancel') as HTMLButtonElement, + spawnModalSubmit: document.getElementById('spawn-modal-submit') as HTMLButtonElement, }; return elements; } @@ -800,3 +810,94 @@ export function getCurrentMentionQuery(): string | null { return null; } + +// Track spawned agents +let spawnedAgents: string[] = []; + +/** + * Open the spawn agent modal + */ +export function openSpawnModal(): void { + elements.spawnModalOverlay.classList.add('visible'); + elements.spawnAgentName.value = ''; + elements.spawnAgentCli.value = 'claude'; + elements.spawnAgentModel.value = ''; + elements.spawnAgentTask.value = ''; + elements.spawnAgentName.focus(); +} + +/** + * Close the spawn agent modal + */ +export function closeSpawnModal(): void { + elements.spawnModalOverlay.classList.remove('visible'); +} + +/** + * Spawn a new agent via the API + */ +export async function spawnAgent(): Promise<{ success: boolean; error?: string }> { + const name = elements.spawnAgentName.value.trim(); + const cli = elements.spawnAgentCli.value || 'claude'; + const model = elements.spawnAgentModel.value.trim(); + const task = elements.spawnAgentTask.value.trim(); + + if (!name) { + return { success: false, error: 'Agent name is required' }; + } + + elements.spawnModalSubmit.disabled = true; + + try { + const response = await fetch('/api/spawn', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, cli, model, task }), + }); + + const result = await response.json(); + + if (response.ok && result.success) { + // Refresh spawned agents list + await fetchSpawnedAgents(); + + // Close modal after brief delay + setTimeout(() => { + closeSpawnModal(); + }, 500); + + return { success: true }; + } else { + throw new Error(result.error || 'Failed to spawn agent'); + } + } catch (err: any) { + return { success: false, error: err.message }; + } finally { + elements.spawnModalSubmit.disabled = false; + } +} + +/** + * Fetch list of spawned agents from API + */ +export async function fetchSpawnedAgents(): Promise { + try { + const response = await fetch('/api/spawned'); + const result = await response.json(); + + if (result.success && Array.isArray(result.agents)) { + spawnedAgents = result.agents.map((a: any) => a.name); + // Re-render agents to show spawned status + renderAgents(); + } + } catch (err) { + console.error('[UI] Failed to fetch spawned agents:', err); + } +} + +/** + * Check if an agent is spawned + */ +export function isSpawnedAgent(name: string): boolean { + return spawnedAgents.includes(name); +} diff --git a/src/dashboard/frontend/types.ts b/src/dashboard/frontend/types.ts index 0950ccecf..dd4ac76c0 100644 --- a/src/dashboard/frontend/types.ts +++ b/src/dashboard/frontend/types.ts @@ -93,14 +93,15 @@ export interface DOMElements { mentionAutocomplete: HTMLElement; mentionAutocompleteList: HTMLElement; // Spawn modal elements - spawnBtn: HTMLButtonElement; + spawnAgentBtn: HTMLButtonElement; spawnModalOverlay: HTMLElement; spawnModalClose: HTMLButtonElement; - spawnNameInput: HTMLInputElement; - spawnCliInput: HTMLInputElement; - spawnTaskInput: HTMLTextAreaElement; - spawnSubmitBtn: HTMLButtonElement; - spawnStatus: HTMLElement; + spawnAgentName: HTMLInputElement; + spawnAgentCli: HTMLSelectElement; + spawnAgentModel: HTMLInputElement; + spawnAgentTask: HTMLTextAreaElement; + spawnModalCancel: HTMLButtonElement; + spawnModalSubmit: HTMLButtonElement; } export interface SpawnedAgent { diff --git a/src/dashboard/server.ts b/src/dashboard/server.ts index 9ecfb5e3a..45ac49789 100644 --- a/src/dashboard/server.ts +++ b/src/dashboard/server.ts @@ -63,7 +63,17 @@ interface AgentSummary { context?: string; } -export async function startDashboard(port: number, dataDir: string, teamDir: string, dbPath?: string): Promise { +export interface DashboardOptions { + port: number; + dataDir: string; + teamDir: string; + dbPath?: string; + enableSpawner?: boolean; + projectRoot?: string; +} + +export async function startDashboard(options: DashboardOptions): Promise { + const { port, dataDir, teamDir, dbPath, enableSpawner, projectRoot } = options; console.log('Starting dashboard...'); console.log('__dirname:', __dirname); const publicDir = path.join(__dirname, 'public'); diff --git a/src/dashboard/start.ts b/src/dashboard/start.ts index 6cf26a6a1..ec8449394 100644 --- a/src/dashboard/start.ts +++ b/src/dashboard/start.ts @@ -13,4 +13,9 @@ console.log(`Starting dashboard for project: ${paths.projectRoot}`); console.log(`Data dir: ${paths.dataDir}`); console.log(`Database: ${paths.dbPath}`); -startDashboard(port, paths.dataDir, paths.teamDir, paths.dbPath).catch(console.error); +startDashboard({ + port, + dataDir: paths.dataDir, + teamDir: paths.teamDir, + dbPath: paths.dbPath, +}).catch(console.error);