import React from 'react'; import { cn, getModifierLabel } from '@/lib/utils'; import { useUIStore } from '@/stores/useUIStore'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useAgentsStore } from '@/stores/useAgentsStore'; import { useCommandsStore } from '@/stores/useCommandsStore'; import { useMcpConfigStore } from '@/stores/useMcpConfigStore'; import { useSnippetsStore } from '@/stores/useSnippetsStore'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore'; import { useConfigStore } from '@/stores/useConfigStore'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; import { AgentsSidebar } from '@/components/sections/agents/AgentsSidebar'; import { AgentsPage } from '@/components/sections/agents/AgentsPage'; import { BehaviorPage } from '@/components/sections/behavior/BehaviorPage'; import { CommandsSidebar } from '@/components/sections/commands/CommandsSidebar'; import { CommandsPage } from '@/components/sections/commands/CommandsPage'; import { McpSidebar } from '@/components/sections/mcp/McpSidebar'; import { McpPage } from '@/components/sections/mcp/McpPage'; import { PluginsSidebar, PluginsPage } from '@/components/sections/plugins'; import { usePluginsStore } from '@/stores/usePluginsStore'; import { SkillsSidebar } from '@/components/sections/skills/SkillsSidebar'; import { SkillsPage } from '@/components/sections/skills/SkillsPage'; import { ProjectsSidebar } from '@/components/sections/projects/ProjectsSidebar'; import { ProjectsPage } from '@/components/sections/projects/ProjectsPage'; import { RemoteInstancesPage } from '@/components/sections/remote-instances/RemoteInstancesPage'; import { ProvidersSidebar } from '@/components/sections/providers/ProvidersSidebar'; import { ProvidersPage } from '@/components/sections/providers/ProvidersPage'; import { UsageSidebar } from '@/components/sections/usage/UsageSidebar'; import { UsagePage } from '@/components/sections/usage/UsagePage'; import { MagicPromptsSidebar } from '@/components/sections/magic-prompts/MagicPromptsSidebar'; import { MagicPromptsPage } from '@/components/sections/magic-prompts/MagicPromptsPage'; import { SnippetsSidebar } from '@/components/sections/snippets/SnippetsSidebar'; import { SnippetsPage } from '@/components/sections/snippets/SnippetsPage'; import { GitPage } from '@/components/sections/git-identities/GitPage'; import type { OpenChamberSection } from '@/components/sections/openchamber/types'; import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage'; import { AboutSettings } from '@/components/sections/openchamber/AboutSettings'; import { useDeviceInfo } from '@/lib/device'; import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop'; import { useI18n } from '@/lib/i18n'; import { Icon } from "@/components/icon/Icon"; import type { IconName } from "@/components/icon/icons"; import { McpIcon } from '@/components/icons/McpIcon'; import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; import { SETTINGS_PAGE_METADATA, getSettingsPageMeta, resolveSettingsSlug, type SettingsPageSlug, type SettingsRuntimeContext, type SettingsPageMeta, } from '@/lib/settings/metadata'; import { buildSettingsSearchResults, type SettingsSearchResult } from '@/lib/settings/search'; // Same constraints as main sidebar const SETTINGS_NAV_MIN_WIDTH = 176; const SETTINGS_NAV_MAX_WIDTH = 280; const SETTINGS_NAV_RESIZE_STEP = 8; const SETTINGS_DETAIL_HISTORY_KEY = '__openchamberSettingsDetail'; function clampSettingsNavWidth(width: number): number { return Math.min(SETTINGS_NAV_MAX_WIDTH, Math.max(SETTINGS_NAV_MIN_WIDTH, width)); } type MobileStage = 'nav' | 'page-sidebar' | 'page-content'; type SettingsDetailHistoryEntry = { page: SettingsPageSlug; stage: 'page-content'; }; interface SettingsViewProps { onClose?: () => void; /** Force mobile layout regardless of device detection */ forceMobile?: boolean; /** Rendered inside a window/dialog (skip traffic light padding) */ isWindowed?: boolean; /** Restrict top-level settings navigation to a specific product surface. */ visiblePageSlugs?: SettingsPageSlug[]; initialMobileStage?: MobileStage; } const pageOrder: SettingsPageSlug[] = [ 'appearance', 'chat', 'notifications', 'sessions', 'shortcuts', 'git', 'magic-prompts', 'snippets', 'projects', 'remote-instances', 'agents', 'behavior', 'commands', 'mcp', 'plugins', 'providers', 'usage', 'skills.installed', 'skills.catalog', 'voice', 'tunnel', 'about', ]; const SNIPPETS_SETTINGS_ICON = { icon: 'chat-thread' } as const; const ADD_PROVIDER_SETTINGS_ID = '__add_provider__'; function buildRuntimeContext(isDesktop: boolean): SettingsRuntimeContext { const isVSCode = isVSCodeRuntime(); const isWeb = !isDesktop && isWebRuntime(); return { isVSCode, isWeb, isDesktop }; } function isPageAvailable(page: SettingsPageMeta, ctx: SettingsRuntimeContext): boolean { if (!page.isAvailable) { return true; } return page.isAvailable(ctx); } function isObjectRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function nextUniqueName(baseName: string, existingNames: Iterable): string { const existing = new Set(existingNames); let name = baseName; let counter = 1; while (existing.has(name)) { name = `${baseName}-${counter}`; counter += 1; } return name; } function getSettingsDetailHistoryEntry(state: unknown): SettingsDetailHistoryEntry | null { if (!isObjectRecord(state)) { return null; } const detail = state[SETTINGS_DETAIL_HISTORY_KEY]; if (!isObjectRecord(detail)) { return null; } const page = detail.page; const stage = detail.stage; if (typeof page !== 'string' || stage !== 'page-content') { return null; } const resolvedPage = resolveSettingsSlug(page); return { page: resolvedPage, stage }; } function getCurrentHistoryState(): Record { if (typeof window === 'undefined' || !isObjectRecord(window.history.state)) { return {}; } return window.history.state; } // eslint-disable-next-line react-refresh/only-export-components export function getSettingsNavIcon(slug: SettingsPageSlug): IconName | null { switch (slug) { case 'projects': return 'folders'; case 'remote-instances': return 'server'; case 'appearance': return 'palette'; case 'chat': return 'chat-ai-3'; case 'magic-prompts': return 'ai-generate-2'; case 'snippets': return SNIPPETS_SETTINGS_ICON.icon; case 'notifications': return 'notification-3'; case 'shortcuts': return 'command'; case 'sessions': return 'chat-history'; case 'providers': return 'cloud'; case 'agents': return 'ai-agent'; case 'behavior': return 'brain'; case 'commands': return 'slash-commands-2'; case 'mcp': return null; case 'plugins': return 'code-box'; case 'skills.installed': return 'book-open'; case 'skills.catalog': return 'book'; case 'git': return 'git-branch'; case 'usage': return 'bar-chart-2'; case 'voice': return 'mic'; case 'tunnel': return 'global'; case 'about': return 'information'; case 'home': return null; default: return 'robot-2'; } } const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({ onOpen }) => { const { t } = useI18n(); return (

{t('settings.view.home.title')}

{t('settings.view.home.description')}

); }; export const SettingsView: React.FC = ({ onClose, forceMobile, isWindowed, visiblePageSlugs, initialMobileStage = 'nav' }) => { const { t } = useI18n(); const deviceInfo = useDeviceInfo(); const isMobile = forceMobile ?? deviceInfo.isMobile; const settingsPageRaw = useUIStore((state) => state.settingsPage); const isSettingsDialogOpen = useUIStore((state) => state.isSettingsDialogOpen); const setSettingsPage = useUIStore((state) => state.setSettingsPage); const settingsSlug = resolveSettingsSlug(settingsPageRaw); const [mobileStage, setMobileStage] = React.useState(initialMobileStage); const autoNavSlugRef = React.useRef(null); const [navWidth, setNavWidth] = React.useState(216); const [settingsSearchQuery, setSettingsSearchQuery] = React.useState(''); const [pendingSearchItemId, setPendingSearchItemId] = React.useState(null); const [activeSearchResultIndex, setActiveSearchResultIndex] = React.useState(0); const [hasManuallyResized, setHasManuallyResized] = React.useState(false); const [isResizing, setIsResizing] = React.useState(false); const startXRef = React.useRef(0); const startWidthRef = React.useRef(navWidth); const containerRef = React.useRef(null); const searchResultRefs = React.useRef<(HTMLButtonElement | null)[]>([]); const activeSearchResultIndexRef = React.useRef(0); const keyboardSearchNavigationRef = React.useRef(false); const isDesktopApp = React.useMemo(() => { return isDesktopShell(); }, []); const isDesktopLocalOrigin = React.useMemo(() => { return isDesktopShell() && isDesktopLocalOriginActive(); }, []); // keep platform check available for future window chrome tweaks const runtimeCtx = React.useMemo(() => buildRuntimeContext(isDesktopApp), [isDesktopApp]); const visiblePages = React.useMemo(() => { const allowedPages = visiblePageSlugs ? new Set(visiblePageSlugs) : null; return SETTINGS_PAGE_METADATA .filter((page) => page.slug !== 'home') .filter((page) => !allowedPages || allowedPages.has(page.slug)) .filter((page) => isPageAvailable(page, runtimeCtx)) .filter((page) => !(runtimeCtx.isVSCode && page.slug === 'projects')) .filter((page) => !(isMobile && page.slug === 'shortcuts')); }, [runtimeCtx, isMobile, visiblePageSlugs]); const sortedFilteredPages = React.useMemo(() => { const rank = new Map(pageOrder.map((s, i) => [s, i])); return visiblePages .slice() .sort((a, b) => (rank.get(a.slug) ?? 999) - (rank.get(b.slug) ?? 999)); }, [visiblePages]); const activeProjectId = useProjectsStore((state) => state.activeProjectId); React.useEffect(() => { if (typeof window === 'undefined') return; const handleResize = () => { if (!hasManuallyResized) { const proportionalWidth = clampSettingsNavWidth(Math.floor(window.innerWidth * 0.12)); setNavWidth(proportionalWidth); } }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, [hasManuallyResized]); React.useEffect(() => { if (!isResizing) return; const handlePointerMove = (event: PointerEvent) => { const delta = event.clientX - startXRef.current; const nextWidth = clampSettingsNavWidth(startWidthRef.current + delta); setNavWidth(nextWidth); setHasManuallyResized(true); }; const handlePointerUp = () => setIsResizing(false); window.addEventListener('pointermove', handlePointerMove); window.addEventListener('pointerup', handlePointerUp, { once: true }); return () => { window.removeEventListener('pointermove', handlePointerMove); window.removeEventListener('pointerup', handlePointerUp); }; }, [isResizing]); const handlePointerDown = (event: React.PointerEvent) => { setIsResizing(true); startXRef.current = event.clientX; startWidthRef.current = navWidth; event.preventDefault(); }; const handleResizeKeyDown = (event: React.KeyboardEvent) => { const step = event.shiftKey ? SETTINGS_NAV_RESIZE_STEP * 4 : SETTINGS_NAV_RESIZE_STEP; let nextWidth: number; switch (event.key) { case 'ArrowLeft': nextWidth = navWidth - step; break; case 'ArrowRight': nextWidth = navWidth + step; break; case 'Home': nextWidth = SETTINGS_NAV_MIN_WIDTH; break; case 'End': nextWidth = SETTINGS_NAV_MAX_WIDTH; break; default: return; } event.preventDefault(); setNavWidth(clampSettingsNavWidth(nextWidth)); setHasManuallyResized(true); }; // Load stores when project changes or when a page becomes active. React.useEffect(() => { if (!isSettingsDialogOpen && !runtimeCtx.isVSCode && !isWindowed) { return; } if (settingsSlug === 'agents') { void useAgentsStore.getState().loadAgents(); return; } if (settingsSlug === 'commands') { void useCommandsStore.getState().loadCommands(); return; } if (settingsSlug === 'mcp') { void useMcpConfigStore.getState().loadMcpConfigs(); return; } if (settingsSlug === 'plugins') { void usePluginsStore.getState().loadPlugins(); return; } if (settingsSlug === 'skills.installed' || settingsSlug === 'skills.catalog') { void useSkillsStore.getState().loadSkills(); void useSkillsCatalogStore.getState().loadCatalog(); } if (settingsSlug === 'snippets') { void useSnippetsStore.getState().loadSnippets(); } }, [activeProjectId, isSettingsDialogOpen, isWindowed, runtimeCtx.isVSCode, settingsSlug]); const openPage = React.useCallback((slug: SettingsPageSlug) => { setSettingsPage(slug); autoNavSlugRef.current = slug; if (!isMobile) { return; } const def = getSettingsPageMeta(slug); if (!def || def.slug === 'home') { setMobileStage('nav'); return; } setMobileStage(def.kind === 'split' ? 'page-sidebar' : 'page-content'); }, [isMobile, setSettingsPage]); const activePageMeta = React.useMemo(() => { return getSettingsPageMeta(settingsSlug); }, [settingsSlug]); // Nav is always open (collapsed state removed) const openChamberSectionBySlug: Partial> = React.useMemo(() => ({ appearance: 'visual', chat: 'chat', shortcuts: 'shortcuts', sessions: 'sessions', notifications: 'notifications', voice: 'voice', tunnel: 'tunnel', }), []); const getPageTitle = React.useCallback((slug: SettingsPageSlug): string => { switch (slug) { case 'projects': return t('settings.page.projects.title'); case 'remote-instances': return t('settings.page.remoteInstances.title'); case 'providers': return t('settings.page.providers.title'); case 'usage': return t('settings.page.usage.title'); case 'agents': return t('settings.page.agents.title'); case 'behavior': return t('settings.page.behavior.title'); case 'commands': return t('settings.page.commands.title'); case 'mcp': return t('settings.page.mcp.title'); case 'plugins': return t('settings.page.plugins.title'); case 'skills.installed': return t('settings.page.skills.title'); case 'skills.catalog': return t('settings.page.skillsCatalog.title'); case 'git': return t('settings.page.git.title'); case 'appearance': return t('settings.page.appearance.title'); case 'chat': return t('settings.page.chat.title'); case 'shortcuts': return t('settings.page.shortcuts.title'); case 'sessions': return t('settings.page.sessions.title'); case 'magic-prompts': return t('settings.page.magicPrompts.title'); case 'snippets': return t('settings.page.snippets.title'); case 'notifications': return t('settings.page.notifications.title'); case 'voice': return t('settings.page.voice.title'); case 'tunnel': return t('settings.page.tunnel.title'); case 'about': return t('settings.page.about.title'); case 'home': default: return t('settings.view.home.title'); } }, [t]); const settingsSearchResults = React.useMemo(() => { return buildSettingsSearchResults({ query: settingsSearchQuery, runtimeCtx: { ...runtimeCtx, isMobile, isDesktopLocalOrigin }, visiblePageSlugs, t, getPageTitle, }); }, [getPageTitle, isDesktopLocalOrigin, isMobile, runtimeCtx, settingsSearchQuery, t, visiblePageSlugs]); const prepareSettingsSearchTarget = React.useCallback((result: SettingsSearchResult): string => { if (result.id.startsWith('agents.')) { const store = useAgentsStore.getState(); const name = nextUniqueName('new-agent', store.agents.map((agent) => agent.name)); store.setAgentDraft({ name, scope: 'user' }); store.setSelectedAgent(name); return result.id === 'agents.create' ? 'agents.name' : result.id; } if (result.id.startsWith('commands.')) { const store = useCommandsStore.getState(); const name = nextUniqueName('new-command', store.commands.map((command) => command.name)); store.setCommandDraft({ name, scope: 'user' }); store.setSelectedCommand(name); return result.id === 'commands.create' ? 'commands.name' : result.id; } if (result.id.startsWith('mcp.')) { const store = useMcpConfigStore.getState(); const name = nextUniqueName('new-mcp-server', store.mcpServers.map((server) => server.name)); store.setMcpDraft({ name, scope: 'user', type: 'local', command: [], url: '', environment: [], headers: [], oauthEnabled: true, oauthClientId: '', oauthClientSecret: '', oauthScope: '', oauthRedirectUri: '', timeout: '', enabled: true, }); store.setSelectedMcp(name); return result.id === 'mcp.create' ? 'mcp.server' : result.id; } if (result.id.startsWith('snippets.')) { const store = useSnippetsStore.getState(); const name = nextUniqueName('new-snippet', store.snippets.map((snippet) => snippet.name)); store.setSnippetDraft({ name, scope: 'global' }); store.setSelectedSnippet(name); return result.id === 'snippets.create' ? 'snippets.content' : result.id; } if (result.id.startsWith('skills.')) { const store = useSkillsStore.getState(); const name = nextUniqueName('new-skill', store.skills.map((skill) => skill.name)); store.setSkillDraft({ name, scope: 'user', source: 'opencode', description: '', instructions: '' }); store.setSelectedSkill(name); return result.id === 'skills.create' ? 'skills.basic-information' : result.id; } if (result.id === 'providers.connect') { useConfigStore.getState().setSelectedProvider(ADD_PROVIDER_SETTINGS_ID); } if (result.id === 'plugins.create') { return 'plugins.spec'; } return result.id; }, []); const groupedSettingsSearchResults = React.useMemo(() => { const groups: Array<{ page: SettingsPageSlug; pageTitle: string; results: SettingsSearchResult[] }> = []; const groupByPage = new Map(); for (const result of settingsSearchResults) { let group = groupByPage.get(result.page); if (!group) { group = { page: result.page, pageTitle: result.pageTitle, results: [] }; groupByPage.set(result.page, group); groups.push(group); } group.results.push(result); } return groups; }, [settingsSearchResults]); React.useEffect(() => { setActiveSearchResultIndex(0); activeSearchResultIndexRef.current = 0; keyboardSearchNavigationRef.current = false; }, [settingsSearchQuery]); React.useEffect(() => { activeSearchResultIndexRef.current = activeSearchResultIndex; }, [activeSearchResultIndex]); React.useEffect(() => { searchResultRefs.current[activeSearchResultIndex]?.scrollIntoView({ block: 'nearest' }); }, [activeSearchResultIndex]); React.useEffect(() => { if (activeSearchResultIndex >= settingsSearchResults.length) { setActiveSearchResultIndex(Math.max(0, settingsSearchResults.length - 1)); } searchResultRefs.current.length = settingsSearchResults.length; }, [activeSearchResultIndex, settingsSearchResults.length]); const openSearchResult = React.useCallback((result: SettingsSearchResult) => { const targetId = prepareSettingsSearchTarget(result); setPendingSearchItemId(targetId); openPage(result.page); if (isMobile) { setMobileStage('page-content'); } if (result.id === 'plugins.create' && typeof window !== 'undefined') { window.setTimeout(() => { window.dispatchEvent(new CustomEvent('openchamber:settings-open-plugin-add')); }, 50); } }, [isMobile, openPage, prepareSettingsSearchTarget]); const handleSettingsSearchKeyDown = React.useCallback((event: React.KeyboardEvent) => { if (!settingsSearchQuery.trim()) { return; } if (event.key === 'Escape') { event.preventDefault(); setSettingsSearchQuery(''); return; } if (settingsSearchResults.length === 0) { return; } if (event.key === 'ArrowDown') { event.preventDefault(); keyboardSearchNavigationRef.current = true; setActiveSearchResultIndex((current) => (current + 1) % settingsSearchResults.length); return; } if (event.key === 'ArrowUp') { event.preventDefault(); keyboardSearchNavigationRef.current = true; setActiveSearchResultIndex((current) => (current - 1 + settingsSearchResults.length) % settingsSearchResults.length); return; } if (event.key === 'Enter') { event.preventDefault(); const safeIndex = ((activeSearchResultIndexRef.current % settingsSearchResults.length) + settingsSearchResults.length) % settingsSearchResults.length; const result = settingsSearchResults[safeIndex] ?? settingsSearchResults[0]; if (result) { openSearchResult(result); } } }, [openSearchResult, settingsSearchQuery, settingsSearchResults]); React.useEffect(() => { const targetId = pendingSearchItemId; if (!targetId) { return; } let cancelled = false; const frame = window.requestAnimationFrame(() => { if (cancelled) { return; } const escapedId = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(targetId) : targetId.replace(/[^a-zA-Z0-9_-]/g, '\\$&'); const target = containerRef.current?.querySelector(`[data-settings-item="${escapedId}"]`); if (!target) { return; } setPendingSearchItemId(null); target.scrollIntoView({ block: 'center', behavior: 'smooth' }); target.setAttribute('data-settings-search-highlight', 'true'); window.setTimeout(() => { target.removeAttribute('data-settings-search-highlight'); }, 1600); }); return () => { cancelled = true; window.cancelAnimationFrame(frame); }; }, [pendingSearchItemId, settingsSlug]); const renderUnavailable = React.useCallback(() => { return (
{t('settings.view.unavailable.title')}

{t('settings.view.unavailable.description')}

); }, [t]); const renderPageSidebar = React.useCallback((slug: SettingsPageSlug, opts: { onItemSelect?: () => void }) => { switch (slug) { case 'projects': return ; case 'agents': return ; case 'commands': return ; case 'mcp': return ; case 'plugins': return ; case 'skills.installed': return ; case 'providers': return ; case 'usage': return ; case 'magic-prompts': return ; case 'snippets': return ; default: return null; } }, []); const renderPageContent = React.useCallback((slug: SettingsPageSlug) => { const meta = getSettingsPageMeta(slug); if (meta && !isPageAvailable(meta, runtimeCtx)) { return renderUnavailable(); } switch (slug) { case 'home': return ; case 'projects': return ; case 'remote-instances': return ; case 'agents': return ; case 'behavior': return ; case 'commands': return ; case 'mcp': return ; case 'plugins': return ; case 'skills.installed': return ; case 'skills.catalog': return ; case 'providers': return ; case 'usage': return ; case 'about': return
; case 'magic-prompts': return ; case 'snippets': return ; case 'git': return ; case 'appearance': case 'chat': case 'shortcuts': case 'sessions': case 'notifications': case 'voice': case 'tunnel': { const section = openChamberSectionBySlug[slug] ?? 'visual'; return ; } default: return ; } }, [openChamberSectionBySlug, openPage, renderUnavailable, runtimeCtx]); // Mobile: if opened via deep-link / palette to a non-home page, jump into it once. React.useEffect(() => { if (!isMobile) { return; } if (mobileStage !== 'nav') { return; } if (settingsSlug === 'home') { return; } if (autoNavSlugRef.current === settingsSlug) { return; } const def = getSettingsPageMeta(settingsSlug); if (!def || def.slug === 'home') { return; } autoNavSlugRef.current = settingsSlug; setMobileStage(def.kind === 'split' ? 'page-sidebar' : 'page-content'); }, [isMobile, mobileStage, settingsSlug]); const showBackButton = isMobile && mobileStage !== 'nav'; const backButtonTargetsPageSidebar = isMobile && mobileStage === 'page-content' && settingsSlug === 'skills.installed'; const showOpenPageSidebarButton = mobileStage === 'page-content' && activePageMeta?.kind === 'split' && !backButtonTargetsPageSidebar; const mobileBackButtonLabel = backButtonTargetsPageSidebar ? t('settings.view.actions.back') : showBackButton ? t('settings.view.actions.backToSettings') : t('settings.view.actions.closeSettings'); const shortcutKey = getModifierLabel(); const pushMobileSplitDetailHistory = React.useCallback((slug: SettingsPageSlug) => { if (typeof window === 'undefined' || runtimeCtx.isVSCode) { return; } const currentDetail = getSettingsDetailHistoryEntry(window.history.state); if (currentDetail?.page === slug && currentDetail.stage === 'page-content') { return; } window.history.pushState( { ...getCurrentHistoryState(), [SETTINGS_DETAIL_HISTORY_KEY]: { page: slug, stage: 'page-content' }, }, '', window.location.href, ); }, [runtimeCtx.isVSCode]); const handleMobilePageSidebarItemSelect = React.useCallback(() => { setMobileStage('page-content'); if (settingsSlug === 'skills.installed') { pushMobileSplitDetailHistory(settingsSlug); } }, [pushMobileSplitDetailHistory, settingsSlug]); const handleBack = React.useCallback(() => { if (backButtonTargetsPageSidebar) { const currentDetail = typeof window !== 'undefined' ? getSettingsDetailHistoryEntry(window.history.state) : null; if (currentDetail?.page === settingsSlug && !runtimeCtx.isVSCode) { window.history.back(); return; } setMobileStage('page-sidebar'); return; } setMobileStage('nav'); }, [backButtonTargetsPageSidebar, runtimeCtx.isVSCode, settingsSlug]); React.useEffect(() => { if (!isMobile || runtimeCtx.isVSCode) { return; } const handlePopState = (event: PopStateEvent) => { if (settingsSlug !== 'skills.installed') { return; } const detail = getSettingsDetailHistoryEntry(event.state); if (detail?.page === 'skills.installed') { setMobileStage('page-content'); return; } setMobileStage((stage) => stage === 'page-content' ? 'page-sidebar' : stage); }; window.addEventListener('popstate', handlePopState); return () => { window.removeEventListener('popstate', handlePopState); }; }, [isMobile, runtimeCtx.isVSCode, settingsSlug]); const handleOpenPageSidebar = React.useCallback(() => { setMobileStage('page-sidebar'); }, []); const renderSettingsNav = () => { const hasSearchQuery = settingsSearchQuery.trim().length > 0; return (
setSettingsSearchQuery(event.target.value)} onKeyDown={handleSettingsSearchKeyDown} placeholder={t('settings.view.search.placeholder')} aria-label={t('settings.view.search.aria')} className="typography-ui min-w-0 flex-1 bg-transparent text-foreground outline-none placeholder:text-muted-foreground/70" /> {hasSearchQuery && ( )}
{/* Scrollable nav items */}
{hasSearchQuery ? ( settingsSearchResults.length > 0 ? (() => { let resultIndex = 0; return groupedSettingsSearchResults.map((group) => (
{group.pageTitle}
{group.results.map((result) => { const currentIndex = resultIndex; resultIndex += 1; const active = currentIndex === activeSearchResultIndex; const hasDescription = Boolean(result.description); return ( ); })}
)); })() : (
{t('settings.view.search.noResults')}
) ) : sortedFilteredPages.map((page) => { const selected = settingsSlug === page.slug; const iconName = getSettingsNavIcon(page.slug); if (!iconName && page.slug !== 'mcp') return null; return ( ); })}
{/* Footer */}
{!runtimeCtx.isVSCode && ( {t('settings.view.actions.reloadOpenCodeTooltip')} )}
); }; const renderMobileStage = () => { if (mobileStage === 'nav') { return (
{renderSettingsNav()}
); } if (!activePageMeta) { return
; } if (mobileStage === 'page-sidebar') { if (activePageMeta.kind !== 'split') { // No sidebar available; fall back to direct content. const fallback = renderPageContent(settingsSlug); return (
{fallback}
); } return (
{renderPageSidebar(settingsSlug, { onItemSelect: handleMobilePageSidebarItemSelect })}
); } // page-content const content = renderPageContent(settingsSlug); return (
{content}
); }; const renderDesktopContent = () => { if (!activePageMeta || settingsSlug === 'home') { return ; } if (activePageMeta.kind === 'split') { return (
{renderPageSidebar(settingsSlug, {})}
{renderPageContent(settingsSlug)}
); } return (
{renderPageContent(settingsSlug)}
); }; return (
{isMobile ? (
{(showBackButton || onClose) ? ( ) : null}
{mobileStage === 'nav' ? t('settings.view.home.title') : (activePageMeta ? getPageTitle(activePageMeta.slug) : t('settings.view.home.title'))}
{showOpenPageSidebarButton && ( )} {onClose && ( )}
) : ( <> {showBackButton && (
)} {onClose && (
)} )}
{isMobile ? ( renderMobileStage() ) : ( <>
{renderSettingsNav()}
{renderDesktopContent()}
)}
); };