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') // 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 = `${height}px` return } } } }) } // 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 => { try { const url = new URL(iframe.src) const port = parseInt(url.port, 10) const toolName = iframe.dataset.toolName const appName = iframe.dataset.appName if (port && toolName) { const cacheKey = appName ? `${toolName}:${appName}` : toolName iframes.set(cacheKey, { iframe, port }) } } catch { // Invalid URL, skip } }) } container.style.cssText = ` display: none; position: fixed; background: ${theme('colors-bg')}; overflow: auto; ` } // Build URL with params using TOES_URL base function buildToolUrl(port: number, params: Record): string { const base = new URL(window.location.origin) base.port = String(port) const searchParams = new URLSearchParams(params) const query = searchParams.toString() return query ? `${base.origin}?${query}` : base.origin } // 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(':') } // Reset a tool iframe to its home page export function resetToolIframe(toolName: string, selectedApp: string | null) { const params: Record = {} if (selectedApp) params.app = selectedApp const cacheKey = buildCacheKey(toolName, params) const cached = iframes.get(cacheKey) if (cached) { // For code app, include empty file param to clear its localStorage const urlParams = toolName === 'code' ? { ...params, file: '' } : params cached.iframe.src = buildToolUrl(cached.port, urlParams) } } // 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 = `calc(100vh - ${rect.top}px)` } 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 }