Files
openchamber/packages/ui/src/components/views/SettingsView.tsx
T
Bohdan Triapitsyn 03cec8d507 fix: restore reliable update flow across sidebar and mobile
Show Update button in sidebar only when an update is available
Support update button on mobile sidebar and remove duplicate mobile Settings entry
Force desktop Tauri recheck and preflight before download to avoid missing pending updates
2026-03-20 02:04:33 +02:00

771 lines
28 KiB
TypeScript

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 { useSkillsStore } from '@/stores/useSkillsStore';
import { useSkillsCatalogStore } from '@/stores/useSkillsCatalogStore';
import {
RiAiAgentLine,
RiArrowLeftSLine,
RiBarChart2Line,
RiBookLine,
RiBookOpenLine,
RiChatAi3Line,
RiChatHistoryLine,
RiCloseLine,
RiCommandLine,
RiCloudLine,
RiFoldersLine,
RiGitBranchLine,
RiGlobalLine,
RiMicLine,
RiNotification3Line,
RiPaletteLine,
RiListUnordered,
RiRobot2Line,
RiRestartLine,
RiServerLine,
RiSlashCommands2,
} from '@remixicon/react';
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 { 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 { 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 { RemoteInstancesSidebar } from '@/components/sections/remote-instances/RemoteInstancesSidebar';
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 { GitPage } from '@/components/sections/git-identities/GitPage';
import type { OpenChamberSection } from '@/components/sections/openchamber/types';
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
import { McpIcon } from '@/components/icons/McpIcon';
import { useDeviceInfo } from '@/lib/device';
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
import {
SETTINGS_PAGE_METADATA,
getSettingsPageMeta,
resolveSettingsSlug,
type SettingsPageSlug,
type SettingsRuntimeContext,
type SettingsPageMeta,
} from '@/lib/settings/metadata';
// Same constraints as main sidebar
const SETTINGS_NAV_MIN_WIDTH = 176;
const SETTINGS_NAV_MAX_WIDTH = 280;
const SETTINGS_NAV_RAIL_WIDTH = 48;
type MobileStage = 'nav' | 'page-sidebar' | '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;
}
const pageOrder: SettingsPageSlug[] = [
'appearance',
'chat',
'notifications',
'sessions',
'shortcuts',
'git',
'projects',
'remote-instances',
'agents',
'commands',
'mcp',
'providers',
'usage',
'skills.installed',
'skills.catalog',
'voice',
'tunnel',
];
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 getSettingsNavIcon(slug: SettingsPageSlug): React.ComponentType<{ className?: string }> | null {
switch (slug) {
case 'projects':
return RiFoldersLine;
case 'remote-instances':
return RiServerLine;
case 'appearance':
return RiPaletteLine;
case 'chat':
return RiChatAi3Line;
case 'notifications':
return RiNotification3Line;
case 'shortcuts':
return RiCommandLine;
case 'sessions':
return RiChatHistoryLine;
case 'providers':
return RiCloudLine;
case 'agents':
return RiAiAgentLine;
case 'commands':
return RiSlashCommands2;
case 'mcp':
return McpIcon;
case 'skills.installed':
return RiBookOpenLine;
case 'skills.catalog':
return RiBookLine;
case 'git':
return RiGitBranchLine;
case 'usage':
return RiBarChart2Line;
case 'voice':
return RiMicLine;
case 'tunnel':
return RiGlobalLine;
case 'home':
return null;
default:
return RiRobot2Line;
}
}
const SettingsHome: React.FC<{ onOpen: (slug: SettingsPageSlug) => void }> = ({ onOpen }) => {
return (
<div className="h-full overflow-auto">
<div className="mx-auto w-full max-w-3xl px-6 py-6 space-y-6">
<div className="space-y-1">
<h1 className="typography-ui-header font-semibold text-foreground">Settings</h1>
<p className="typography-ui text-muted-foreground">Jump to common pages.</p>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => onOpen('providers')}
className={cn(
'rounded-lg border border-border bg-[var(--surface-elevated)] p-4 text-left',
'hover:bg-[var(--interactive-hover)] transition-colors'
)}
>
<div className="typography-ui-label text-foreground">Providers</div>
<div className="typography-micro text-muted-foreground/70">Connect models + credentials</div>
</button>
<button
type="button"
onClick={() => onOpen('agents')}
className={cn(
'rounded-lg border border-border bg-[var(--surface-elevated)] p-4 text-left',
'hover:bg-[var(--interactive-hover)] transition-colors'
)}
>
<div className="typography-ui-label text-foreground">Agents</div>
<div className="typography-micro text-muted-foreground/70">Prompts, tools, permissions</div>
</button>
<button
type="button"
onClick={() => onOpen('skills.catalog')}
className={cn(
'rounded-lg border border-border bg-[var(--surface-elevated)] p-4 text-left',
'hover:bg-[var(--interactive-hover)] transition-colors'
)}
>
<div className="typography-ui-label text-foreground">Skills Catalog</div>
<div className="typography-micro text-muted-foreground/70">Install skills from catalogs</div>
</button>
<button
type="button"
onClick={() => onOpen('mcp')}
className={cn(
'rounded-lg border border-border bg-[var(--surface-elevated)] p-4 text-left',
'hover:bg-[var(--interactive-hover)] transition-colors'
)}
>
<div className="typography-ui-label text-foreground">MCP</div>
<div className="typography-micro text-muted-foreground/70">Configure MCP servers + connections</div>
</button>
<button
type="button"
onClick={() => onOpen('usage')}
className={cn(
'rounded-lg border border-border bg-[var(--surface-elevated)] p-4 text-left',
'hover:bg-[var(--interactive-hover)] transition-colors'
)}
>
<div className="typography-ui-label text-foreground">Usage</div>
<div className="typography-micro text-muted-foreground/70">Quota + spend visibility</div>
</button>
</div>
</div>
</div>
);
};
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile, isWindowed }) => {
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<MobileStage>('nav');
const autoNavSlugRef = React.useRef<string | null>(null);
const [navWidth, setNavWidth] = React.useState(216);
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<HTMLDivElement>(null);
const isDesktopApp = React.useMemo(() => {
return isDesktopShell();
}, []);
// keep platform check available for future window chrome tweaks
const runtimeCtx = React.useMemo(() => buildRuntimeContext(isDesktopApp), [isDesktopApp]);
const visiblePages = React.useMemo(() => {
return SETTINGS_PAGE_METADATA
.filter((page) => page.slug !== 'home')
.filter((page) => isPageAvailable(page, runtimeCtx))
.filter((page) => !(runtimeCtx.isVSCode && page.slug === 'projects'))
.filter((page) => !(isMobile && page.slug === 'shortcuts'));
}, [runtimeCtx, isMobile]);
const sortedFilteredPages = React.useMemo(() => {
const rank = new Map<SettingsPageSlug, number>(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 = Math.min(
SETTINGS_NAV_MAX_WIDTH,
Math.max(SETTINGS_NAV_MIN_WIDTH, 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 = Math.min(
SETTINGS_NAV_MAX_WIDTH,
Math.max(SETTINGS_NAV_MIN_WIDTH, 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();
};
// Load stores when project changes or when a page becomes active.
React.useEffect(() => {
if (!isSettingsDialogOpen && !runtimeCtx.isVSCode) {
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 === 'skills.installed' || settingsSlug === 'skills.catalog') {
void useSkillsStore.getState().loadSkills();
void useSkillsCatalogStore.getState().loadCatalog();
}
}, [activeProjectId, isSettingsDialogOpen, 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]);
// Collapse main nav to icon rail when active page has its own sidebar
const isNavCollapsed = !isMobile && activePageMeta?.kind === 'split';
const openChamberSectionBySlug: Partial<Record<SettingsPageSlug, OpenChamberSection>> = React.useMemo(() => ({
appearance: 'visual',
chat: 'chat',
shortcuts: 'shortcuts',
sessions: 'sessions',
notifications: 'notifications',
voice: 'voice',
tunnel: 'tunnel',
}), []);
const renderUnavailable = React.useCallback(() => {
return (
<div className="flex h-full items-center justify-center px-6">
<div className="max-w-md text-center">
<div className="typography-ui-header font-semibold text-foreground">Not available</div>
<p className="typography-ui text-muted-foreground mt-1">This settings page is not available in this runtime.</p>
</div>
</div>
);
}, []);
const renderPageSidebar = React.useCallback((slug: SettingsPageSlug, opts: { onItemSelect?: () => void }) => {
switch (slug) {
case 'projects':
return <ProjectsSidebar onItemSelect={opts.onItemSelect} />;
case 'remote-instances':
return <RemoteInstancesSidebar onItemSelect={opts.onItemSelect} />;
case 'agents':
return <AgentsSidebar onItemSelect={opts.onItemSelect} />;
case 'commands':
return <CommandsSidebar onItemSelect={opts.onItemSelect} />;
case 'mcp':
return <McpSidebar onItemSelect={opts.onItemSelect} />;
case 'skills.installed':
return <SkillsSidebar onItemSelect={opts.onItemSelect} />;
case 'providers':
return <ProvidersSidebar onItemSelect={opts.onItemSelect} />;
case 'usage':
return <UsageSidebar onItemSelect={opts.onItemSelect} />;
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 <SettingsHome onOpen={openPage} />;
case 'projects':
return <ProjectsPage />;
case 'remote-instances':
return <RemoteInstancesPage />;
case 'agents':
return <AgentsPage />;
case 'commands':
return <CommandsPage />;
case 'mcp':
return <McpPage />;
case 'skills.installed':
return <SkillsPage view="installed" />;
case 'skills.catalog':
return <SkillsPage view="catalog" />;
case 'providers':
return <ProvidersPage />;
case 'usage':
return <UsagePage />;
case 'git':
return <GitPage />;
case 'appearance':
case 'chat':
case 'shortcuts':
case 'sessions':
case 'notifications':
case 'voice':
case 'tunnel': {
const section = openChamberSectionBySlug[slug] ?? 'visual';
return <OpenChamberPage section={section} />;
}
default:
return <SettingsHome onOpen={openPage} />;
}
}, [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 shortcutKey = getModifierLabel();
const handleBack = React.useCallback(() => {
setMobileStage('nav');
}, []);
const handleOpenPageSidebar = React.useCallback(() => {
setMobileStage('page-sidebar');
}, []);
const renderSettingsNav = (collapsed: boolean) => {
return (
<div className="flex h-full flex-col overflow-hidden">
{/* Scrollable nav items */}
<div className="flex-1 min-h-0 overflow-y-auto overflow-x-hidden">
<div className="flex flex-col gap-0.5 pt-4 pb-2 px-2">
{sortedFilteredPages.map((page) => {
const selected = settingsSlug === page.slug;
const Icon = getSettingsNavIcon(page.slug);
if (!Icon) return null;
return (
<Tooltip key={page.slug} delayDuration={collapsed ? 100 : 600}>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => openPage(page.slug)}
className={cn(
'flex h-8 items-center gap-2 rounded-md px-2 overflow-hidden',
selected
? 'bg-interactive-selection text-foreground'
: 'text-foreground hover:bg-interactive-hover'
)}
>
<Icon className="h-4 w-4 shrink-0" />
<span
className={cn(
'flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150',
collapsed ? 'opacity-0' : 'opacity-100'
)}
>
<span className="typography-ui-label font-normal truncate">{page.title}</span>
{(page.slug === 'voice' || page.slug === 'tunnel') && (
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">
beta
</span>
)}
</span>
</button>
</TooltipTrigger>
{collapsed && (
<TooltipContent side="right" sideOffset={8}>
{page.title}
</TooltipContent>
)}
</Tooltip>
);
})}
</div>
</div>
{/* Footer — hidden when collapsed via overflow on parent */}
<div
className={cn(
'overflow-hidden transition-opacity duration-150',
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'
)}
>
<div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5">
{!runtimeCtx.isVSCode && (
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<button
type="button"
className={cn(
'flex h-7 w-full items-center gap-2 rounded-md px-2 overflow-hidden whitespace-nowrap',
'text-sm font-semibold text-sidebar-foreground/90',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
)}
onClick={() => void reloadOpenCodeConfiguration({ message: 'Restarting OpenCode…', mode: 'projects', scopes: ['all'] })}
>
<RiRestartLine className="h-4 w-4 shrink-0" />
<span>Reload OpenCode</span>
</button>
</TooltipTrigger>
<TooltipContent>
Restart OpenCode and reload its configuration.
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</div>
);
};
const renderMobileStage = () => {
if (mobileStage === 'nav') {
return (
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<div className="flex h-full min-h-0 flex-col">
<ErrorBoundary>{renderSettingsNav(false)}</ErrorBoundary>
</div>
</div>
);
}
if (!activePageMeta) {
return <div className="flex-1 bg-background" />;
}
if (mobileStage === 'page-sidebar') {
if (activePageMeta.kind !== 'split') {
// No sidebar available; fall back to direct content.
const fallback = renderPageContent(settingsSlug);
return (
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
<ErrorBoundary>{fallback}</ErrorBoundary>
</div>
);
}
return (
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<ErrorBoundary>
{renderPageSidebar(settingsSlug, { onItemSelect: () => setMobileStage('page-content') })}
</ErrorBoundary>
</div>
);
}
// page-content
const content = renderPageContent(settingsSlug);
return (
<div className="flex-1 min-h-0 overflow-hidden bg-background" data-keyboard-avoid="true">
<ErrorBoundary>{content}</ErrorBoundary>
</div>
);
};
const renderDesktopContent = () => {
if (!activePageMeta || settingsSlug === 'home') {
return <SettingsHome onOpen={openPage} />;
}
if (activePageMeta.kind === 'split') {
return (
<div className="flex h-full min-h-0 overflow-hidden">
<div className={cn('w-[264px] min-w-[264px] border-r', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')} style={{ borderColor: 'var(--interactive-border)' }}>
<ErrorBoundary>{renderPageSidebar(settingsSlug, {})}</ErrorBoundary>
</div>
<div className="flex-1 overflow-hidden bg-background">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
</div>
</div>
);
}
return (
<div className="h-full overflow-hidden bg-background">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
</div>
);
};
return (
<div ref={containerRef} data-settings-view="true" className={cn('relative flex h-full min-h-0 flex-col overflow-hidden bg-background')}>
{isMobile ? (
<div
className={cn(
'flex items-center gap-2 px-3 py-2 border-b',
'bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80'
)}
style={{ borderColor: 'var(--interactive-border)' }}
>
<button
type="button"
onClick={showBackButton ? handleBack : onClose}
aria-label={showBackButton ? 'Back to Settings' : 'Close settings'}
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiArrowLeftSLine className="h-5 w-5" />
</button>
<div className="min-w-0 flex-1 typography-ui-label font-medium text-foreground truncate">
{mobileStage === 'nav'
? 'Settings'
: (activePageMeta?.title ?? 'Settings')}
</div>
{mobileStage === 'page-content' && activePageMeta?.kind === 'split' && (
<button
type="button"
onClick={handleOpenPageSidebar}
aria-label="Open section list"
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiListUnordered className="h-5 w-5" />
</button>
)}
{onClose && (
<button
type="button"
onClick={onClose}
aria-label="Close settings"
title={`Close Settings (${shortcutKey}+,)`}
className="inline-flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiCloseLine className="h-5 w-5" />
</button>
)}
</div>
) : (
<>
{showBackButton && (
<div className={cn('absolute left-3 z-50', isWindowed ? 'top-2' : 'top-3')}>
<button
type="button"
onClick={handleBack}
aria-label="Back"
className="inline-flex h-9 w-9 items-center justify-center rounded-lg p-2 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiArrowLeftSLine className="h-5 w-5" />
</button>
</div>
)}
{onClose && (
<div className={cn('absolute right-0.5 z-50', isWindowed ? 'top-0.5' : 'top-1')}>
<button
type="button"
onClick={onClose}
aria-label="Close settings"
title={`Close Settings (${shortcutKey}+,)`}
className="inline-flex h-7 w-7 items-center justify-center rounded-md p-0.5 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
>
<RiCloseLine className="h-5 w-5" />
</button>
</div>
)}
</>
)}
<div className="flex flex-1 min-h-0 overflow-hidden">
{isMobile ? (
renderMobileStage()
) : (
<>
<div
className={cn(
'relative flex h-full min-h-0 flex-col overflow-hidden border-r',
isDesktopApp
? 'bg-[color:var(--sidebar-overlay-strong)] supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
: runtimeCtx.isVSCode
? 'bg-background'
: 'bg-sidebar',
isResizing && !isNavCollapsed ? '' : 'transition-[width,min-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]'
)}
style={{
width: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`,
minWidth: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`,
borderColor: 'var(--interactive-border)',
}}
>
{!isNavCollapsed && (
<div
className={cn(
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
)}
onPointerDown={handlePointerDown}
role="separator"
aria-orientation="vertical"
aria-label="Resize settings navigation"
/>
)}
<ErrorBoundary>
{renderSettingsNav(isNavCollapsed)}
</ErrorBoundary>
</div>
<div className="flex-1 overflow-hidden bg-background">
{renderDesktopContent()}
</div>
</>
)}
</div>
</div>
);
};