feat: move projects to sidebar rail and speed up session switching (#506)

* feat: move projects from header tabs to left sidebar rail

* refactor: remove variant from git generation session context

* feat: implemented drandrop action for navrails

* refactor: improve sidebar drag-and-drop smoothness and remove floating overlay

* fix: prevent mobile nav rail touch from closing drawer and opening menu

* fix: hide header tabs on desktop

* fix: prevent session flicker when switching projects

* style: soften chat panel divider borders

* style: improve icon contrast across core navigation

* fix: stabilize session selection during project switching

* style: unify sidebar surfaces and transparent section layers

* style: remove UI shadows and keep only scroll shadow

* perf: speed up project switching with cached session loading

* perf: speed up Git changes view and background refresh

* perf: make session switching lighter and less aggressive

* perf: optimized sessions list loading while project switching

* perf: smooth chat rendering and reduce interaction spikes

* fix: restore reliable load older messages visibility
This commit is contained in:
Bohdan Triapitsyn
2026-02-25 14:32:23 +02:00
committed by GitHub
parent 4c69bccf56
commit 10851bd7ac
51 changed files with 1995 additions and 1399 deletions
+5 -154
View File
@@ -1,17 +1,11 @@
import React from 'react';
import { RiDownloadLine, RiInformationLine, RiQuestionLine, RiSettings3Line } from '@remixicon/react';
import { toast } from '@/components/ui';
import { cn } from '@/lib/utils';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { useUIStore } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { UpdateDialog } from '../ui/UpdateDialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
export const SIDEBAR_CONTENT_WIDTH = 264;
const SIDEBAR_MIN_WIDTH = 300;
export const SIDEBAR_CONTENT_WIDTH = 250;
const SIDEBAR_MIN_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 500;
const CHECK_FOR_UPDATES_EVENT = 'openchamber:check-for-updates';
interface SidebarProps {
isOpen: boolean;
@@ -20,70 +14,10 @@ interface SidebarProps {
}
export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children }) => {
const { sidebarWidth, setSidebarWidth, setSettingsDialogOpen, setAboutDialogOpen, toggleHelpDialog } = useUIStore();
const { sidebarWidth, setSidebarWidth } = useUIStore();
const [isResizing, setIsResizing] = React.useState(false);
const startXRef = React.useRef(0);
const startWidthRef = React.useRef(sidebarWidth || SIDEBAR_CONTENT_WIDTH);
const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false);
const updateStore = useUpdateStore();
const pendingMenuUpdateCheckRef = React.useRef(false);
const checkForUpdates = updateStore.checkForUpdates;
const { available, downloaded, checking } = updateStore;
const [isDesktopApp, setIsDesktopApp] = React.useState<boolean>(() => {
if (typeof window === 'undefined') {
return false;
}
return Boolean((window as unknown as { __TAURI__?: unknown }).__TAURI__);
});
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
setIsDesktopApp(Boolean((window as unknown as { __TAURI__?: unknown }).__TAURI__));
}, []);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const handleMenuUpdateCheck = () => {
if (!(window as unknown as { __TAURI__?: unknown }).__TAURI__) {
return;
}
pendingMenuUpdateCheckRef.current = true;
void checkForUpdates();
};
window.addEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener);
return () => {
window.removeEventListener(CHECK_FOR_UPDATES_EVENT, handleMenuUpdateCheck as EventListener);
};
}, [checkForUpdates]);
React.useEffect(() => {
if (!pendingMenuUpdateCheckRef.current) {
return;
}
if (checking) {
return;
}
if (available || downloaded) {
setUpdateDialogOpen(true);
} else {
toast.success('No updates available', {
description: 'You are running the latest version.',
});
}
pendingMenuUpdateCheckRef.current = false;
}, [available, downloaded, checking]);
React.useEffect(() => {
if (isMobile || !isResizing) {
@@ -119,7 +53,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
}, [isMobile, isResizing]);
if (isMobile) {
return null;
}
@@ -141,10 +74,8 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
return (
<aside
className={cn(
'relative flex h-full overflow-hidden border-r border-border',
isDesktopApp
? 'bg-[color:var(--sidebar-overlay-strong)] backdrop-blur supports-[backdrop-filter]:bg-[color:var(--sidebar-overlay-soft)]'
: 'bg-sidebar',
'relative flex h-full overflow-hidden border-r border-border/40',
'bg-sidebar/50',
isResizing ? 'transition-none' : 'transition-[width] duration-300 ease-in-out',
!isOpen && 'border-r-0'
)}
@@ -179,86 +110,6 @@ export const Sidebar: React.FC<SidebarProps> = ({ isOpen, isMobile, children })
<div className="flex-1 overflow-hidden">
<ErrorBoundary>{children}</ErrorBoundary>
</div>
<div className="flex-shrink-0 border-t border-border h-12 px-2 bg-sidebar">
<div className="flex h-full items-center justify-between gap-2">
<button
onClick={() => setSettingsDialogOpen(true)}
className={cn(
'flex h-8 items-center gap-2 rounded-md px-2',
'text-sm font-semibold text-sidebar-foreground/90',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
'transition-all duration-200'
)}
>
<RiSettings3Line className="h-4 w-4" />
<span>Settings</span>
</button>
<div className="flex items-center gap-1">
{(available || downloaded) ? (
<button
onClick={() => setUpdateDialogOpen(true)}
className={cn(
'flex items-center gap-1.5 rounded-md px-2 py-1',
'text-xs font-semibold',
'bg-primary/10 text-primary',
'hover:bg-primary/20',
'transition-colors'
)}
>
<RiDownloadLine className="h-3.5 w-3.5" />
<span>Update</span>
</button>
) : !isDesktopApp && (
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={() => setAboutDialogOpen(true)}
className={cn(
'flex h-8 w-8 items-center justify-center rounded-md',
'text-sidebar-foreground/70',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
'transition-all duration-200'
)}
>
<RiInformationLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top">About OpenChamber</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={toggleHelpDialog}
className={cn(
'flex h-8 w-8 items-center justify-center rounded-md',
'text-sidebar-foreground/70',
'hover:text-sidebar-foreground hover:bg-interactive-hover',
'transition-all duration-200'
)}
aria-label="Keyboard shortcuts"
>
<RiQuestionLine className="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top">Keyboard shortcuts</TooltipContent>
</Tooltip>
</div>
</div>
</div>
<UpdateDialog
open={updateDialogOpen}
onOpenChange={setUpdateDialogOpen}
info={updateStore.info}
downloading={updateStore.downloading}
downloaded={updateStore.downloaded}
progress={updateStore.progress}
error={updateStore.error}
onDownload={updateStore.downloadUpdate}
onRestart={updateStore.restartToUpdate}
runtimeType={updateStore.runtimeType}
/>
</div>
</aside>
);