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
+58 -17
View File
@@ -82,6 +82,10 @@ const readSessionSelectionMap = (): SessionSelectionMap => {
let sessionSelectionCache: SessionSelectionMap | null = null;
let loadSessionsRequestSeq = 0;
let loadSessionsInFlight: Promise<void> | null = null;
let loadSessionsQueued = false;
let persistSelectionTimer: ReturnType<typeof setTimeout> | undefined;
let pendingSelectionMap: SessionSelectionMap | null = null;
type ProjectSessionResult = {
projectId: string;
@@ -152,11 +156,28 @@ const getSessionSelectionMap = (): SessionSelectionMap => {
const persistSessionSelectionMap = (map: SessionSelectionMap) => {
sessionSelectionCache = map;
try {
safeStorage.setItem(SESSION_SELECTION_STORAGE_KEY, JSON.stringify(map));
} catch { /* ignored */ }
pendingSelectionMap = map;
clearTimeout(persistSelectionTimer);
persistSelectionTimer = setTimeout(() => {
try {
safeStorage.setItem(SESSION_SELECTION_STORAGE_KEY, JSON.stringify(map));
pendingSelectionMap = null;
} catch { /* ignored */ }
}, 300);
};
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
if (pendingSelectionMap !== null) {
clearTimeout(persistSelectionTimer);
try {
safeStorage.setItem(SESSION_SELECTION_STORAGE_KEY, JSON.stringify(pendingSelectionMap));
} catch { /* ignored */ }
pendingSelectionMap = null;
}
});
}
const getStoredSessionForDirectory = (directory: string | null | undefined): string | null => {
if (!directory) {
return null;
@@ -463,10 +484,16 @@ export const useSessionStore = create<SessionStore>()(
availableWorktreesByProject: new Map(),
loadSessions: async () => {
const requestSeq = ++loadSessionsRequestSeq;
const isLatestRequest = () => requestSeq === loadSessionsRequestSeq;
set({ isLoading: true, error: null });
try {
if (loadSessionsInFlight) {
loadSessionsQueued = true;
return loadSessionsInFlight;
}
const task = (async () => {
const requestSeq = ++loadSessionsRequestSeq;
const isLatestRequest = () => requestSeq === loadSessionsRequestSeq;
set({ isLoading: true, error: null });
try {
const directoryStore = useDirectoryStore.getState();
const projectsStore = useProjectsStore.getState();
const apiClient = opencodeClient.getApiClient();
@@ -809,14 +836,28 @@ export const useSessionStore = create<SessionStore>()(
const fallbackSessions = dedupeSessionsById(Array.isArray(fallbackResponse.data) ? fallbackResponse.data : []);
const fallbackProjectResults = await buildProjectResults(fallbackSessions);
await applyProjectResults(fallbackProjectResults, []);
} catch (error) {
if (!isLatestRequest()) {
return;
} catch (error) {
if (!isLatestRequest()) {
return;
}
set({
error: error instanceof Error ? error.message : "Failed to load sessions",
isLoading: false,
});
}
})();
loadSessionsInFlight = task;
try {
await task;
} finally {
if (loadSessionsInFlight === task) {
loadSessionsInFlight = null;
}
if (loadSessionsQueued) {
loadSessionsQueued = false;
void get().loadSessions();
}
set({
error: error instanceof Error ? error.message : "Failed to load sessions",
isLoading: false,
});
}
},
@@ -1464,9 +1505,9 @@ export const useSessionStore = create<SessionStore>()(
mergedSession.directory !== existingSession.directory ||
mergedSession.version !== existingSession.version ||
mergedSession.projectID !== existingSession.projectID ||
JSON.stringify(mergedSession.time) !== JSON.stringify(existingSession.time) ||
JSON.stringify(mergedSession.summary ?? null) !== JSON.stringify(existingSession.summary ?? null) ||
JSON.stringify(mergedSession.share ?? null) !== JSON.stringify(existingSession.share ?? null);
(mergedTime !== existingSession.time && JSON.stringify(mergedTime) !== JSON.stringify(existingSession.time)) ||
(mergedSummary !== existingSession.summary && JSON.stringify(mergedSummary ?? null) !== JSON.stringify(existingSession.summary ?? null)) ||
(mergedShare !== existingSession.share && JSON.stringify(mergedShare ?? null) !== JSON.stringify(existingSession.share ?? null));
const sessions = [...state.sessions];
sessions[index] = hasChanged ? mergedSession : existingSession;