feat(ui): redesign workspace shell with context panel, tabbed sidebars, and faster diff UX (#433)

* feat: tabbed right sidebar, context panel, floating diff comments

* fix: auto-close left sidebar when context panel opens

- Increase default context panel width from 520 to 600 pixels
- Increase sidebar minimum width from 200 to 300 pixels
- Replace collapsible component with custom button in diff view

* refactoring: rework sidebars, tabs, and file tree layout

- Rewrite AnimatedTabs as segment-style with sliding indicator
- Upgrade SidebarFilesTree to match FilesView features (context menus,
  git status, file icons, CRUD dialogs, fuzzy search ranking)
- Restructure FilesView header: tabs row + actions row, remove breadcrumbs
- Show relative path in context panel header, track active tab
- Allow left sidebar to stay open alongside context panel
- Hide diff/files tabs from header on desktop (mobile-only)
- Move chevron after group name in session sidebar
- Compact tab heights in right sidebar and git view
- Size PreviewToggleButton to match other action buttons
- Remove directory loading spinner from folder icons

* feat: add project icon and color customization

- Enable users to assign custom icons to projects
- Allow users to choose accent colors for projects
- Stabilize repo status UI during project switching

* feat: add scroll fade indicators to editor tabs

* style: reduce spacing and icon sizes in header

* style: adjust tab component padding from uniform to vertical-horizontal

* feat: Add session state indicators to project tabs

* feat: Enhance session status handling and improve UI responsiveness

* fix: preserve upstream tracking on branch rename

* fix: improve initial remote selection for pull requests

- Uses saved remote name from previous session when available
- Selects remote based on tracking branch when possible
- Falls back to origin or first available remote

* perf(diff): faster highlight, stable stacked scroll

- split/unified Pierre worker pools; prefer shiki-wasm
- align diff CSS line-height; disable scroll anchoring; drop WebKit compositing hacks
- harden stacked pin/align (cancel on user scroll/input); prevent overscroll
- make overlay scrollbar MutationObserver optional; disable for diff container

* feat: handle binary files in diff view

* fix: adjust project tabs layout and drag regions

* style: update drag overlay visual styling

* feat: enable number keys to switch projects in the sidebar

* fix: recognize octet-stream as text-based MIME type

* feat: add keyboard navigation to context panel

* feat: add session pinning to sidebar

- Pin important sessions to keep them at the top
- Pinned sessions persist across browser sessions

* refactor: move context usage display from chat input to header
This commit is contained in:
Bohdan Triapitsyn
2026-02-16 14:15:19 +02:00
committed by GitHub
parent 12606b9e53
commit 47c943b487
42 changed files with 4874 additions and 1163 deletions
@@ -3,6 +3,8 @@ import { Header } from './Header';
import { BottomTerminalDock } from './BottomTerminalDock';
import { Sidebar } from './Sidebar';
import { RightSidebar } from './RightSidebar';
import { RightSidebarTabs } from './RightSidebarTabs';
import { ContextPanel } from './ContextPanel';
import { ErrorBoundary } from '../ui/ErrorBoundary';
import { CommandPalette } from '../ui/CommandPalette';
import { HelpDialog } from '../ui/HelpDialog';
@@ -15,11 +17,31 @@ import { MultiRunLauncher } from '@/components/multirun';
import { useUIStore } from '@/stores/useUIStore';
import { useUpdateStore } from '@/stores/useUpdateStore';
import { useDeviceInfo } from '@/lib/device';
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
import { useEdgeSwipe } from '@/hooks/useEdgeSwipe';
import { cn } from '@/lib/utils';
import { ChatView, PlanView, GitView, DiffView, TerminalView, FilesView, SettingsView, SettingsWindow } from '@/components/views';
const normalizeDirectoryKey = (value: string): string => {
if (!value) return '';
const raw = value.replace(/\\/g, '/');
const hadUncPrefix = raw.startsWith('//');
let normalized = raw.replace(/\/+$/g, '');
normalized = normalized.replace(/\/+/g, '/');
if (hadUncPrefix && !normalized.startsWith('//')) {
normalized = `/${normalized}`;
}
if (normalized === '') {
return raw.startsWith('/') ? '/' : '';
}
return normalized;
};
export const MainLayout: React.FC = () => {
const RIGHT_SIDEBAR_AUTO_CLOSE_WIDTH = 1140;
const RIGHT_SIDEBAR_AUTO_OPEN_WIDTH = 1220;
@@ -42,8 +64,19 @@ export const MainLayout: React.FC = () => {
} = useUIStore();
const { isMobile } = useDeviceInfo();
const effectiveDirectory = useEffectiveDirectory() ?? '';
const directoryKey = React.useMemo(() => normalizeDirectoryKey(effectiveDirectory), [effectiveDirectory]);
const isContextPanelOpen = useUIStore((state) => {
if (!directoryKey) {
return false;
}
const panelState = state.contextPanelByDirectory[directoryKey];
return Boolean(panelState?.isOpen && panelState?.mode);
});
const setSidebarOpen = useUIStore((state) => state.setSidebarOpen);
const rightSidebarAutoClosedRef = React.useRef(false);
const bottomTerminalAutoClosedRef = React.useRef(false);
const leftSidebarAutoClosedByContextRef = React.useRef(false);
useEdgeSwipe({ enabled: true });
@@ -90,6 +123,22 @@ export const MainLayout: React.FC = () => {
};
}, []);
React.useEffect(() => {
if (isContextPanelOpen) {
const currentlyOpen = useUIStore.getState().isSidebarOpen;
if (currentlyOpen) {
setSidebarOpen(false);
leftSidebarAutoClosedByContextRef.current = true;
}
return;
}
if (leftSidebarAutoClosedByContextRef.current) {
setSidebarOpen(true);
leftSidebarAutoClosedByContextRef.current = false;
}
}, [isContextPanelOpen, setSidebarOpen]);
React.useEffect(() => {
if (typeof window === 'undefined') {
return;
@@ -508,22 +557,25 @@ export const MainLayout: React.FC = () => {
<Header />
<div className="flex flex-1 overflow-hidden">
<Sidebar isOpen={isSidebarOpen} isMobile={isMobile}>
<SessionSidebar />
<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">
<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 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>
)}
</main>
{secondaryView && (
<div className="absolute inset-0">
<ErrorBoundary>{secondaryView}</ErrorBoundary>
</div>
)}
</main>
<ContextPanel />
</div>
<RightSidebar isOpen={isRightSidebarOpen} isMobile={isMobile}>
<ErrorBoundary><GitView mode="sidebar" /></ErrorBoundary>
<ErrorBoundary><RightSidebarTabs /></ErrorBoundary>
</RightSidebar>
</div>
<BottomTerminalDock isOpen={isBottomTerminalOpen} isMobile={isMobile}>