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:
Bohdan Triapitsyn
2026-03-20 01:01:03 +02:00
committed by GitHub
co-authored by Iuliia Ivashko
parent 359879153a
commit 321cc7252a
222 changed files with 8575 additions and 5456 deletions
@@ -4,13 +4,11 @@ import type { SessionGroup } from '../types';
export const useGroupOrdering = (groupOrderByProject: Map<string, string[]>) => {
const getOrderedGroups = React.useCallback(
(projectId: string, groups: SessionGroup[]) => {
const archivedGroup = groups.find((group) => group.isArchivedBucket === true) ?? null;
const reorderableGroups = archivedGroup ? groups.filter((group) => group !== archivedGroup) : groups;
const preferredOrder = groupOrderByProject.get(projectId);
if (!preferredOrder || preferredOrder.length === 0) {
return archivedGroup ? [...reorderableGroups, archivedGroup] : reorderableGroups;
return groups;
}
const groupById = new Map(reorderableGroups.map((group) => [group.id, group]));
const groupById = new Map(groups.map((group) => [group.id, group]));
const ordered: SessionGroup[] = [];
preferredOrder.forEach((id) => {
const group = groupById.get(id);
@@ -19,12 +17,12 @@ export const useGroupOrdering = (groupOrderByProject: Map<string, string[]>) =>
groupById.delete(id);
}
});
reorderableGroups.forEach((group) => {
groups.forEach((group) => {
if (groupById.has(group.id)) {
ordered.push(group);
}
});
return archivedGroup ? [...ordered, archivedGroup] : ordered;
return ordered;
},
[groupOrderByProject],
);
@@ -15,7 +15,6 @@ type Args = {
setActiveSessionByProject: React.Dispatch<React.SetStateAction<Map<string, string>>>;
currentSessionId: string | null;
handleSessionSelect: (sessionId: string, sessionDirectory: string | null, isMissingDirectory: boolean, projectId?: string | null) => void;
isVSCode: boolean;
newSessionDraftOpen: boolean;
mobileVariant: boolean;
openNewSessionDraft: (options?: { directoryOverride?: string | null }) => void;
@@ -33,7 +32,6 @@ export const useProjectSessionSelection = (args: Args): { currentSessionDirector
setActiveSessionByProject,
currentSessionId,
handleSessionSelect,
isVSCode,
newSessionDraftOpen,
mobileVariant,
openNewSessionDraft,
@@ -84,18 +82,13 @@ export const useProjectSessionSelection = (args: Args): { currentSessionDirector
}, [projectSections]);
const previousActiveProjectRef = React.useRef<string | null>(null);
const lastSeenActiveProjectRef = React.useRef<string | null>(null);
React.useLayoutEffect(() => {
if (!activeProjectId) {
return;
}
const previousSeenProjectId = lastSeenActiveProjectRef.current;
const isProjectSwitch = Boolean(previousSeenProjectId && previousSeenProjectId !== activeProjectId);
lastSeenActiveProjectRef.current = activeProjectId;
if (newSessionDraftOpen && (isVSCode || !isProjectSwitch)) {
if (newSessionDraftOpen) {
return;
}
@@ -146,7 +139,6 @@ export const useProjectSessionSelection = (args: Args): { currentSessionDirector
activeSessionByProject,
currentSessionId,
handleSessionSelect,
isVSCode,
newSessionDraftOpen,
mobileVariant,
openNewSessionDraft,
@@ -83,6 +83,11 @@ export const useSessionActions = (args: Args) => {
if (sessionId === args.currentSessionId) {
if (args.allowReselect) {
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent<string>('openchamber:session-reselected', {
detail: sessionId,
}));
}
args.onSessionSelected?.(sessionId);
}
resetSessionSearch();
@@ -19,6 +19,8 @@ type Args = {
isVSCode: boolean;
};
const isArchivedSession = (session: Session): boolean => Boolean(session.time?.archived);
export const useSessionGrouping = (args: Args) => {
const buildGroupSearchText = React.useCallback((group: SessionGroup): string => {
return [group.label, group.branch ?? '', group.description ?? '', group.directory ?? ''].join(' ').toLowerCase();
@@ -70,6 +72,10 @@ export const useSessionGrouping = (args: Args) => {
sortedProjectSessions.forEach((session) => {
const parentID = (session as Session & { parentID?: string | null }).parentID;
if (!parentID) return;
const parentSession = sessionMap.get(parentID);
if (!parentSession || isArchivedSession(parentSession) !== isArchivedSession(session)) {
return;
}
const collection = childrenMap.get(parentID) ?? [];
collection.push(session);
childrenMap.set(parentID, collection);
@@ -105,7 +111,9 @@ export const useSessionGrouping = (args: Args) => {
const roots = sortedProjectSessions.filter((session) => {
const parentID = (session as Session & { parentID?: string | null }).parentID;
if (!parentID) return true;
return !sessionMap.has(parentID);
const parentSession = sessionMap.get(parentID);
if (!parentSession) return true;
return isArchivedSession(parentSession) !== isArchivedSession(session);
});
const groupedNodes = new Map<string, SessionNode[]>();
@@ -9,10 +9,11 @@ const SESSION_PREFETCH_PENDING_LIMIT = 6;
type Args = {
currentSessionId: string | null;
sortedSessions: Session[];
recentSessionIds?: string[];
loadMessages: (sessionId: string, limit?: number) => Promise<void>;
};
export const useSessionPrefetch = ({ currentSessionId, sortedSessions, loadMessages }: Args): void => {
export const useSessionPrefetch = ({ currentSessionId, sortedSessions, recentSessionIds = [], loadMessages }: Args): void => {
const sessionPrefetchTimersRef = React.useRef<Map<string, number>>(new Map());
const sessionPrefetchQueueRef = React.useRef<string[]>([]);
const sessionPrefetchInFlightRef = React.useRef<Set<string>>(new Set());
@@ -100,6 +101,20 @@ export const useSessionPrefetch = ({ currentSessionId, sortedSessions, loadMessa
scheduleSessionPrefetch(sortedSessions[currentIndex + 1]?.id);
}, [currentSessionId, scheduleSessionPrefetch, sortedSessions]);
React.useEffect(() => {
if (!currentSessionId || recentSessionIds.length === 0) {
return;
}
const currentIndex = recentSessionIds.indexOf(currentSessionId);
if (currentIndex < 0) {
return;
}
scheduleSessionPrefetch(recentSessionIds[currentIndex - 1]);
scheduleSessionPrefetch(recentSessionIds[currentIndex + 1]);
}, [currentSessionId, recentSessionIds, scheduleSessionPrefetch]);
React.useEffect(() => {
const prefetchTimers = sessionPrefetchTimersRef.current;
return () => {
@@ -9,6 +9,10 @@ type ProjectItem = {
path: string;
label?: string;
normalizedPath: string;
icon?: string;
color?: string;
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
iconBackground?: string;
};
type ProjectSection = {
@@ -18,7 +22,6 @@ type ProjectSection = {
type Args = {
normalizedProjects: ProjectItem[];
activeProjectId: string | null;
getSessionsForProject: (project: { normalizedPath: string }) => Session[];
getArchivedSessionsForProject: (project: { normalizedPath: string }) => Session[];
availableWorktreesByProject: Map<string, WorktreeMetadata[]>;
@@ -42,7 +45,6 @@ type Args = {
export const useSessionSidebarSections = (args: Args) => {
const {
normalizedProjects,
activeProjectId,
getSessionsForProject,
getArchivedSessionsForProject,
availableWorktreesByProject,
@@ -88,12 +90,8 @@ export const useSessionSidebarSections = (args: Args) => {
]);
const visibleProjectSections = React.useMemo(() => {
if (projectSections.length === 0) {
return projectSections;
}
const active = projectSections.find((section) => section.project.id === activeProjectId);
return active ? [active] : [projectSections[0]];
}, [projectSections, activeProjectId]);
return projectSections;
}, [projectSections]);
const groupSearchDataByGroup = React.useMemo(() => {
const result = new WeakMap<SessionGroup, GroupSearchData>();