feat: enhance sidebar components for VS Code runtime support and improve styling
This commit is contained in:
@@ -9,6 +9,7 @@ All notable changes to this project will be documented in this file.
|
||||
- ESC key now closes settings; double-ESC abort only works on chat tab without overlays.
|
||||
- Added responsive tab labels in settings header (icons only at narrow widths).
|
||||
- Improved session activity status handling and message step completion logic.
|
||||
- Introduced enchanced VSCode extension settings with dynamic layout based on width.
|
||||
|
||||
|
||||
## [1.3.6] - 2025-12-27
|
||||
|
||||
@@ -48,7 +48,7 @@ export const SidebarContextSummary: React.FC<SidebarContextSummaryProps> = ({ cl
|
||||
}, [directoryFull]);
|
||||
|
||||
return (
|
||||
<div className={cn('hidden min-h-[48px] flex-col justify-center gap-0.5 border-b border-border/40 bg-sidebar/60 px-3 py-2 backdrop-blur md:flex md:pb-2', className)}>
|
||||
<div className={cn('hidden min-h-[48px] flex-col justify-center gap-0.5 border-b bg-sidebar/60 px-3 py-2 backdrop-blur md:flex md:pb-2', className)}>
|
||||
<span className="typography-meta text-muted-foreground">Session</span>
|
||||
<span className="typography-ui-label font-semibold text-foreground truncate" title={activeSessionTitle}>
|
||||
{activeSessionTitle}
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
||||
import { ChatView } from '@/components/views';
|
||||
import { ChatView, SettingsView } from '@/components/views';
|
||||
import { useSessionStore } from '@/stores/useSessionStore';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
import { RiAddLine, RiArrowLeftLine, RiSettings3Line } from '@remixicon/react';
|
||||
import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPage';
|
||||
|
||||
// Width threshold for mobile vs desktop layout in settings
|
||||
const MOBILE_WIDTH_THRESHOLD = 550;
|
||||
|
||||
type VSCodeView = 'sessions' | 'chat' | 'settings';
|
||||
|
||||
export const VSCodeLayout: React.FC = () => {
|
||||
const [currentView, setCurrentView] = React.useState<VSCodeView>('chat');
|
||||
const [containerWidth, setContainerWidth] = React.useState<number>(0);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||
const sessions = useSessionStore((state) => state.sessions);
|
||||
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
||||
@@ -110,8 +114,28 @@ export const VSCodeLayout: React.FC = () => {
|
||||
void hydrateMessages();
|
||||
}, [connectionStatus, currentSessionId, currentView, hasInitializedOnce, loadMessages, messages, newSessionDraftOpen]);
|
||||
|
||||
// Track container width for responsive settings layout
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setContainerWidth(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
// Set initial width
|
||||
setContainerWidth(container.clientWidth);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const usesMobileLayout = containerWidth > 0 && containerWidth < MOBILE_WIDTH_THRESHOLD;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full bg-background text-foreground flex flex-col">
|
||||
<div ref={containerRef} className="h-full w-full bg-background text-foreground flex flex-col">
|
||||
{currentView === 'sessions' ? (
|
||||
<div className="flex flex-col h-full">
|
||||
<VSCodeHeader
|
||||
@@ -129,16 +153,10 @@ export const VSCodeLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
) : currentView === 'settings' ? (
|
||||
<div className="flex flex-col h-full">
|
||||
<VSCodeHeader
|
||||
title="Settings"
|
||||
showBack
|
||||
onBack={handleBackToSessions}
|
||||
/>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<VSCodeSettingsView />
|
||||
</div>
|
||||
</div>
|
||||
<SettingsView
|
||||
onClose={() => setCurrentView('sessions')}
|
||||
forceMobile={usesMobileLayout}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col h-full">
|
||||
<VSCodeHeader
|
||||
@@ -186,7 +204,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
{showBack && onBack && (
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="p-1 rounded hover:bg-muted transition-colors"
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
aria-label="Back to sessions"
|
||||
>
|
||||
<RiArrowLeftLine className="h-5 w-5" />
|
||||
@@ -196,7 +214,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
{onNewSession && (
|
||||
<button
|
||||
onClick={onNewSession}
|
||||
className="p-1 rounded hover:bg-muted transition-colors"
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
aria-label="New session"
|
||||
>
|
||||
<RiAddLine className="h-5 w-5" />
|
||||
@@ -205,7 +223,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
{onSettings && (
|
||||
<button
|
||||
onClick={onSettings}
|
||||
className="p-1 rounded hover:bg-muted transition-colors"
|
||||
className="inline-flex h-9 w-9 items-center justify-center p-2 text-muted-foreground hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<RiSettings3Line className="h-5 w-5" />
|
||||
@@ -224,13 +242,4 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
||||
);
|
||||
};
|
||||
|
||||
const VSCodeSettingsView: React.FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<OpenChamberPage />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { RiAddLine, RiAiAgentFill, RiAiAgentLine, RiDeleteBinLine, RiFileCopyLin
|
||||
import { useAgentsStore, isAgentBuiltIn, isAgentHidden } from '@/stores/useAgentsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { Agent } from '@opencode-ai/sdk';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
@@ -50,6 +51,8 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
@@ -59,6 +62,12 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
loadAgents();
|
||||
}, [loadAgents]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
const handleCreateAgent = () => {
|
||||
if (!newAgentName.trim()) {
|
||||
toast.error('Agent name is required');
|
||||
@@ -132,9 +141,9 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
||||
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {visibleAgents.length}</span>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { RiAddLine, RiTerminalBoxLine, RiMore2Line, RiDeleteBinLine, RiFileCopyL
|
||||
import { useCommandsStore, type Command } from '@/stores/useCommandsStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -49,6 +50,8 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
@@ -58,6 +61,12 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
||||
loadCommands();
|
||||
}, [loadCommands]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
const handleCreateCommand = () => {
|
||||
if (!newCommandName.trim()) {
|
||||
toast.error('Command name is required');
|
||||
@@ -110,9 +119,9 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<Dialog open={isCreateDialogOpen} onOpenChange={setIsCreateDialogOpen}>
|
||||
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {commands.length}</span>
|
||||
<DialogTrigger asChild>
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { GitIdentityProfile } from '@/stores/useGitIdentitiesStore';
|
||||
@@ -65,6 +66,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
@@ -75,6 +78,12 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
loadGlobalIdentity();
|
||||
}, [loadProfiles, loadGlobalIdentity]);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
const handleCreateProfile = () => {
|
||||
setSelectedProfile('new');
|
||||
onItemSelect?.();
|
||||
@@ -95,8 +104,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
||||
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {profiles.length}</span>
|
||||
<Button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isWebRuntime } from '@/lib/desktop';
|
||||
import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { AboutSettings } from './AboutSettings';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -48,13 +48,24 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
// Desktop app: transparent for blur effect
|
||||
// VS Code: bg-background (same as page content)
|
||||
// Web/mobile: bg-sidebar
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
|
||||
{OPENCHAMBER_SECTION_GROUPS.map((group) => {
|
||||
const isSelected = selectedSection === group.id;
|
||||
@@ -84,7 +95,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
||||
|
||||
{/* Mobile footer: About section */}
|
||||
{showAbout && (
|
||||
<div className="border-t border-border/40 px-3 py-4">
|
||||
<div className="border-t px-3 py-4">
|
||||
<AboutSettings />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useConfigStore } from '@/stores/useConfigStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { RiAddLine, RiStackLine } from '@remixicon/react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -24,14 +25,22 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
||||
<div className={cn('border-b border-border/40 px-3 dark:border-white/10', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||
<div className={cn('border-b px-3', isMobile ? 'mt-2 py-3' : 'py-3')}>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="typography-meta text-muted-foreground">Total {providers.length}</span>
|
||||
<Button
|
||||
|
||||
@@ -37,7 +37,7 @@ export const SettingsSidebarHeader: React.FC<SettingsSidebarHeaderProps> = ({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'border-b border-border/40 px-3 dark:border-white/10',
|
||||
'border-b px-3',
|
||||
isMobile ? 'mt-2 py-3' : 'py-3'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface SettingsSidebarLayoutProps {
|
||||
@@ -37,16 +38,27 @@ export const SettingsSidebarLayout: React.FC<SettingsSidebarLayoutProps> = ({
|
||||
return typeof window.opencodeDesktop !== 'undefined';
|
||||
});
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
// Desktop app: transparent for blur effect
|
||||
// VS Code: bg-background (same as page content)
|
||||
// Web/mobile: bg-sidebar
|
||||
const bgClass = isDesktopRuntime
|
||||
? 'bg-transparent'
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-full flex-col',
|
||||
isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar',
|
||||
bgClass,
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPa
|
||||
import { OpenChamberSidebar, type OpenChamberSection } from '@/components/sections/openchamber/OpenChamberSidebar';
|
||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
|
||||
const SETTINGS_SECTIONS = (() => {
|
||||
const filtered = SIDEBAR_SECTIONS.filter(section => section.id !== 'sessions');
|
||||
@@ -29,12 +30,18 @@ const SETTINGS_SIDEBAR_MIN_WIDTH = 200;
|
||||
const SETTINGS_SIDEBAR_MAX_WIDTH = 500;
|
||||
const SETTINGS_SIDEBAR_DEFAULT_WIDTH = 264;
|
||||
|
||||
// Width threshold for hiding tab labels (show icons only)
|
||||
const TAB_LABELS_MIN_WIDTH = 700;
|
||||
|
||||
interface SettingsViewProps {
|
||||
onClose?: () => void;
|
||||
/** Force mobile layout regardless of device detection */
|
||||
forceMobile?: boolean;
|
||||
}
|
||||
|
||||
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
const { isMobile } = useDeviceInfo();
|
||||
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile }) => {
|
||||
const deviceInfo = useDeviceInfo();
|
||||
const isMobile = forceMobile ?? deviceInfo.isMobile;
|
||||
const [activeTab, setActiveTab] = React.useState<SidebarSection>('settings');
|
||||
const [selectedOpenChamberSection, setSelectedOpenChamberSection] = React.useState<OpenChamberSection>('visual');
|
||||
// Mobile drill-down state: show page content instead of sidebar
|
||||
@@ -51,6 +58,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
});
|
||||
const [hasManuallyResized, setHasManuallyResized] = React.useState(false);
|
||||
const [isResizing, setIsResizing] = React.useState(false);
|
||||
const [containerWidth, setContainerWidth] = React.useState(0);
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
const startXRef = React.useRef(0);
|
||||
const startWidthRef = React.useRef(sidebarWidth);
|
||||
|
||||
@@ -64,11 +73,32 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||
}, []);
|
||||
|
||||
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
setIsDesktopApp(typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined');
|
||||
}, []);
|
||||
|
||||
// Track container width for responsive tab labels
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
setContainerWidth(entry.contentRect.width);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
setContainerWidth(container.clientWidth);
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
const showTabLabels = containerWidth === 0 || containerWidth >= TAB_LABELS_MIN_WIDTH;
|
||||
|
||||
// Update proportional width on window resize (if not manually resized)
|
||||
React.useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
@@ -221,7 +251,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
const showLeadingDivider = isDesktopApp && isMacPlatform;
|
||||
|
||||
return (
|
||||
<div className={cn('flex h-full flex-col overflow-hidden', isDesktopApp ? 'bg-transparent' : 'bg-background')}>
|
||||
<div ref={containerRef} className={cn('flex h-full flex-col overflow-hidden', isDesktopApp ? 'bg-transparent' : 'bg-background')}>
|
||||
{/* Header with tabs and close button */}
|
||||
<div
|
||||
onMouseDown={!isMobile ? handleDragStart : undefined}
|
||||
@@ -296,7 +326,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
aria-label={label}
|
||||
>
|
||||
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
||||
<span className="header-tab-label">{label}</span>
|
||||
{showTabLabels && <span>{label}</span>}
|
||||
</button>
|
||||
{/* Vertical divider after each tab */}
|
||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
||||
@@ -338,7 +368,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
<ErrorBoundary>{renderPageContent()}</ErrorBoundary>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 overflow-hidden bg-sidebar">
|
||||
<div className={cn('flex-1 overflow-hidden', isVSCode ? 'bg-background' : 'bg-sidebar')}>
|
||||
<ErrorBoundary>{renderSidebarContent()}</ErrorBoundary>
|
||||
</div>
|
||||
)
|
||||
@@ -351,7 +381,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
||||
'relative overflow-hidden border-r',
|
||||
isDesktopApp
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar',
|
||||
: isVSCode
|
||||
? 'bg-background'
|
||||
: 'bg-sidebar',
|
||||
isResizing ? 'transition-none' : ''
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -12,12 +12,14 @@ export type VSCodeThemeColorToken =
|
||||
| 'editor.lineHighlightBackground'
|
||||
| 'editorCursor.foreground'
|
||||
| 'focusBorder'
|
||||
| 'contrastBorder'
|
||||
| 'diffEditor.insertedTextBackground'
|
||||
| 'diffEditor.insertedTextBorder'
|
||||
| 'diffEditor.insertedLineBackground'
|
||||
| 'gitDecoration.addedResourceForeground'
|
||||
| 'sideBar.background'
|
||||
| 'sideBar.foreground'
|
||||
| 'sideBar.border'
|
||||
| 'panel.background'
|
||||
| 'panel.foreground'
|
||||
| 'panel.border'
|
||||
@@ -69,12 +71,14 @@ const VARIABLE_MAP: Record<VSCodeThemeColorToken, string> = {
|
||||
'editor.lineHighlightBackground': '--vscode-editor-lineHighlightBackground',
|
||||
'editorCursor.foreground': '--vscode-editorCursor-foreground',
|
||||
focusBorder: '--vscode-focusBorder',
|
||||
contrastBorder: '--vscode-contrastBorder',
|
||||
'diffEditor.insertedTextBackground': '--vscode-diffEditor-insertedTextBackground',
|
||||
'diffEditor.insertedTextBorder': '--vscode-diffEditor-insertedTextBorder',
|
||||
'diffEditor.insertedLineBackground': '--vscode-diffEditor-insertedLineBackground',
|
||||
'gitDecoration.addedResourceForeground': '--vscode-gitDecoration-addedResourceForeground',
|
||||
'sideBar.background': '--vscode-sideBar-background',
|
||||
'sideBar.foreground': '--vscode-sideBar-foreground',
|
||||
'sideBar.border': '--vscode-sideBar-border',
|
||||
'panel.background': '--vscode-panel-background',
|
||||
'panel.foreground': '--vscode-panel-foreground',
|
||||
'panel.border': '--vscode-panel-border',
|
||||
@@ -200,11 +204,14 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
||||
palette.colors[token] ?? fallback;
|
||||
|
||||
const sidebarBg = read('sideBar.background', base.colors.surface.background);
|
||||
const sidebarFg = read('sideBar.foreground', read('descriptionForeground', base.colors.surface.mutedForeground));
|
||||
const panelBg = read('panel.background', read('editor.background', base.colors.surface.elevated));
|
||||
const panelFg = read('panel.foreground', read('editor.foreground', base.colors.surface.foreground));
|
||||
const background = sidebarBg;
|
||||
const foreground = read('editor.foreground', base.colors.surface.foreground);
|
||||
// Use descriptionForeground for muted text with reduced opacity for less prominence
|
||||
// This makes inactive tabs and secondary text clearly distinguishable from active/primary text
|
||||
const rawMutedFg = read('descriptionForeground', base.colors.surface.mutedForeground);
|
||||
const mutedFg = applyAlpha(rawMutedFg, palette.kind === 'light' ? 0.55 : 0.5);
|
||||
// Prefer VS Code's "added diff" color as our primary accent when available (users expect this to match their theme).
|
||||
const diffInserted = palette.colors['diffEditor.insertedTextBorder']
|
||||
?? palette.colors['diffEditor.insertedLineBackground']
|
||||
@@ -219,8 +226,14 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
||||
const selection = read('editor.selectionBackground', activeBg);
|
||||
const selectionFg = read('editor.selectionForeground', foreground);
|
||||
const focus = read('focusBorder', accent);
|
||||
// Prefer panel border for a less prominent, more consistent border color in webviews.
|
||||
const border = read('panel.border', read('input.border', base.colors.interactive.border));
|
||||
// Build a visible border color: prefer contrastBorder (high-contrast themes), then sideBar.border, panel.border, input.border
|
||||
// If the chosen border is too transparent or matches background, derive one from foreground
|
||||
const rawBorder = read('contrastBorder', '') ||
|
||||
read('sideBar.border', '') ||
|
||||
read('panel.border', '') ||
|
||||
read('input.border', base.colors.interactive.border);
|
||||
// Ensure border has enough visibility by applying minimum opacity
|
||||
const border = rawBorder ? applyAlpha(forceOpaque(rawBorder), palette.kind === 'light' ? 0.15 : 0.2) : applyAlpha(foreground, palette.kind === 'light' ? 0.1 : 0.15);
|
||||
const focusRing = applyAlpha(focus, palette.kind === 'light' ? 0.35 : 0.45);
|
||||
const cursor = read('editorCursor.foreground', base.colors.interactive.cursor);
|
||||
const badgeBg = read('badge.background', accent);
|
||||
@@ -263,7 +276,7 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
||||
background,
|
||||
foreground,
|
||||
muted: activeBg,
|
||||
mutedForeground: sidebarFg,
|
||||
mutedForeground: mutedFg,
|
||||
elevated: panelBg,
|
||||
elevatedForeground: panelFg,
|
||||
overlay: read('statusBar.background', base.colors.surface.overlay),
|
||||
|
||||
Reference in New Issue
Block a user