import React from 'react'; import { Button } from '@/components/ui/button'; import { Icon } from '@/components/icon/Icon'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; import { useI18n } from '@/lib/i18n'; import { fetchDevServers, mergeDevServerCandidates, type DevServerDiscovery } from '@/lib/browser/devServers'; import { clearAnnouncedDevServers, useAnnouncedDevServers } from '@/lib/browser/announcedServers'; import { browserUrlLabel, isLoopbackUrl } from '@/lib/browser/url'; import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; /** * What the panel shows before anything is loaded. * * Rather than an inert placeholder, this lists the servers actually running, * which is almost always what the user came here to open. Discovery failure is * stated plainly instead of being rendered as "nothing is running" — the two * mean very different things to someone whose dev server is definitely up. */ /** The base path a server is served under, or '' when it sits at the root. */ const pathLabel = (url: string): string => { try { const path = new URL(url).pathname; return path === '/' ? '' : path; } catch { return ''; } }; /** Re-checked while the panel is open: a project's servers appear seconds apart. */ const REFRESH_INTERVAL_MS = 2_000; /** * True when the listed servers are on another machine and this client has no * way to reach them. The desktop shell tunnels a local port for exactly this * case; a browser tab has no equivalent, and its `localhost` is its own. */ const isUnreachableFromHere = (): boolean => { if (typeof window === 'undefined') return false; if (window.__OPENCHAMBER_ELECTRON__) return false; const baseUrl = getRuntimeApiBaseUrl(); if (!baseUrl) return false; try { return !isLoopbackUrl(new URL(baseUrl, window.location.href).toString()); } catch { return false; } }; export const BrowserEmptyState: React.FC<{ onOpen: (url: string) => void; directory?: string; }> = ({ onOpen, directory = '' }) => { const { t } = useI18n(); const [discovery, setDiscovery] = React.useState({ kind: 'loading' }); const announced = useAnnouncedDevServers(directory); const [remoteOnly] = React.useState(isUnreachableFromHere); React.useEffect(() => { let active = true; let timer: ReturnType | null = null; const controller = new AbortController(); const poll = () => { void fetchDevServers(controller.signal).then((result) => { if (!active) return; setDiscovery(result); // One look is a snapshot of whichever servers happened to be up first. timer = setTimeout(poll, REFRESH_INTERVAL_MS); }); }; poll(); return () => { active = false; if (timer) clearTimeout(timer); controller.abort(); }; }, []); const candidates = React.useMemo(() => mergeDevServerCandidates({ announced, discovered: discovery.kind === 'ready' ? discovery.servers : null, }), [announced, discovery]); return ( // The whole panel must not scroll: a centred column that overflows clips its // own top, and no amount of scrolling reaches it. Only the list of servers // scrolls, and it shrinks to whatever room is left before it does.
{t('contextPanel.browser.empty')} {t('contextPanel.browser.emptyHint')}
{candidates.length > 0 ? (
{announced.length > 0 ? t('contextPanel.browser.devServers.justStarted') : t('contextPanel.browser.devServers.title')} {remoteOnly ? ( {t('contextPanel.browser.devServers.remoteOnly')} ) : null}
{candidates.map((candidate) => ( ))}
) : null} {candidates.length === 0 && discovery.kind === 'unavailable' ? ( {t('contextPanel.browser.devServers.unavailable')} ) : null}
); };