import { theme } from './themes' // Iframe cache - these never get recreated once loaded // Use a global to survive hot reloads const iframes: Map = (window as any).__toolIframes ??= new Map() // Track current state to avoid unnecessary DOM updates // Also stored on window to survive hot reloads let currentTool: string | null = (window as any).__currentTool ?? null // Get the stable container (outside Hono-managed DOM) const getContainer = () => document.getElementById('tool-iframes') // Callback for tool navigation requests let onNavigateTool: ((tool: string, params: Record) => void) | null = null export function setNavigateToolHandler(handler: (tool: string, params: Record) => void) { onNavigateTool = handler } // Listen for messages from iframes function setupMessageListener() { if ((window as any).__toolIframeMessageListener) return ; (window as any).__toolIframeMessageListener = true window.addEventListener('message', (event) => { const { type } = event.data || {} if (type === 'resize-iframe') { const height = event.data.height if (!height || typeof height !== 'number') return // Find which iframe sent this message for (const [, cached] of iframes) { if (cached.iframe.contentWindow === event.source) { cached.contentHeight = height cached.iframe.style.height = `100vh` return } } } if (type === 'navigate-tool') { const { tool, params } = event.data if (tool && onNavigateTool) { onNavigateTool(tool, params || {}) } } }) } // Initialize the container styles export function initToolIframes() { const container = getContainer() if (!container) return setupMessageListener() // Restore iframe cache from DOM if module was hot-reloaded if (iframes.size === 0) { const existingIframes = container.querySelectorAll('iframe') existingIframes.forEach(iframe => { const match = iframe.src.match(/localhost:(\d+)/) if (match && match[1]) { const port = parseInt(match[1], 10) const toolName = iframe.dataset.toolName const appName = iframe.dataset.appName if (toolName) { const cacheKey = appName ? `${toolName}:${appName}` : toolName iframes.set(cacheKey, { iframe, port }) } } }) } container.style.cssText = ` display: none; position: fixed; background: ${theme('colors-bg')}; overflow: auto; ` } // Build URL with params function buildToolUrl(port: number, params: Record): string { const searchParams = new URLSearchParams(params) const query = searchParams.toString() return query ? `http://localhost:${port}?${query}` : `http://localhost:${port}` } // Build cache key from tool name and params function buildCacheKey(toolName: string, params: Record): string { const parts = [toolName] const sortedKeys = Object.keys(params).sort() for (const key of sortedKeys) { parts.push(`${key}=${params[key]}`) } return parts.join(':') } // Navigate to a tool with specific params (called from postMessage handler) export function navigateToTool( toolName: string, params: Record, tools: Array<{ name: string; port?: number; state: string }>, onSelectTab: (tab: string) => void ) { const tool = tools.find(t => t.name === toolName) if (!tool || tool.state !== 'running' || !tool.port) return const container = getContainer() if (!container) return const cacheKey = buildCacheKey(toolName, params) // Create iframe if needed let cached = iframes.get(cacheKey) if (!cached || cached.port !== tool.port) { const iframe = document.createElement('iframe') iframe.src = buildToolUrl(tool.port, params) iframe.dataset.toolName = toolName iframe.dataset.appName = params.app ?? '' iframe.style.cssText = `width: 100%; border: none;` cached = { iframe, port: tool.port } iframes.set(cacheKey, cached) container.appendChild(iframe) } // Switch to that tab onSelectTab(toolName) // Force show this specific iframe currentTool = cacheKey ;(window as any).__currentTool = cacheKey for (const [key, { iframe }] of iframes) { iframe.style.display = key === cacheKey ? 'block' : 'none' } } // Update which iframe is visible based on selected tab and tool state export function updateToolIframes( selectedTab: string, tools: Array<{ name: string; port?: number; state: string }>, selectedApp: string | null ) { const container = getContainer() if (!container) return // Find the selected tool const selectedTool = tools.find(t => t.name === selectedTab) const showIframe = selectedTool?.state === 'running' && selectedTool?.port if (!showIframe) { if (currentTool !== null) { container.style.display = 'none' currentTool = null ;(window as any).__currentTool = null } return } const tool = selectedTool! // Build params and cache key const params: Record = {} if (selectedApp) params.app = selectedApp const cacheKey = buildCacheKey(tool.name, params) // Skip if nothing changed if (currentTool === cacheKey) { const tabContent = document.querySelector('[data-tool-target]') if (tabContent) { const rect = tabContent.getBoundingClientRect() container.style.top = `${rect.top}px` container.style.left = `${rect.left}px` container.style.width = `${rect.width}px` container.style.height = '100vh' } return } // Position container over the tab content area const tabContent = document.querySelector('[data-tool-target]') if (!tabContent) { container.style.display = 'none' currentTool = null return } const rect = tabContent.getBoundingClientRect() container.style.cssText = ` display: block; position: fixed; top: ${rect.top}px; left: ${rect.left}px; width: ${rect.width}px; height: calc(100vh - ${rect.top}px); background: ${theme('colors-bg')}; z-index: 100; overflow: auto; ` // Get or create the iframe let cached = iframes.get(cacheKey) if (!cached || cached.port !== tool.port) { const iframe = document.createElement('iframe') iframe.src = buildToolUrl(tool.port!, params) iframe.dataset.toolName = tool.name iframe.dataset.appName = selectedApp ?? '' iframe.style.cssText = `width: 100%; border: none;` cached = { iframe, port: tool.port! } iframes.set(cacheKey, cached) container.appendChild(iframe) } // Show only the selected iframe for (const [key, { iframe }] of iframes) { const shouldShow = key === cacheKey if (shouldShow && iframe.parentElement !== container) { container.appendChild(iframe) } iframe.style.display = shouldShow ? 'block' : 'none' } currentTool = cacheKey ;(window as any).__currentTool = cacheKey }