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
@@ -1,4 +1,5 @@
import React from 'react';
import { useVirtualizer } from '@tanstack/react-virtual';
import { Button } from '@/components/ui/button';
import { ScrollShadow } from '@/components/ui/ScrollShadow';
import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar';
@@ -18,8 +19,12 @@ interface ChangesSectionProps {
onRevertFile: (path: string) => void;
variant?: 'framed' | 'plain';
maxListHeightClassName?: string;
onVisiblePathsChange?: (paths: string[]) => void;
}
const CHANGE_LIST_VIRTUALIZE_THRESHOLD = 120;
const CHANGE_ROW_ESTIMATE_PX = 34;
export const ChangesSection: React.FC<ChangesSectionProps> = ({
changeEntries,
selectedPaths,
@@ -32,10 +37,43 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
onRevertFile,
variant = 'framed',
maxListHeightClassName,
onVisiblePathsChange,
}) => {
const scrollRef = React.useRef<HTMLDivElement | null>(null);
const selectedCount = selectedPaths.size;
const totalCount = changeEntries.length;
const shouldVirtualize = totalCount >= CHANGE_LIST_VIRTUALIZE_THRESHOLD;
const rowVirtualizer = useVirtualizer({
count: totalCount,
getScrollElement: () => scrollRef.current,
estimateSize: () => CHANGE_ROW_ESTIMATE_PX,
overscan: 10,
enabled: shouldVirtualize,
});
const virtualRows = React.useMemo(
() => (shouldVirtualize ? rowVirtualizer.getVirtualItems() : []),
[rowVirtualizer, shouldVirtualize],
);
React.useEffect(() => {
if (!onVisiblePathsChange) {
return;
}
if (totalCount === 0) {
onVisiblePathsChange([]);
return;
}
if (!shouldVirtualize) {
onVisiblePathsChange(changeEntries.slice(0, Math.min(30, totalCount)).map((entry) => entry.path));
return;
}
onVisiblePathsChange(virtualRows.map((row) => changeEntries[row.index]?.path).filter((value): value is string => Boolean(value)));
}, [changeEntries, onVisiblePathsChange, shouldVirtualize, totalCount, virtualRows]);
const containerClassName =
variant === 'framed'
@@ -86,20 +124,54 @@ export const ChangesSection: React.FC<ChangesSectionProps> = ({
ref={scrollRef}
className="overlay-scrollbar-target overlay-scrollbar-container flex-1 min-h-0 w-full overflow-y-auto overflow-x-hidden"
>
<ul className="divide-y divide-border/60">
{changeEntries.map((file) => (
<ChangeRow
key={file.path}
file={file}
checked={selectedPaths.has(file.path)}
stats={diffStats?.[file.path]}
onToggle={() => onToggleFile(file.path)}
onViewDiff={() => onViewDiff(file.path)}
onRevert={() => onRevertFile(file.path)}
isReverting={revertingPaths.has(file.path)}
/>
))}
</ul>
{shouldVirtualize ? (
<div
className="relative w-full divide-y divide-border/60"
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}
>
{virtualRows.map((row) => {
const file = changeEntries[row.index];
if (!file) {
return null;
}
return (
<div
key={file.path}
ref={rowVirtualizer.measureElement}
data-index={row.index}
className="absolute left-0 top-0 w-full"
style={{ transform: `translateY(${row.start}px)` }}
>
<ChangeRow
file={file}
checked={selectedPaths.has(file.path)}
stats={diffStats?.[file.path]}
onToggle={() => onToggleFile(file.path)}
onViewDiff={() => onViewDiff(file.path)}
onRevert={() => onRevertFile(file.path)}
isReverting={revertingPaths.has(file.path)}
/>
</div>
);
})}
</div>
) : (
<div className="divide-y divide-border/60" role="list" aria-label="Changed files">
{changeEntries.map((file) => (
<ChangeRow
key={file.path}
file={file}
checked={selectedPaths.has(file.path)}
stats={diffStats?.[file.path]}
onToggle={() => onToggleFile(file.path)}
onViewDiff={() => onViewDiff(file.path)}
onRevert={() => onRevertFile(file.path)}
isReverting={revertingPaths.has(file.path)}
/>
))}
</div>
)}
</ScrollShadow>
<OverlayScrollbar containerRef={scrollRef} disableHorizontal />
</div>