feat: enhance VSCode layout with expanded view and responsive design; update sidebar visibility and settings for VSCode runtime

This commit is contained in:
Bohdan Triapitsyn
2026-01-16 00:26:29 +02:00
parent 592351df9f
commit b1964c217c
10 changed files with 161 additions and 35 deletions
@@ -11,6 +11,10 @@ import { RiAddLine, RiArrowLeftLine, RiRobot2Line, RiSettings3Line } from '@remi
// Width threshold for mobile vs desktop layout in settings
const MOBILE_WIDTH_THRESHOLD = 550;
// Width threshold for expanded layout (sidebar + chat side by side)
const EXPANDED_LAYOUT_THRESHOLD = 700;
// Sessions sidebar width in expanded layout
const SESSIONS_SIDEBAR_WIDTH = 280;
type VSCodeView = 'sessions' | 'chat' | 'settings';
@@ -38,7 +42,7 @@ export const VSCodeLayout: React.FC = () => {
const hasAppliedInitialSession = React.useRef(false);
const [currentView, setCurrentView] = React.useState<VSCodeView>('chat');
const [currentView, setCurrentView] = React.useState<VSCodeView>('sessions');
const [containerWidth, setContainerWidth] = React.useState<number>(0);
const containerRef = React.useRef<HTMLDivElement>(null);
const currentSessionId = useSessionStore((state) => state.currentSessionId);
@@ -83,16 +87,16 @@ export const VSCodeLayout: React.FC = () => {
void vscodeApi.executeCommand('openchamber.setActiveSession', currentSessionId, activeSessionTitle);
}, [activeSessionTitle, currentSessionId, runtimeApis.vscode]);
// If the active session disappears (e.g., deleted), show a new chat view
// If the active session disappears (e.g., deleted), go back to sessions list
React.useEffect(() => {
if (viewMode === 'editor') {
return;
}
if (!currentSessionId && !newSessionDraftOpen) {
openNewSessionDraft();
if (!currentSessionId && !newSessionDraftOpen && currentView === 'chat') {
setCurrentView('sessions');
}
}, [currentSessionId, newSessionDraftOpen, openNewSessionDraft, viewMode]);
}, [currentSessionId, newSessionDraftOpen, currentView, viewMode]);
const handleBackToSessions = React.useCallback(() => {
setCurrentView('sessions');
@@ -199,19 +203,24 @@ export const VSCodeLayout: React.FC = () => {
if (hasAppliedInitialSession.current) {
return;
}
if (!initialSessionId) {
return;
}
if (!hasInitializedOnce || connectionStatus !== 'connected') {
return;
}
// No initialSessionId means open a new session draft
if (!initialSessionId) {
hasAppliedInitialSession.current = true;
openNewSessionDraft();
return;
}
if (!sessions.some((session) => session.id === initialSessionId)) {
return;
}
hasAppliedInitialSession.current = true;
void useSessionStore.getState().setCurrentSession(initialSessionId);
}, [connectionStatus, hasInitializedOnce, initialSessionId, sessions, viewMode]);
}, [connectionStatus, hasInitializedOnce, initialSessionId, openNewSessionDraft, sessions, viewMode]);
// Hydrate messages when viewing chat
React.useEffect(() => {
@@ -254,10 +263,20 @@ export const VSCodeLayout: React.FC = () => {
}, []);
const usesMobileLayout = containerWidth > 0 && containerWidth < MOBILE_WIDTH_THRESHOLD;
const usesExpandedLayout = containerWidth >= EXPANDED_LAYOUT_THRESHOLD;
// In expanded layout, always show chat (with sidebar alongside)
// Navigate to chat automatically when expanded layout is enabled and we're on sessions view
React.useEffect(() => {
if (usesExpandedLayout && currentView === 'sessions' && viewMode === 'sidebar') {
setCurrentView('chat');
}
}, [usesExpandedLayout, currentView, viewMode]);
return (
<div ref={containerRef} className="h-full w-full bg-background text-foreground flex flex-col">
{viewMode === 'editor' ? (
// Editor mode: just chat, no sidebar
<div className="flex flex-col h-full">
<VSCodeHeader
title={sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
@@ -270,7 +289,45 @@ export const VSCodeLayout: React.FC = () => {
</ErrorBoundary>
</div>
</div>
) : currentView === 'settings' ? (
// Settings view
<SettingsView
onClose={() => setCurrentView(usesExpandedLayout ? 'chat' : 'sessions')}
forceMobile={usesMobileLayout}
/>
) : usesExpandedLayout ? (
// Expanded layout: sessions sidebar + chat side by side
<div className="flex h-full">
{/* Sessions sidebar */}
<div
className="h-full border-r border-border overflow-hidden flex-shrink-0"
style={{ width: SESSIONS_SIDEBAR_WIDTH }}
>
<SessionSidebar
mobileVariant
allowReselect
hideDirectoryControls
showOnlyMainWorkspace
/>
</div>
{/* Chat content */}
<div className="flex-1 flex flex-col min-w-0">
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
? 'New session'
: sessions.find((session) => session.id === currentSessionId)?.title || 'Chat'}
showMcp
showContextUsage
/>
<div className="flex-1 overflow-hidden">
<ErrorBoundary>
<ChatView />
</ErrorBoundary>
</div>
</div>
</div>
) : currentView === 'sessions' ? (
// Compact layout: sessions list (drill-down)
<div className="flex flex-col h-full">
<VSCodeHeader
title="Sessions"
@@ -285,12 +342,8 @@ export const VSCodeLayout: React.FC = () => {
/>
</div>
</div>
) : currentView === 'settings' ? (
<SettingsView
onClose={() => setCurrentView('sessions')}
forceMobile={usesMobileLayout}
/>
) : (
// Compact layout: chat view (drill-down)
<div className="flex flex-col h-full">
<VSCodeHeader
title={newSessionDraftOpen && !currentSessionId
@@ -336,17 +389,17 @@ const VSCodeHeader: React.FC<VSCodeHeaderProps> = ({ title, showBack, onBack, on
const contextUsage = getContextUsage(contextLimit, outputLimit);
return (
<div className="flex items-center gap-2 px-3 py-2 border-b border-border bg-background shrink-0">
<div className="flex items-center gap-1.5 pl-1 pr-2 py-1 border-b border-border bg-background shrink-0">
{showBack && onBack && (
<button
onClick={onBack}
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"
className="inline-flex h-7 w-7 items-center justify-center 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" />
</button>
)}
<h1 className="text-sm font-medium truncate flex-1 h-9 w-9 items-center justify-center p-2" title={title}>{title}</h1>
<h1 className="text-sm font-medium truncate flex-1" title={title}>{title}</h1>
{onNewSession && (
<button
onClick={onNewSession}
@@ -17,6 +17,7 @@ interface SectionGroup {
label: string;
items: string[];
webOnly?: boolean;
hideInVSCode?: boolean;
}
const OPENCHAMBER_SECTION_GROUPS: SectionGroup[] = [
@@ -39,6 +40,7 @@ const OPENCHAMBER_SECTION_GROUPS: SectionGroup[] = [
id: 'git',
label: 'Git',
items: ['Commit Messages', 'Worktree'],
hideInVSCode: true,
},
{
id: 'notifications',
@@ -69,8 +71,12 @@ export const OpenChamberSidebar: React.FC<OpenChamberSidebarProps> = ({
}, []);
const visibleSections = React.useMemo(() => {
return OPENCHAMBER_SECTION_GROUPS.filter((group) => !group.webOnly || isWeb);
}, [isWeb]);
return OPENCHAMBER_SECTION_GROUPS.filter((group) => {
if (group.webOnly && !isWeb) return false;
if (group.hideInVSCode && isVSCode) return false;
return true;
});
}, [isWeb, isVSCode]);
// Desktop app: transparent for blur effect
// VS Code: bg-background (same as page content)
@@ -310,7 +310,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</div>
)}
{shouldShow('diffLayout') && !isMobile && (
{shouldShow('diffLayout') && !isMobile && !isVSCodeRuntime() && (
<div className="space-y-6">
<div className="space-y-1">
<h3 className="typography-ui-header font-semibold text-foreground">
@@ -32,12 +32,16 @@ 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');
const getSettingsSections = (isVSCode: boolean) => {
let filtered = SIDEBAR_SECTIONS.filter(section => section.id !== 'sessions');
// Hide Git Identities tab for VS Code
if (isVSCode) {
filtered = filtered.filter(section => section.id !== 'git-identities');
}
const settingsSection = filtered.find(s => s.id === 'settings');
const otherSections = filtered.filter(s => s.id !== 'settings');
return settingsSection ? [settingsSection, ...otherSections] : filtered;
})();
};
// Same constraints as main sidebar
const SETTINGS_SIDEBAR_MIN_WIDTH = 200;
@@ -89,6 +93,8 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
const settingsSections = React.useMemo(() => getSettingsSections(isVSCode), [isVSCode]);
React.useEffect(() => {
if (typeof window === 'undefined') return;
setIsDesktopApp(typeof (window as typeof window & { opencodeDesktop?: unknown }).opencodeDesktop !== 'undefined');
@@ -143,7 +149,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
return formatProjectLabel(rawLabel);
}, [activeProject, formatProjectLabel]);
const showProjectSwitcher = sortedProjects.length > 0;
const showProjectSwitcher = sortedProjects.length > 0 && !isVSCode;
const showTabLabels = containerWidth === 0 || containerWidth >= TAB_LABELS_MIN_WIDTH;
@@ -352,7 +358,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
<div className={cn('flex items-center', isMobile ? 'gap-1' : 'h-full')}>
{/* Leading divider before first tab - only on Mac desktop */}
{!isMobile && showLeadingDivider && <div className="h-full w-px bg-border" aria-hidden="true" />}
{SETTINGS_SECTIONS.map(({ id, label, icon: Icon }) => {
{settingsSections.map(({ id, label, icon: Icon }) => {
const isActive = activeTab === id;
const PhosphorIcon = Icon as React.ComponentType<{ className?: string; weight?: string }>;
@@ -7,6 +7,7 @@ import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
import { hasModifier } from '@/lib/utils';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { useConfigStore } from '@/stores/useConfigStore';
import { isVSCodeRuntime } from '@/lib/desktop';
export const useKeyboardShortcuts = () => {
const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore();
@@ -83,10 +84,12 @@ export const useKeyboardShortcuts = () => {
if (hasModifier(e) && e.key.toLowerCase() === 'n') {
e.preventDefault();
const isVSCode = isVSCodeRuntime();
const autoWorktree = useConfigStore.getState().settingsAutoCreateWorktree;
// If autoWorktree is true: Cmd+N -> Worktree, Cmd+Shift+N -> Standard
// If autoWorktree is false: Cmd+N -> Standard, Cmd+Shift+N -> Worktree
const shouldCreateWorktree = autoWorktree ? !e.shiftKey : e.shiftKey;
// VS Code: always open standard session (no worktree support)
const shouldCreateWorktree = isVSCode ? false : (autoWorktree ? !e.shiftKey : e.shiftKey);
if (shouldCreateWorktree) {
// Create new session with auto-generated worktree