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
+166 -16
View File
@@ -17,10 +17,12 @@ import { useDirectoryStore } from "./useDirectoryStore";
import { useConfigStore } from "./useConfigStore";
import { useProjectsStore } from "./useProjectsStore";
import { useSessionFoldersStore } from "./useSessionFoldersStore";
import { getSafeStorage } from "./utils/safeStorage";
import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta";
import { markPendingUserSendAnimation } from "@/lib/userSendAnimation";
import { flattenAssistantTextParts } from "@/lib/messages/messageText";
import { normalizeMessageRecordsForProjection } from "./utils/messageProjectors";
import type { ProjectEntry } from "@/lib/api/types";
export type { AttachedFile, EditPermissionMode };
export { MEMORY_LIMITS, ACTIVE_SESSION_WINDOW } from "./types/sessionTypes";
@@ -47,6 +49,62 @@ const normalizePath = (value?: string | null): string | null => {
};
const sessionChoiceAnalysisSignature = new Map<string, string>();
const DRAFT_TARGET_STORAGE_KEY = "oc.chatInput.lastDraftTarget";
type PersistedDraftTarget = {
projectId: string | null;
directory: string | null;
};
const safeStorage = getSafeStorage();
const readPersistedDraftTarget = (): PersistedDraftTarget | null => {
try {
const raw = safeStorage.getItem(DRAFT_TARGET_STORAGE_KEY);
if (!raw) {
return null;
}
const parsed = JSON.parse(raw) as { projectId?: unknown; directory?: unknown };
return {
projectId: typeof parsed?.projectId === "string" ? parsed.projectId : null,
directory: normalizePath(typeof parsed?.directory === "string" ? parsed.directory : null),
};
} catch {
return null;
}
};
const persistDraftTarget = (target: PersistedDraftTarget): void => {
try {
safeStorage.setItem(DRAFT_TARGET_STORAGE_KEY, JSON.stringify(target));
} catch {
// ignored
}
};
const resolveProjectForDirectory = (projects: ProjectEntry[], directory: string | null): ProjectEntry | null => {
const normalizedDirectory = normalizePath(directory);
if (!normalizedDirectory) {
return null;
}
let bestMatch: ProjectEntry | null = null;
for (const project of projects) {
const projectPath = normalizePath(project.path);
if (!projectPath) {
continue;
}
const isExact = normalizedDirectory === projectPath;
const isNested = normalizedDirectory.startsWith(`${projectPath}/`);
if (!isExact && !isNested) {
continue;
}
if (!bestMatch || projectPath.length > (normalizePath(bestMatch.path)?.length ?? 0)) {
bestMatch = project;
}
}
return bestMatch;
};
const buildSessionChoiceAnalysisSignature = (messages: Array<{ info: Message; parts: Part[] }>): string => {
const lastMessage = messages[messages.length - 1];
@@ -120,7 +178,7 @@ export const useSessionStore = create<SessionStore>()(
pendingInputText: null,
pendingInputMode: 'replace',
pendingSyntheticParts: null,
newSessionDraft: { open: true, directoryOverride: null, parentID: null },
newSessionDraft: { open: true, selectedProjectId: null, directoryOverride: null, parentID: null },
// Voice state (initialized to disconnected/idle)
voiceStatus: 'disconnected',
@@ -149,18 +207,71 @@ export const useSessionStore = create<SessionStore>()(
loadSessions: () => useSessionManagementStore.getState().loadSessions(),
openNewSessionDraft: (options) => {
// Use explicit directoryOverride if provided, otherwise use active project path
let directory: string | null = null;
if (options?.directoryOverride !== undefined) {
directory = options.directoryOverride;
} else {
const activeProject = useProjectsStore.getState().getActiveProject();
directory = activeProject?.path ?? null;
}
const projectsState = useProjectsStore.getState();
const projects = projectsState.projects;
const activeProject = projectsState.getActiveProject();
const currentDirectory = normalizePath(useDirectoryStore.getState().currentDirectory ?? null);
const persistedTarget = readPersistedDraftTarget();
const explicitDirectory = options?.directoryOverride !== undefined
? normalizePath(options.directoryOverride)
: null;
const explicitProject = options?.projectId
? projects.find((project) => project.id === options.projectId) ?? null
: null;
const inferredProjectFromDirectory = resolveProjectForDirectory(projects, explicitDirectory);
const fallbackProject = (() => {
if (activeProject) {
return activeProject;
}
if (projectsState.activeProjectId) {
return projects.find((project) => project.id === projectsState.activeProjectId) ?? null;
}
return projects[0] ?? null;
})();
const persistedProjectById = persistedTarget?.projectId
? projects.find((project) => project.id === persistedTarget.projectId) ?? null
: null;
const persistedProjectByDirectory = resolveProjectForDirectory(projects, persistedTarget?.directory ?? null);
const currentDirectoryProject = resolveProjectForDirectory(projects, currentDirectory);
const selectedProject = (() => {
if (explicitProject || explicitDirectory !== null) {
return explicitProject ?? inferredProjectFromDirectory ?? fallbackProject;
}
if (currentDirectory) {
return currentDirectoryProject ?? fallbackProject;
}
return persistedProjectByDirectory ?? persistedProjectById ?? fallbackProject;
})();
const directory = (() => {
if (explicitDirectory !== null) {
return explicitDirectory;
}
if (explicitProject) {
return normalizePath(explicitProject.path ?? null);
}
if (currentDirectory) {
return currentDirectory;
}
if (persistedTarget?.directory) {
return persistedTarget.directory;
}
return normalizePath(selectedProject?.path ?? null);
})();
persistDraftTarget({
projectId: selectedProject?.id ?? null,
directory,
});
set({
newSessionDraft: {
open: true,
selectedProjectId: selectedProject?.id ?? null,
directoryOverride: directory,
parentID: options?.parentID ?? null,
title: options?.title,
@@ -200,10 +311,39 @@ export const useSessionStore = create<SessionStore>()(
}
},
setNewSessionDraftTarget: ({ projectId, directoryOverride }) => {
const projects = useProjectsStore.getState().projects;
const project = projectId
? projects.find((entry) => entry.id === projectId) ?? null
: null;
const normalizedDirectory = normalizePath(directoryOverride);
const normalizedProjectPath = normalizePath(project?.path ?? null);
const nextDirectory = normalizedDirectory ?? normalizedProjectPath ?? null;
set((state) => {
if (!state.newSessionDraft?.open) {
return state;
}
return {
newSessionDraft: {
...state.newSessionDraft,
selectedProjectId: project?.id ?? null,
directoryOverride: nextDirectory,
parentID: null,
},
};
});
persistDraftTarget({
projectId: project?.id ?? null,
directory: nextDirectory,
});
},
closeNewSessionDraft: () => {
const realCurrentSessionId = useSessionManagementStore.getState().currentSessionId;
set({
newSessionDraft: { open: false, directoryOverride: null, parentID: null, title: undefined, initialPrompt: undefined, syntheticParts: undefined, targetFolderId: undefined },
newSessionDraft: { open: false, selectedProjectId: null, directoryOverride: null, parentID: null, title: undefined, initialPrompt: undefined, syntheticParts: undefined, targetFolderId: undefined },
currentSessionId: realCurrentSessionId,
});
},
@@ -375,6 +515,7 @@ export const useSessionStore = create<SessionStore>()(
if (draft?.open) {
const draftTargetFolderId = draft.targetFolderId;
const draftDirectoryOverride = draft.directoryOverride ?? null;
const draftProjectId = draft.selectedProjectId ?? null;
const created = await useSessionManagementStore
.getState()
@@ -384,6 +525,11 @@ export const useSessionStore = create<SessionStore>()(
throw new Error('Failed to create session');
}
persistDraftTarget({
projectId: draftProjectId,
directory: normalizePath(draftDirectoryOverride ?? created.directory ?? null),
});
const configState = useConfigStore.getState();
const draftAgentName = configState.currentAgentName;
const effectiveDraftAgent = trimmedAgent ?? draftAgentName;
@@ -560,12 +706,7 @@ export const useSessionStore = create<SessionStore>()(
updateMessageInfo: (sessionId: string, messageId: string, messageInfo: Record<string, unknown>) => useMessageStore.getState().updateMessageInfo(sessionId, messageId, messageInfo),
updateSessionCompaction: (sessionId: string, compactingTimestamp?: number | null) => useMessageStore.getState().updateSessionCompaction(sessionId, compactingTimestamp ?? null),
addPermission: (permission: PermissionRequest) => {
const contextData = {
currentAgentContext: useContextStore.getState().currentAgentContext,
sessionAgentSelections: useContextStore.getState().sessionAgentSelections,
getSessionAgentEditMode: useContextStore.getState().getSessionAgentEditMode,
};
return usePermissionStore.getState().addPermission(permission, contextData);
return usePermissionStore.getState().addPermission(permission);
},
respondToPermission: (sessionId: string, requestId: string, response: PermissionResponse) => usePermissionStore.getState().respondToPermission(sessionId, requestId, response),
dismissPermission: (sessionId: string, requestId: string) => usePermissionStore.getState().dismissPermission(sessionId, requestId),
@@ -1067,13 +1208,22 @@ useDirectoryStore.subscribe((state, prevState) => {
return;
}
const projects = useProjectsStore.getState().projects;
const resolvedProject = resolveProjectForDirectory(projects, nextDirectory);
useSessionStore.setState((store) => ({
newSessionDraft: {
...store.newSessionDraft,
selectedProjectId: resolvedProject?.id ?? store.newSessionDraft.selectedProjectId ?? null,
directoryOverride: nextDirectory,
parentID: null,
},
}));
persistDraftTarget({
projectId: resolvedProject?.id ?? draft.selectedProjectId ?? null,
directory: nextDirectory,
});
});
const bootDraftOpen = useSessionStore.getState().newSessionDraft?.open;