Major UI refresh: sidebar redesign, theme expansion, and chat performance optimizations (#706)
## Summary Complete sidebar redesign and comprehensive UI polish pass with performance optimizations, theme system refinements, and desktop integration improvements. ## Key Changes **Sidebar & Navigation Redesign** - Redesigned sessions sidebar layout with unified button primitives - Added activity sections with project grouping and improved session organization - Refined sidebar corners, spacing, and visual hierarchy - Removed NavRail component in favor of streamlined sidebar - Stabilized sessions bar toggle position in fullscreen mode **Performance Optimizations** - Reduced chat streaming CPU usage and storage churn - Optimized task tool polling and live timers with debouncing - Prevented chat state races and reduced background request load - Debounced draft writes and coalesced session reloads - Optimized message store updates and turn tracking **Theme & Visual System** - Added theme-aware window corners (desktop) and border radius tokens - Introduced glassmorphism effects on desktop sidebar - Added backdrop blur to UI elements **Chat Experience** - Added session-based permission auto-accept toggle in chat input - Polished permission shield UX with improved icon sizing and spacing - Fixed chat scroll-to-bottom behavior and timeline tracking - Enhanced tool output display with better path label detection - Removed duplicate draft context details in chat header - Added text selection menu to chat messages **Git Improvements** - Refreshed git history visual design with cleaner dividers - Added remote removal action in sync selector - Stabilized git polling to prevent excessive requests - Improved tool output rendering for git operations **Settings & Panels** - Fixed mobile scrolling on settings pages - Made outside-click settings close instantly - Reduced settings load churn and CPU spikes - Improved services dropdown layout and spacing - Softened panel resize handles **Desktop Integration** - Synced macOS window theme with app theme - Restored window dragging in sidebar header zones - Fixed system window corners on macOS - Improved header session metadata and action controls **Button & Component Standardization** - Unified button primitives across all components - Standardized destructive action patterns - Removed unused button variants (button-large, button-small) - Aligned context tab close hit areas --------- Co-authored-by: Iuliia Ivashko <yulia.ivashko@gmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Iuliia Ivashko
parent
359879153a
commit
321cc7252a
@@ -2,9 +2,8 @@ import React, { useRef, useEffect } from 'react';
|
||||
import { motion, useMotionValue, animate } from 'motion/react';
|
||||
import { Header } from './Header';
|
||||
import { BottomTerminalDock } from './BottomTerminalDock';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { NavRail } from './NavRail';
|
||||
import { RightSidebar } from './RightSidebar';
|
||||
import { Sidebar, SIDEBAR_CONTENT_WIDTH } from './Sidebar';
|
||||
import { RightSidebar, RIGHT_SIDEBAR_CONTENT_WIDTH } from './RightSidebar';
|
||||
import { RightSidebarTabs } from './RightSidebarTabs';
|
||||
import { ContextPanel } from './ContextPanel';
|
||||
import { ErrorBoundary } from '../ui/ErrorBoundary';
|
||||
@@ -22,11 +21,16 @@ import { useUpdateStore } from '@/stores/useUpdateStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { isDesktopShell } from '@/lib/desktop';
|
||||
|
||||
import { ChatView, PlanView, GitView, DiffView, TerminalView, FilesView, SettingsView, SettingsWindow } from '@/components/views';
|
||||
|
||||
// Mobile drawer width as screen percentage
|
||||
const MOBILE_DRAWER_WIDTH_PERCENT = 85;
|
||||
const DESKTOP_SIDEBAR_MIN_WIDTH = 250;
|
||||
const DESKTOP_SIDEBAR_MAX_WIDTH = 500;
|
||||
const DESKTOP_RIGHT_SIDEBAR_MIN_WIDTH = 400;
|
||||
const DESKTOP_RIGHT_SIDEBAR_MAX_WIDTH = 860;
|
||||
|
||||
const normalizeDirectoryKey = (value: string): string => {
|
||||
if (!value) return '';
|
||||
@@ -69,6 +73,10 @@ export const MainLayout: React.FC = () => {
|
||||
} = useUIStore();
|
||||
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
|
||||
const sidebarWidth = useUIStore((state) => state.sidebarWidth);
|
||||
const rightSidebarWidth = useUIStore((state) => state.rightSidebarWidth);
|
||||
const [desktopRightSidebarActionsHost, setDesktopRightSidebarActionsHost] = React.useState<HTMLDivElement | null>(null);
|
||||
const effectiveDirectory = useEffectiveDirectory() ?? '';
|
||||
const directoryKey = React.useMemo(() => normalizeDirectoryKey(effectiveDirectory), [effectiveDirectory]);
|
||||
const isContextPanelOpen = useUIStore((state) => {
|
||||
@@ -590,6 +598,14 @@ export const MainLayout: React.FC = () => {
|
||||
}, [activeMainTab]);
|
||||
|
||||
const isChatActive = activeMainTab === 'chat';
|
||||
const visibleSidebarWidth = React.useMemo(() => {
|
||||
const rawWidth = sidebarWidth || SIDEBAR_CONTENT_WIDTH;
|
||||
return Math.min(DESKTOP_SIDEBAR_MAX_WIDTH, Math.max(DESKTOP_SIDEBAR_MIN_WIDTH, rawWidth));
|
||||
}, [sidebarWidth]);
|
||||
const visibleRightSidebarWidth = React.useMemo(() => {
|
||||
const rawWidth = rightSidebarWidth || RIGHT_SIDEBAR_CONTENT_WIDTH;
|
||||
return Math.min(DESKTOP_RIGHT_SIDEBAR_MAX_WIDTH, Math.max(DESKTOP_RIGHT_SIDEBAR_MIN_WIDTH, rawWidth));
|
||||
}, [rightSidebarWidth]);
|
||||
|
||||
return (
|
||||
<DiffWorkerProvider>
|
||||
@@ -597,7 +613,7 @@ export const MainLayout: React.FC = () => {
|
||||
className={cn(
|
||||
'main-content-safe-area h-[100dvh]',
|
||||
isMobile ? 'flex flex-col' : 'flex',
|
||||
'bg-background'
|
||||
isDesktopShellRuntime ? 'bg-transparent' : 'bg-background'
|
||||
)}
|
||||
>
|
||||
<CommandPalette />
|
||||
@@ -654,7 +670,7 @@ export const MainLayout: React.FC = () => {
|
||||
opacity: mobileLeftDrawerOpen || isRightSidebarOpen ? 1 : 0,
|
||||
pointerEvents: mobileLeftDrawerOpen || isRightSidebarOpen ? 'auto' : 'none',
|
||||
}}
|
||||
className="fixed inset-0 z-40 bg-black/50 cursor-default"
|
||||
className="fixed left-0 right-0 bottom-0 top-[var(--oc-header-height,56px)] z-40 bg-black/50 cursor-default"
|
||||
onClick={() => {
|
||||
setMobileLeftDrawerOpen(false);
|
||||
setRightSidebarOpen(false);
|
||||
@@ -696,15 +712,15 @@ export const MainLayout: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-50 h-full bg-transparent',
|
||||
'fixed left-0 top-[var(--oc-header-height,56px)] z-50 h-[calc(100%-var(--oc-header-height,56px))] bg-transparent',
|
||||
'cursor-grab active:cursor-grabbing'
|
||||
)}
|
||||
aria-hidden={!mobileLeftDrawerOpen}
|
||||
>
|
||||
<div className="h-full overflow-hidden flex bg-sidebar shadow-none drawer-safe-area">
|
||||
<div onPointerDownCapture={(e) => e.stopPropagation()}>
|
||||
<NavRail className="shrink-0" mobile />
|
||||
</div>
|
||||
<div
|
||||
className="h-full overflow-hidden flex bg-[var(--surface-background)] shadow-none drawer-safe-area"
|
||||
style={{ backgroundImage: 'linear-gradient(var(--surface-muted), var(--surface-muted))' }}
|
||||
>
|
||||
<div className="flex-1 min-w-0 overflow-hidden flex flex-col">
|
||||
<ErrorBoundary>
|
||||
<SessionSidebar mobileVariant />
|
||||
@@ -747,7 +763,7 @@ export const MainLayout: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'fixed right-0 top-0 z-50 h-full bg-transparent',
|
||||
'fixed right-0 top-[var(--oc-header-height,56px)] z-50 h-[calc(100%-var(--oc-header-height,56px))] bg-transparent',
|
||||
'cursor-grab active:cursor-grabbing'
|
||||
)}
|
||||
aria-hidden={!isRightSidebarOpen}
|
||||
@@ -765,7 +781,6 @@ export const MainLayout: React.FC = () => {
|
||||
'flex flex-1 overflow-hidden relative',
|
||||
(isSettingsDialogOpen || isMultiRunLauncherOpen) && 'hidden'
|
||||
)}
|
||||
style={{ paddingTop: 'var(--oc-header-height, 56px)' }}
|
||||
>
|
||||
<main className="w-full h-full overflow-hidden bg-background relative">
|
||||
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
|
||||
@@ -781,7 +796,10 @@ export const MainLayout: React.FC = () => {
|
||||
|
||||
{/* Mobile multi-run launcher: full screen */}
|
||||
{isMultiRunLauncherOpen && (
|
||||
<div className="absolute inset-0 z-10 bg-background header-safe-area">
|
||||
<div
|
||||
className="absolute inset-0 z-10 bg-background"
|
||||
style={{ paddingTop: 'var(--oc-safe-area-top, 0px)' }}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<MultiRunLauncher
|
||||
initialPrompt={multiRunLauncherPrefillPrompt}
|
||||
@@ -794,49 +812,143 @@ export const MainLayout: React.FC = () => {
|
||||
|
||||
{/* Mobile settings: full screen */}
|
||||
{isSettingsDialogOpen && (
|
||||
<div className="absolute inset-0 z-10 bg-background header-safe-area">
|
||||
<div
|
||||
className="absolute inset-0 z-10 bg-background"
|
||||
style={{ paddingTop: 'var(--oc-safe-area-top, 0px)' }}
|
||||
>
|
||||
<ErrorBoundary><SettingsView onClose={() => setSettingsDialogOpen(false)} /></ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</DrawerProvider>
|
||||
) : (
|
||||
<>
|
||||
{/* Desktop: Header always on top, then Sidebar + Content below */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden relative">
|
||||
{/* Normal view: Header above Sidebar + content (like SettingsView) */}
|
||||
<div className={cn('absolute inset-0 flex flex-col', isMultiRunLauncherOpen && 'invisible')}>
|
||||
<Header />
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<NavRail />
|
||||
<div className="flex flex-1 min-w-0 overflow-hidden border-t border-l border-border/50 rounded-tl-xl">
|
||||
<Sidebar isOpen={isSidebarOpen} isMobile={isMobile}>
|
||||
<SessionSidebar hideProjectSelector />
|
||||
</Sidebar>
|
||||
<div className="flex flex-1 min-w-0 flex-col overflow-hidden">
|
||||
<div className="flex flex-1 min-h-0 overflow-hidden">
|
||||
<div className="relative flex flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
<main className="flex-1 overflow-hidden bg-background relative">
|
||||
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
|
||||
<ErrorBoundary><ChatView /></ErrorBoundary>
|
||||
{/* Desktop: Sidebar is a left column; header belongs to content column */}
|
||||
<div className="flex flex-1 overflow-hidden relative">
|
||||
<div className={cn(
|
||||
'absolute inset-0 flex overflow-hidden',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar',
|
||||
isMultiRunLauncherOpen && 'invisible'
|
||||
)}>
|
||||
{isSidebarOpen ? (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute top-0 z-0',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar'
|
||||
)}
|
||||
style={{
|
||||
left: `${visibleSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 100% 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
maskImage: 'radial-gradient(circle at 100% 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute bottom-0 z-0',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar'
|
||||
)}
|
||||
style={{
|
||||
left: `${visibleSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 100% 0%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
maskImage: 'radial-gradient(circle at 100% 0%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{isRightSidebarOpen ? (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute top-0 z-0',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar'
|
||||
)}
|
||||
style={{
|
||||
right: `${visibleRightSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 0 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
maskImage: 'radial-gradient(circle at 0 100%, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'pointer-events-none absolute bottom-0 z-0',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar'
|
||||
)}
|
||||
style={{
|
||||
right: `${visibleRightSidebarWidth}px`,
|
||||
width: 'var(--radius-md)',
|
||||
height: 'var(--radius-md)',
|
||||
WebkitMaskImage: 'radial-gradient(circle at 0 0, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
maskImage: 'radial-gradient(circle at 0 0, transparent calc(var(--radius-md) - 1px), black var(--radius-md))',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<Sidebar
|
||||
isOpen={isSidebarOpen}
|
||||
isMobile={isMobile}
|
||||
className="border-0"
|
||||
>
|
||||
<SessionSidebar />
|
||||
</Sidebar>
|
||||
<div className={cn(
|
||||
'relative flex flex-1 min-w-0 flex-col overflow-hidden',
|
||||
isDesktopShellRuntime
|
||||
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
|
||||
: 'bg-sidebar',
|
||||
isSidebarOpen && 'border-l border-border/50 rounded-tl-md rounded-bl-md',
|
||||
isRightSidebarOpen && 'border-r border-border/50 rounded-tr-md rounded-br-md'
|
||||
)}>
|
||||
<Header desktopRightSidebarActionsHost={desktopRightSidebarActionsHost} />
|
||||
<div className={cn(
|
||||
'flex flex-1 min-h-0 overflow-hidden',
|
||||
isSidebarOpen || isChatActive ? '' : 'border-l border-border/50',
|
||||
isRightSidebarOpen ? '' : 'border-r border-border/50'
|
||||
)}>
|
||||
<div className="relative flex flex-1 min-h-0 min-w-0 overflow-hidden">
|
||||
<main className="flex-1 overflow-hidden bg-background relative">
|
||||
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
|
||||
<ErrorBoundary><ChatView /></ErrorBoundary>
|
||||
</div>
|
||||
{secondaryView && (
|
||||
<div className="absolute inset-0">
|
||||
<ErrorBoundary>{secondaryView}</ErrorBoundary>
|
||||
</div>
|
||||
{secondaryView && (
|
||||
<div className="absolute inset-0">
|
||||
<ErrorBoundary>{secondaryView}</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
<ContextPanel />
|
||||
</div>
|
||||
<RightSidebar isOpen={isRightSidebarOpen}>
|
||||
<ErrorBoundary><RightSidebarTabs /></ErrorBoundary>
|
||||
</RightSidebar>
|
||||
)}
|
||||
</main>
|
||||
<ContextPanel />
|
||||
</div>
|
||||
<BottomTerminalDock isOpen={isBottomTerminalOpen} isMobile={isMobile}>
|
||||
<ErrorBoundary><TerminalView /></ErrorBoundary>
|
||||
</BottomTerminalDock>
|
||||
</div>
|
||||
</div>
|
||||
<BottomTerminalDock isOpen={isBottomTerminalOpen} isMobile={isMobile}>
|
||||
<ErrorBoundary><TerminalView /></ErrorBoundary>
|
||||
</BottomTerminalDock>
|
||||
</div>
|
||||
<RightSidebar
|
||||
isOpen={isRightSidebarOpen}
|
||||
className="border-0"
|
||||
onTopActionsHostChange={setDesktopRightSidebarActionsHost}
|
||||
>
|
||||
<ErrorBoundary><RightSidebarTabs /></ErrorBoundary>
|
||||
</RightSidebar>
|
||||
</div>
|
||||
|
||||
{/* Multi-Run Launcher: replaces tabs content only */}
|
||||
|
||||
Reference in New Issue
Block a user