import { useEffect, useState } from 'react'; import type { BonsaiShellProps } from '../../lib/contracts'; import { normalizeThemeMode, resolveThemeMode, THEME_STORAGE_KEY, type ThemeMode } from '../theme'; import { ChatSurface } from './ChatSurface'; import { ConversationList } from './ConversationList'; import { Header, type ChatWidth } from './Header'; import { InspectorPanel } from './InspectorPanel'; import { ModelRail } from './ModelRail'; import { SettingsSheet } from './SettingsSheet'; const BROWSER_THEME_COLORS = { light: '#f3f6f2', dark: '#111511', } as const; export function BonsaiShell(props: BonsaiShellProps) { const [leftSidebarOpen, setLeftSidebarOpen] = useState(true); const [rightSidebarOpen, setRightSidebarOpen] = useState(true); const [chatWidth, setChatWidth] = useState('centered'); const [themeMode, setThemeMode] = useState(() => { try { return normalizeThemeMode(globalThis.localStorage?.getItem(THEME_STORAGE_KEY)); } catch { return 'system'; } }); const [systemDark, setSystemDark] = useState(() => globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false); const activeModel = props.models.find((model) => model.id === props.activeModelId); const busy = props.streamState === 'loading' || props.streamState === 'streaming'; const resolvedTheme = resolveThemeMode(themeMode, systemDark); useEffect(() => { const media = globalThis.matchMedia?.('(prefers-color-scheme: dark)'); if (!media) return; const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches); setSystemDark(media.matches); media.addEventListener('change', handleChange); return () => media.removeEventListener('change', handleChange); }, []); useEffect(() => { try { globalThis.localStorage?.setItem(THEME_STORAGE_KEY, themeMode); } catch { // Theme still works for the current tab when storage is unavailable. } }, [themeMode]); useEffect(() => { const themeColor = BROWSER_THEME_COLORS[resolvedTheme]; const meta = document.querySelector('meta[name="theme-color"]'); const previousMetaColor = meta?.content; const previousRootBackground = document.documentElement.style.backgroundColor; const previousBodyBackground = document.body.style.backgroundColor; const previousColorScheme = document.documentElement.style.colorScheme; if (meta) meta.content = themeColor; document.documentElement.style.backgroundColor = themeColor; document.documentElement.style.colorScheme = resolvedTheme; document.body.style.backgroundColor = themeColor; return () => { if (meta && previousMetaColor !== undefined) meta.content = previousMetaColor; document.documentElement.style.backgroundColor = previousRootBackground; document.documentElement.style.colorScheme = previousColorScheme; document.body.style.backgroundColor = previousBodyBackground; }; }, [resolvedTheme]); if (!activeModel) { throw new Error(`Active model ${props.activeModelId} is missing from the model catalog`); } return (
setLeftSidebarOpen((open) => !open)} onToggleRightSidebar={() => setRightSidebarOpen((open) => !open)} onToggleChatWidth={() => setChatWidth((width) => width === 'centered' ? 'wide' : 'centered')} />
); }