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.
|
- 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).
|
- Added responsive tab labels in settings header (icons only at narrow widths).
|
||||||
- Improved session activity status handling and message step completion logic.
|
- 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
|
## [1.3.6] - 2025-12-27
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const SidebarContextSummary: React.FC<SidebarContextSummaryProps> = ({ cl
|
|||||||
}, [directoryFull]);
|
}, [directoryFull]);
|
||||||
|
|
||||||
return (
|
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-meta text-muted-foreground">Session</span>
|
||||||
<span className="typography-ui-label font-semibold text-foreground truncate" title={activeSessionTitle}>
|
<span className="typography-ui-label font-semibold text-foreground truncate" title={activeSessionTitle}>
|
||||||
{activeSessionTitle}
|
{activeSessionTitle}
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||||
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
||||||
import { ChatView } from '@/components/views';
|
import { ChatView, SettingsView } from '@/components/views';
|
||||||
import { useSessionStore } from '@/stores/useSessionStore';
|
import { useSessionStore } from '@/stores/useSessionStore';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||||
import { RiAddLine, RiArrowLeftLine, RiSettings3Line } from '@remixicon/react';
|
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';
|
type VSCodeView = 'sessions' | 'chat' | 'settings';
|
||||||
|
|
||||||
export const VSCodeLayout: React.FC = () => {
|
export const VSCodeLayout: React.FC = () => {
|
||||||
const [currentView, setCurrentView] = React.useState<VSCodeView>('chat');
|
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 currentSessionId = useSessionStore((state) => state.currentSessionId);
|
||||||
const sessions = useSessionStore((state) => state.sessions);
|
const sessions = useSessionStore((state) => state.sessions);
|
||||||
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
||||||
@@ -110,8 +114,28 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
void hydrateMessages();
|
void hydrateMessages();
|
||||||
}, [connectionStatus, currentSessionId, currentView, hasInitializedOnce, loadMessages, messages, newSessionDraftOpen]);
|
}, [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 (
|
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' ? (
|
{currentView === 'sessions' ? (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<VSCodeHeader
|
<VSCodeHeader
|
||||||
@@ -129,16 +153,10 @@ export const VSCodeLayout: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : currentView === 'settings' ? (
|
) : currentView === 'settings' ? (
|
||||||
<div className="flex flex-col h-full">
|
<SettingsView
|
||||||
<VSCodeHeader
|
onClose={() => setCurrentView('sessions')}
|
||||||
title="Settings"
|
forceMobile={usesMobileLayout}
|
||||||
showBack
|
/>
|
||||||
onBack={handleBackToSessions}
|
|
||||||
/>
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
|
||||||
<VSCodeSettingsView />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<VSCodeHeader
|
<VSCodeHeader
|
||||||
@@ -186,7 +204,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
{showBack && onBack && (
|
{showBack && onBack && (
|
||||||
<button
|
<button
|
||||||
onClick={onBack}
|
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"
|
aria-label="Back to sessions"
|
||||||
>
|
>
|
||||||
<RiArrowLeftLine className="h-5 w-5" />
|
<RiArrowLeftLine className="h-5 w-5" />
|
||||||
@@ -196,7 +214,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
{onNewSession && (
|
{onNewSession && (
|
||||||
<button
|
<button
|
||||||
onClick={onNewSession}
|
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"
|
aria-label="New session"
|
||||||
>
|
>
|
||||||
<RiAddLine className="h-5 w-5" />
|
<RiAddLine className="h-5 w-5" />
|
||||||
@@ -205,7 +223,7 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
|
|||||||
{onSettings && (
|
{onSettings && (
|
||||||
<button
|
<button
|
||||||
onClick={onSettings}
|
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"
|
aria-label="Settings"
|
||||||
>
|
>
|
||||||
<RiSettings3Line className="h-5 w-5" />
|
<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 { useAgentsStore, isAgentBuiltIn, isAgentHidden } from '@/stores/useAgentsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { Agent } from '@opencode-ai/sdk';
|
import type { Agent } from '@opencode-ai/sdk';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
@@ -50,6 +51,8 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||||
@@ -59,6 +62,12 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
|||||||
loadAgents();
|
loadAgents();
|
||||||
}, [loadAgents]);
|
}, [loadAgents]);
|
||||||
|
|
||||||
|
const bgClass = isDesktopRuntime
|
||||||
|
? 'bg-transparent'
|
||||||
|
: isVSCode
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-sidebar';
|
||||||
|
|
||||||
const handleCreateAgent = () => {
|
const handleCreateAgent = () => {
|
||||||
if (!newAgentName.trim()) {
|
if (!newAgentName.trim()) {
|
||||||
toast.error('Agent name is required');
|
toast.error('Agent name is required');
|
||||||
@@ -132,9 +141,9 @@ export const AgentsSidebar: React.FC<AgentsSidebarProps> = ({ onItemSelect }) =>
|
|||||||
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
|
const customAgents = visibleAgents.filter((agent) => !isAgentBuiltIn(agent));
|
||||||
|
|
||||||
return (
|
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}>
|
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="typography-meta text-muted-foreground">Total {visibleAgents.length}</span>
|
<span className="typography-meta text-muted-foreground">Total {visibleAgents.length}</span>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { RiAddLine, RiTerminalBoxLine, RiMore2Line, RiDeleteBinLine, RiFileCopyL
|
|||||||
import { useCommandsStore, type Command } from '@/stores/useCommandsStore';
|
import { useCommandsStore, type Command } from '@/stores/useCommandsStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||||
@@ -58,6 +61,12 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
|||||||
loadCommands();
|
loadCommands();
|
||||||
}, [loadCommands]);
|
}, [loadCommands]);
|
||||||
|
|
||||||
|
const bgClass = isDesktopRuntime
|
||||||
|
? 'bg-transparent'
|
||||||
|
: isVSCode
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-sidebar';
|
||||||
|
|
||||||
const handleCreateCommand = () => {
|
const handleCreateCommand = () => {
|
||||||
if (!newCommandName.trim()) {
|
if (!newCommandName.trim()) {
|
||||||
toast.error('Command name is required');
|
toast.error('Command name is required');
|
||||||
@@ -110,9 +119,9 @@ export const CommandsSidebar: React.FC<CommandsSidebarProps> = ({ onItemSelect }
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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}>
|
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="typography-meta text-muted-foreground">Total {commands.length}</span>
|
<span className="typography-meta text-muted-foreground">Total {commands.length}</span>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
import { useGitIdentitiesStore } from '@/stores/useGitIdentitiesStore';
|
||||||
import { useUIStore } from '@/stores/useUIStore';
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import type { GitIdentityProfile } from '@/stores/useGitIdentitiesStore';
|
import type { GitIdentityProfile } from '@/stores/useGitIdentitiesStore';
|
||||||
@@ -65,6 +66,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||||
@@ -75,6 +78,12 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
|||||||
loadGlobalIdentity();
|
loadGlobalIdentity();
|
||||||
}, [loadProfiles, loadGlobalIdentity]);
|
}, [loadProfiles, loadGlobalIdentity]);
|
||||||
|
|
||||||
|
const bgClass = isDesktopRuntime
|
||||||
|
? 'bg-transparent'
|
||||||
|
: isVSCode
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-sidebar';
|
||||||
|
|
||||||
const handleCreateProfile = () => {
|
const handleCreateProfile = () => {
|
||||||
setSelectedProfile('new');
|
setSelectedProfile('new');
|
||||||
onItemSelect?.();
|
onItemSelect?.();
|
||||||
@@ -95,8 +104,8 @@ export const GitIdentitiesSidebar: React.FC<GitIdentitiesSidebarProps> = ({ onIt
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||||
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="typography-meta text-muted-foreground">Total {profiles.length}</span>
|
<span className="typography-meta text-muted-foreground">Total {profiles.length}</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { isWebRuntime } from '@/lib/desktop';
|
import { isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||||
import { AboutSettings } from './AboutSettings';
|
import { AboutSettings } from './AboutSettings';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -48,13 +48,24 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
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 (
|
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">
|
<ScrollableOverlay outerClassName="flex-1 min-h-0" className="space-y-1 px-3 py-2 overflow-x-hidden">
|
||||||
{OPENCHAMBER_SECTION_GROUPS.map((group) => {
|
{OPENCHAMBER_SECTION_GROUPS.map((group) => {
|
||||||
const isSelected = selectedSection === group.id;
|
const isSelected = selectedSection === group.id;
|
||||||
@@ -84,7 +95,7 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
|
|||||||
|
|
||||||
{/* Mobile footer: About section */}
|
{/* Mobile footer: About section */}
|
||||||
{showAbout && (
|
{showAbout && (
|
||||||
<div className="border-t border-border/40 px-3 py-4">
|
<div className="border-t px-3 py-4">
|
||||||
<AboutSettings />
|
<AboutSettings />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ProviderLogo } from '@/components/ui/ProviderLogo';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useConfigStore } from '@/stores/useConfigStore';
|
import { useConfigStore } from '@/stores/useConfigStore';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { RiAddLine, RiStackLine } from '@remixicon/react';
|
import { RiAddLine, RiStackLine } from '@remixicon/react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -24,14 +25,22 @@ export const ProvidersSidebar: React.FC<ProvidersSidebarProps> = ({ onItemSelect
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const bgClass = isDesktopRuntime
|
||||||
|
? 'bg-transparent'
|
||||||
|
: isVSCode
|
||||||
|
? 'bg-background'
|
||||||
|
: 'bg-sidebar';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex h-full flex-col', isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar')}>
|
<div className={cn('flex h-full flex-col', bgClass)}>
|
||||||
<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">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="typography-meta text-muted-foreground">Total {providers.length}</span>
|
<span className="typography-meta text-muted-foreground">Total {providers.length}</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const SettingsSidebarHeader: React.FC<SettingsSidebarHeaderProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-b border-border/40 px-3 dark:border-white/10',
|
'border-b px-3',
|
||||||
isMobile ? 'mt-2 py-3' : 'py-3'
|
isMobile ? 'mt-2 py-3' : 'py-3'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface SettingsSidebarLayoutProps {
|
interface SettingsSidebarLayoutProps {
|
||||||
@@ -37,16 +38,27 @@ export const SettingsSidebarLayout: React.FC<SettingsSidebarLayoutProps> = ({
|
|||||||
return typeof window.opencodeDesktop !== 'undefined';
|
return typeof window.opencodeDesktop !== 'undefined';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined');
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-full flex-col',
|
'flex h-full flex-col',
|
||||||
isDesktopRuntime ? 'bg-transparent' : 'bg-sidebar',
|
bgClass,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { OpenChamberPage } from '@/components/sections/openchamber/OpenChamberPa
|
|||||||
import { OpenChamberSidebar, type OpenChamberSection } from '@/components/sections/openchamber/OpenChamberSidebar';
|
import { OpenChamberSidebar, type OpenChamberSection } from '@/components/sections/openchamber/OpenChamberSidebar';
|
||||||
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/ui/ErrorBoundary';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
|
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||||
|
|
||||||
const SETTINGS_SECTIONS = (() => {
|
const SETTINGS_SECTIONS = (() => {
|
||||||
const filtered = SIDEBAR_SECTIONS.filter(section => section.id !== 'sessions');
|
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_MAX_WIDTH = 500;
|
||||||
const SETTINGS_SIDEBAR_DEFAULT_WIDTH = 264;
|
const SETTINGS_SIDEBAR_DEFAULT_WIDTH = 264;
|
||||||
|
|
||||||
|
// Width threshold for hiding tab labels (show icons only)
|
||||||
|
const TAB_LABELS_MIN_WIDTH = 700;
|
||||||
|
|
||||||
interface SettingsViewProps {
|
interface SettingsViewProps {
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
/** Force mobile layout regardless of device detection */
|
||||||
|
forceMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile }) => {
|
||||||
const { isMobile } = useDeviceInfo();
|
const deviceInfo = useDeviceInfo();
|
||||||
|
const isMobile = forceMobile ?? deviceInfo.isMobile;
|
||||||
const [activeTab, setActiveTab] = React.useState<SidebarSection>('settings');
|
const [activeTab, setActiveTab] = React.useState<SidebarSection>('settings');
|
||||||
const [selectedOpenChamberSection, setSelectedOpenChamberSection] = React.useState<OpenChamberSection>('visual');
|
const [selectedOpenChamberSection, setSelectedOpenChamberSection] = React.useState<OpenChamberSection>('visual');
|
||||||
// Mobile drill-down state: show page content instead of sidebar
|
// 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 [hasManuallyResized, setHasManuallyResized] = React.useState(false);
|
||||||
const [isResizing, setIsResizing] = 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 startXRef = React.useRef(0);
|
||||||
const startWidthRef = React.useRef(sidebarWidth);
|
const startWidthRef = React.useRef(sidebarWidth);
|
||||||
|
|
||||||
@@ -64,11 +73,32 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
|||||||
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
setIsDesktopApp(typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined');
|
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)
|
// Update proportional width on window resize (if not manually resized)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -221,7 +251,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
|||||||
const showLeadingDivider = isDesktopApp && isMacPlatform;
|
const showLeadingDivider = isDesktopApp && isMacPlatform;
|
||||||
|
|
||||||
return (
|
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 */}
|
{/* Header with tabs and close button */}
|
||||||
<div
|
<div
|
||||||
onMouseDown={!isMobile ? handleDragStart : undefined}
|
onMouseDown={!isMobile ? handleDragStart : undefined}
|
||||||
@@ -296,7 +326,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
|||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
<PhosphorIcon className="h-4 w-4" weight="regular" />
|
||||||
<span className="header-tab-label">{label}</span>
|
{showTabLabels && <span>{label}</span>}
|
||||||
</button>
|
</button>
|
||||||
{/* Vertical divider after each tab */}
|
{/* Vertical divider after each tab */}
|
||||||
<div className="h-full w-px bg-border" aria-hidden="true" />
|
<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>
|
<ErrorBoundary>{renderPageContent()}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex-1 overflow-hidden bg-sidebar">
|
<div className={cn('flex-1 overflow-hidden', isVSCode ? 'bg-background' : 'bg-sidebar')}>
|
||||||
<ErrorBoundary>{renderSidebarContent()}</ErrorBoundary>
|
<ErrorBoundary>{renderSidebarContent()}</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -351,7 +381,9 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose }) => {
|
|||||||
'relative overflow-hidden border-r',
|
'relative overflow-hidden border-r',
|
||||||
isDesktopApp
|
isDesktopApp
|
||||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
? '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' : ''
|
isResizing ? 'transition-none' : ''
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ export type VSCodeThemeColorToken =
|
|||||||
| 'editor.lineHighlightBackground'
|
| 'editor.lineHighlightBackground'
|
||||||
| 'editorCursor.foreground'
|
| 'editorCursor.foreground'
|
||||||
| 'focusBorder'
|
| 'focusBorder'
|
||||||
|
| 'contrastBorder'
|
||||||
| 'diffEditor.insertedTextBackground'
|
| 'diffEditor.insertedTextBackground'
|
||||||
| 'diffEditor.insertedTextBorder'
|
| 'diffEditor.insertedTextBorder'
|
||||||
| 'diffEditor.insertedLineBackground'
|
| 'diffEditor.insertedLineBackground'
|
||||||
| 'gitDecoration.addedResourceForeground'
|
| 'gitDecoration.addedResourceForeground'
|
||||||
| 'sideBar.background'
|
| 'sideBar.background'
|
||||||
| 'sideBar.foreground'
|
| 'sideBar.foreground'
|
||||||
|
| 'sideBar.border'
|
||||||
| 'panel.background'
|
| 'panel.background'
|
||||||
| 'panel.foreground'
|
| 'panel.foreground'
|
||||||
| 'panel.border'
|
| 'panel.border'
|
||||||
@@ -69,12 +71,14 @@ const VARIABLE_MAP: Record<VSCodeThemeColorToken, string> = {
|
|||||||
'editor.lineHighlightBackground': '--vscode-editor-lineHighlightBackground',
|
'editor.lineHighlightBackground': '--vscode-editor-lineHighlightBackground',
|
||||||
'editorCursor.foreground': '--vscode-editorCursor-foreground',
|
'editorCursor.foreground': '--vscode-editorCursor-foreground',
|
||||||
focusBorder: '--vscode-focusBorder',
|
focusBorder: '--vscode-focusBorder',
|
||||||
|
contrastBorder: '--vscode-contrastBorder',
|
||||||
'diffEditor.insertedTextBackground': '--vscode-diffEditor-insertedTextBackground',
|
'diffEditor.insertedTextBackground': '--vscode-diffEditor-insertedTextBackground',
|
||||||
'diffEditor.insertedTextBorder': '--vscode-diffEditor-insertedTextBorder',
|
'diffEditor.insertedTextBorder': '--vscode-diffEditor-insertedTextBorder',
|
||||||
'diffEditor.insertedLineBackground': '--vscode-diffEditor-insertedLineBackground',
|
'diffEditor.insertedLineBackground': '--vscode-diffEditor-insertedLineBackground',
|
||||||
'gitDecoration.addedResourceForeground': '--vscode-gitDecoration-addedResourceForeground',
|
'gitDecoration.addedResourceForeground': '--vscode-gitDecoration-addedResourceForeground',
|
||||||
'sideBar.background': '--vscode-sideBar-background',
|
'sideBar.background': '--vscode-sideBar-background',
|
||||||
'sideBar.foreground': '--vscode-sideBar-foreground',
|
'sideBar.foreground': '--vscode-sideBar-foreground',
|
||||||
|
'sideBar.border': '--vscode-sideBar-border',
|
||||||
'panel.background': '--vscode-panel-background',
|
'panel.background': '--vscode-panel-background',
|
||||||
'panel.foreground': '--vscode-panel-foreground',
|
'panel.foreground': '--vscode-panel-foreground',
|
||||||
'panel.border': '--vscode-panel-border',
|
'panel.border': '--vscode-panel-border',
|
||||||
@@ -200,11 +204,14 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
|||||||
palette.colors[token] ?? fallback;
|
palette.colors[token] ?? fallback;
|
||||||
|
|
||||||
const sidebarBg = read('sideBar.background', base.colors.surface.background);
|
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 panelBg = read('panel.background', read('editor.background', base.colors.surface.elevated));
|
||||||
const panelFg = read('panel.foreground', read('editor.foreground', base.colors.surface.foreground));
|
const panelFg = read('panel.foreground', read('editor.foreground', base.colors.surface.foreground));
|
||||||
const background = sidebarBg;
|
const background = sidebarBg;
|
||||||
const foreground = read('editor.foreground', base.colors.surface.foreground);
|
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).
|
// 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']
|
const diffInserted = palette.colors['diffEditor.insertedTextBorder']
|
||||||
?? palette.colors['diffEditor.insertedLineBackground']
|
?? palette.colors['diffEditor.insertedLineBackground']
|
||||||
@@ -219,8 +226,14 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
|||||||
const selection = read('editor.selectionBackground', activeBg);
|
const selection = read('editor.selectionBackground', activeBg);
|
||||||
const selectionFg = read('editor.selectionForeground', foreground);
|
const selectionFg = read('editor.selectionForeground', foreground);
|
||||||
const focus = read('focusBorder', accent);
|
const focus = read('focusBorder', accent);
|
||||||
// Prefer panel border for a less prominent, more consistent border color in webviews.
|
// Build a visible border color: prefer contrastBorder (high-contrast themes), then sideBar.border, panel.border, input.border
|
||||||
const border = read('panel.border', read('input.border', base.colors.interactive.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 focusRing = applyAlpha(focus, palette.kind === 'light' ? 0.35 : 0.45);
|
||||||
const cursor = read('editorCursor.foreground', base.colors.interactive.cursor);
|
const cursor = read('editorCursor.foreground', base.colors.interactive.cursor);
|
||||||
const badgeBg = read('badge.background', accent);
|
const badgeBg = read('badge.background', accent);
|
||||||
@@ -263,7 +276,7 @@ export const buildVSCodeThemeFromPalette = (palette: VSCodeThemePalette): Theme
|
|||||||
background,
|
background,
|
||||||
foreground,
|
foreground,
|
||||||
muted: activeBg,
|
muted: activeBg,
|
||||||
mutedForeground: sidebarFg,
|
mutedForeground: mutedFg,
|
||||||
elevated: panelBg,
|
elevated: panelBg,
|
||||||
elevatedForeground: panelFg,
|
elevatedForeground: panelFg,
|
||||||
overlay: read('statusBar.background', base.colors.surface.overlay),
|
overlay: read('statusBar.background', base.colors.surface.overlay),
|
||||||
|
|||||||
Reference in New Issue
Block a user