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
@@ -24,7 +24,7 @@ export const AIHighlightsBox: React.FC<AIHighlightsBoxProps> = ({
};
return (
<div className="space-y-2 rounded-xl border border-border/60 bg-background/60 px-3 py-2">
<div className="space-y-2 rounded-xl border border-border/60 bg-transparent px-3 py-2">
<div className="flex items-center justify-between gap-2">
<p className="typography-micro text-muted-foreground">AI highlights</p>
<Tooltip delayDuration={1000}>
@@ -96,14 +96,13 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
);
return (
<li>
<div
className="group flex items-center gap-2 px-3 py-1.5 hover:bg-sidebar/40 cursor-pointer"
role="button"
tabIndex={0}
onClick={onViewDiff}
onKeyDown={handleKeyDown}
>
<div
className="group flex items-center gap-2 px-3 py-1.5 hover:bg-sidebar/40 cursor-pointer"
role="button"
tabIndex={0}
onClick={onViewDiff}
onKeyDown={handleKeyDown}
>
<button
type="button"
onClick={handleToggleClick}
@@ -175,7 +174,6 @@ export const ChangeRow = React.memo<ChangeRowProps>(function ChangeRow({
</TooltipTrigger>
<TooltipContent sideOffset={8}>Revert changes</TooltipContent>
</Tooltip>
</div>
</li>
</div>
);
});
@@ -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>
@@ -45,7 +45,7 @@ export const CommitInput: React.FC<CommitInputProps> = ({
autoCapitalize={hasTouchInput ? 'sentences' : 'off'}
spellCheck={hasTouchInput ? true : false}
className={cn(
'rounded-lg bg-background/80 resize-none overflow-y-auto',
'rounded-lg bg-transparent resize-none overflow-y-auto',
disabled && 'opacity-50'
)}
style={{ minHeight: MIN_HEIGHT, maxHeight: MAX_HEIGHT }}
@@ -285,7 +285,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
if (useTwoRowHeader) {
return (
<header className="@container/git-header border-b border-border/40 px-3 py-2 bg-background">
<header className={`@container/git-header border-b border-border/40 px-3 py-2 ${isSidebarMode ? 'bg-transparent' : 'bg-background'}`}>
<div className="flex items-center justify-between gap-2 min-w-0">
<div className="min-w-0 flex-1">
{isWorktreeMode ? (
@@ -320,7 +320,7 @@ export const GitHeader: React.FC<GitHeaderProps> = ({
}
return (
<header className="@container/git-header flex items-center gap-2 border-b border-border/40 px-3 py-2 bg-background">
<header className={`@container/git-header flex items-center gap-2 border-b border-border/40 px-3 py-2 ${isSidebarMode ? 'bg-transparent' : 'bg-background'}`}>
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
{isWorktreeMode ? (
<WorktreeBranchDisplay
@@ -698,7 +698,7 @@ export const PullRequestSection: React.FC<{
</div>
) : null}
{run.output?.text ? (
<div className="rounded border border-border/40 bg-background/40 px-2 py-2 typography-micro text-muted-foreground whitespace-pre-wrap max-h-48 overflow-y-auto">
<div className="rounded border border-border/40 bg-transparent px-2 py-2 typography-micro text-muted-foreground whitespace-pre-wrap max-h-48 overflow-y-auto">
{run.output.text}
</div>
) : null}
@@ -776,7 +776,7 @@ export const PullRequestSection: React.FC<{
{step.conclusion ? <span className="ml-auto flex-shrink-0">{step.conclusion}</span> : null}
</button>
<CollapsibleContent>
<div className="ml-6 mt-1 rounded border border-border/40 bg-background/40 px-2 py-2 typography-micro text-muted-foreground space-y-1">
<div className="ml-6 mt-1 rounded border border-border/40 bg-transparent px-2 py-2 typography-micro text-muted-foreground space-y-1">
{typeof step.number === 'number' ? <div>Step: {step.number}</div> : null}
{step.status ? <div>Status: {step.status}</div> : null}
{step.conclusion ? <div>Conclusion: {step.conclusion}</div> : null}
@@ -1304,7 +1304,7 @@ export const PullRequestSection: React.FC<{
const containerClassName =
variant === 'framed'
? 'rounded-xl border border-border/60 bg-background/70 overflow-hidden'
? 'rounded-xl border border-border/60 bg-transparent overflow-hidden'
: 'border-0 bg-transparent rounded-none';
const headerClassName =
variant === 'framed'