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:
committed by
GitHub
co-authored by
Iuliia Ivashko
parent
359879153a
commit
321cc7252a
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user