feat: enhance sidebar components for VS Code runtime support and improve styling

This commit is contained in:
Bohdan Triapitsyn
2025-12-28 03:28:13 +02:00
parent 21f164ea73
commit df07094d1e
12 changed files with 164 additions and 50 deletions
+1
View File
@@ -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={{
+17 -4
View File
@@ -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),