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
@@ -1,6 +1,8 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import { RiLayoutLeftLine } from '@remixicon/react';
import { toast } from '@/components/ui';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell } from '@/lib/desktop';
import { MobileOverlayPanel } from '@/components/ui/MobileOverlayPanel';
import { sessionEvents } from '@/lib/sessionEvents';
@@ -9,10 +11,8 @@ import { useSessionStore } from '@/stores/useSessionStore';
import { useDirectoryStore } from '@/stores/useDirectoryStore';
import { useProjectsStore } from '@/stores/useProjectsStore';
import { useUIStore } from '@/stores/useUIStore';
import { useConfigStore } from '@/stores/useConfigStore';
import type { GitHubPullRequestStatus } from '@/lib/api/types';
import { getSafeStorage } from '@/stores/utils/safeStorage';
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
import { useGitStore } from '@/stores/useGitStore';
import { useDeviceInfo } from '@/lib/device';
import { isVSCodeRuntime } from '@/lib/desktop';
@@ -35,10 +35,14 @@ import { useProjectSessionLists } from './sidebar/hooks/useProjectSessionLists';
import { useSessionFolderCleanup } from './sidebar/hooks/useSessionFolderCleanup';
import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders';
import { useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
import { SessionGroupSection } from './sidebar/SessionGroupSection';
import { SidebarHeader } from './sidebar/SidebarHeader';
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
import { SidebarFooter } from './sidebar/SidebarFooter';
import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
import { SessionNodeItem } from './sidebar/SessionNodeItem';
import type { SortableDragHandleProps } from './sidebar/sortableItems';
import {
FolderDeleteConfirmDialog,
SessionDeleteConfirmDialog,
@@ -46,6 +50,13 @@ import {
type DeleteSessionConfirmState,
} from './sidebar/ConfirmDialogs';
import { type SessionGroup, type SessionNode } from './sidebar/types';
import {
addActiveNowSession,
deriveActiveNowSessions,
persistActiveNowEntries,
pruneActiveNowEntries,
readActiveNowEntries,
} from './sidebar/activitySections';
import {
compareSessionsByPinnedAndTime,
formatProjectLabel,
@@ -130,7 +141,6 @@ interface SessionSidebarProps {
onSessionSelected?: (sessionId: string) => void;
allowReselect?: boolean;
hideDirectoryControls?: boolean;
hideProjectSelector?: boolean;
showOnlyMainWorkspace?: boolean;
}
@@ -139,7 +149,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
onSessionSelected,
allowReselect = false,
hideDirectoryControls = false,
hideProjectSelector = true,
showOnlyMainWorkspace = false,
}) => {
const [isSessionSearchOpen, setIsSessionSearchOpen] = React.useState(false);
@@ -148,13 +157,13 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const sessionSearchInputRef = React.useRef<HTMLInputElement | null>(null);
const [editingId, setEditingId] = React.useState<string | null>(null);
const [editTitle, setEditTitle] = React.useState('');
const [editingProjectId, setEditingProjectId] = React.useState<string | null>(null);
const [editProjectTitle, setEditProjectTitle] = React.useState('');
const [editingProjectDialogId, setEditingProjectDialogId] = React.useState<string | null>(null);
const [expandedParents, setExpandedParents] = React.useState<Set<string>>(new Set());
const [directoryStatus, setDirectoryStatus] = React.useState<Map<string, 'unknown' | 'exists' | 'missing'>>(
() => new Map(),
);
const safeStorage = React.useMemo(() => getSafeStorage(), []);
const [activeNowEntries, setActiveNowEntries] = React.useState(() => readActiveNowEntries(safeStorage));
const [collapsedProjects, setCollapsedProjects] = React.useState<Set<string>>(new Set());
const [projectRepoStatus, setProjectRepoStatus] = React.useState<Map<string, boolean | null>>(new Map());
@@ -162,7 +171,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const [hoveredProjectId, setHoveredProjectId] = React.useState<string | null>(null);
const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false);
const [projectNotesPanelOpen, setProjectNotesPanelOpen] = React.useState(false);
const [openMenuSessionId, setOpenMenuSessionId] = React.useState<string | null>(null);
const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState<string | null>(null);
const [renamingFolderId, setRenamingFolderId] = React.useState<string | null>(null);
const [renameFolderDraft, setRenameFolderDraft] = React.useState('');
const [deleteSessionConfirm, setDeleteSessionConfirm] = React.useState<DeleteSessionConfirmState>(null);
@@ -228,8 +237,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}
});
const [isProjectRenameInline, setIsProjectRenameInline] = React.useState(false);
const [projectRenameDraft, setProjectRenameDraft] = React.useState('');
const [projectRootBranches, setProjectRootBranches] = React.useState<Map<string, string>>(new Map());
const projectHeaderSentinelRefs = React.useRef<Map<string, HTMLDivElement | null>>(new Map());
const ignoreIntersectionUntil = React.useRef<number>(0);
@@ -243,17 +250,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const addProject = useProjectsStore((state) => state.addProject);
const removeProject = useProjectsStore((state) => state.removeProject);
const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly);
const renameProject = useProjectsStore((state) => state.renameProject);
const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta);
const reorderProjects = useProjectsStore((state) => state.reorderProjects);
const setActiveMainTab = useUIStore((state) => state.setActiveMainTab);
const openContextPanelTab = useUIStore((state) => state.openContextPanelTab);
const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen);
const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog);
const setAboutDialogOpen = useUIStore((state) => state.setAboutDialogOpen);
const deviceInfo = useDeviceInfo();
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const toggleSidebar = useUIStore((state) => state.toggleSidebar);
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog);
const settingsAutoCreateWorktree = useConfigStore((state) => state.settingsAutoCreateWorktree);
const debouncedSessionSearchQuery = useDebouncedValue(sessionSearchQuery, 120);
const normalizedSessionSearchQuery = React.useMemo(
@@ -307,8 +318,89 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const tauriIpcAvailable = React.useMemo(() => isTauriShell(), []);
const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []);
const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false);
const isVSCode = React.useMemo(() => isVSCodeRuntime(), []);
const isMacPlatform = React.useMemo(() => {
if (typeof navigator === 'undefined') {
return false;
}
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
}, []);
const showDesktopSidebarChrome = !mobileVariant && !isVSCode;
const desktopSidebarTopPaddingClass = isDesktopShellRuntime && isMacPlatform && !isDesktopWindowFullscreen ? 'pl-[5.5rem]' : 'pl-3';
const desktopSidebarToggleButtonClass = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50';
React.useEffect(() => {
if (!isDesktopShellRuntime || !isMacPlatform) {
setIsDesktopWindowFullscreen(false);
return;
}
let disposed = false;
let unlistenResize: (() => void) | null = null;
const syncFullscreenState = async () => {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
const fullscreen = await currentWindow.isFullscreen();
if (!disposed) {
setIsDesktopWindowFullscreen(fullscreen);
}
} catch {
if (!disposed) {
setIsDesktopWindowFullscreen(false);
}
}
};
const attach = async () => {
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const currentWindow = getCurrentWindow();
unlistenResize = await currentWindow.onResized(() => {
void syncFullscreenState();
});
} catch {
// Ignore listener setup failures; fallback state remains false.
}
};
void syncFullscreenState();
void attach();
return () => {
disposed = true;
if (unlistenResize) {
unlistenResize();
}
};
}, [isDesktopShellRuntime, isMacPlatform]);
const handleDesktopSidebarDragStart = React.useCallback(async (event: React.MouseEvent) => {
const target = event.target as HTMLElement;
if (target.closest('.app-region-no-drag')) {
return;
}
if (target.closest('button, a, input, select, textarea')) {
return;
}
if (event.button !== 0) {
return;
}
if (!isDesktopShellRuntime) {
return;
}
try {
const { getCurrentWindow } = await import('@tauri-apps/api/window');
const appWindow = getCurrentWindow();
await appWindow.startDragging();
} catch (error) {
console.error('Failed to start window dragging:', error);
}
}, [isDesktopShellRuntime]);
const {
buildGroupSearchText,
@@ -359,11 +451,93 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
return [...sessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds));
}, [sessions, pinnedSessionIds]);
useSessionPrefetch({
currentSessionId,
sortedSessions,
loadMessages,
});
const allKnownSessionsById = React.useMemo(() => {
const next = new Map<string, Session>();
[...sessions, ...archivedSessions].forEach((session) => {
next.set(session.id, session);
});
return next;
}, [sessions, archivedSessions]);
React.useEffect(() => {
const pruned = pruneActiveNowEntries(activeNowEntries, allKnownSessionsById);
if (pruned.length === activeNowEntries.length && pruned.every((entry, index) => entry.sessionId === activeNowEntries[index]?.sessionId)) {
return;
}
setActiveNowEntries(pruned);
persistActiveNowEntries(safeStorage, pruned);
}, [activeNowEntries, allKnownSessionsById, safeStorage]);
const previousStreamingIdsRef = React.useRef<Set<string>>(new Set());
React.useEffect(() => {
const nextStreamingIds = new Set<string>();
sessionStatus?.forEach((status, sessionId) => {
if (status?.type === 'busy' || status?.type === 'retry') {
nextStreamingIds.add(sessionId);
}
});
const previousStreamingIds = previousStreamingIdsRef.current;
const startedStreamingIds = Array.from(nextStreamingIds).filter((sessionId) => !previousStreamingIds.has(sessionId));
if (startedStreamingIds.length > 0) {
setActiveNowEntries((prev) => {
const next = startedStreamingIds.reduce((entries, sessionId) => addActiveNowSession(entries, sessionId), prev);
if (next === prev) {
return prev;
}
persistActiveNowEntries(safeStorage, next);
return next;
});
}
previousStreamingIdsRef.current = nextStreamingIds;
}, [sessionStatus, safeStorage]);
React.useEffect(() => {
const busyIds: string[] = [];
sessionStatus?.forEach((status, sessionId) => {
if (status?.type === 'busy' || status?.type === 'retry') {
busyIds.push(sessionId);
}
});
if (busyIds.length === 0) {
return;
}
setActiveNowEntries((prev) => {
const known = new Set(prev.map((entry) => entry.sessionId));
let next = prev;
let changed = false;
busyIds.forEach((sessionId) => {
if (known.has(sessionId)) {
return;
}
const session = allKnownSessionsById.get(sessionId);
if (!session || session.time?.archived) {
return;
}
const isSubtask = Boolean((session as Session & { parentID?: string | null }).parentID);
if (isSubtask) {
return;
}
next = addActiveNowSession(next, sessionId);
known.add(sessionId);
changed = true;
});
if (!changed) {
return prev;
}
persistActiveNowEntries(safeStorage, next);
return next;
});
}, [sessionStatus, allKnownSessionsById, safeStorage]);
const childrenMap = React.useMemo(() => {
const map = new Map<string, Session[]>();
@@ -394,17 +568,21 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
</div>
);
const handleSaveProjectEdit = React.useCallback(() => {
if (editingProjectId && editProjectTitle.trim()) {
renameProject(editingProjectId, editProjectTitle.trim());
setEditingProjectId(null);
setEditProjectTitle('');
}
}, [editingProjectId, editProjectTitle, renameProject]);
const editingProject = React.useMemo(
() => projects.find((project) => project.id === editingProjectDialogId) ?? null,
[projects, editingProjectDialogId],
);
const handleCancelProjectEdit = React.useCallback(() => {
setEditingProjectId(null);
setEditProjectTitle('');
const handleSaveProjectEdit = React.useCallback((data: { label: string; icon: string | null; color: string | null; iconBackground: string | null }) => {
if (!editingProjectDialogId) {
return;
}
updateProjectMeta(editingProjectDialogId, data);
setEditingProjectDialogId(null);
}, [editingProjectDialogId, updateProjectMeta]);
const openNewWorktreeDialog = React.useCallback(() => {
setNewWorktreeDialogOpen(true);
}, []);
const deleteSession = useSessionStore((state) => state.deleteSession);
@@ -569,6 +747,10 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
path: string;
label?: string;
normalizedPath: string;
icon?: string;
color?: string;
iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' };
iconBackground?: string;
}>;
}, [projects]);
@@ -627,7 +809,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
searchMatchCount,
} = useSessionSidebarSections({
normalizedProjects,
activeProjectId,
getSessionsForProject,
getArchivedSessionsForProject,
availableWorktreesByProject,
@@ -671,7 +852,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
const stableActiveProjectIsRepo = activeProjectForHeader && projectRepoStatus.has(activeProjectForHeader.id)
? activeProjectIsRepo
: lastRepoStatusRef.current;
const reserveHeaderActionsSpace = Boolean(activeProjectForHeader);
const reserveHeaderActionsSpace = true;
const useMobileNotesPanel = mobileVariant || deviceInfo.isMobile;
React.useEffect(() => {
@@ -687,7 +868,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
setActiveSessionByProject,
currentSessionId,
handleSessionSelect,
isVSCode,
newSessionDraftOpen,
mobileVariant,
openNewSessionDraft,
@@ -698,30 +878,141 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
});
const { getOrderedGroups } = useGroupOrdering(groupOrderByProject);
const hasInitializedArchivedCollapseRef = React.useRef(false);
const handleStartInlineProjectRename = React.useCallback(() => {
if (!activeProjectForHeader) {
React.useEffect(() => {
if (hasInitializedArchivedCollapseRef.current || projectSections.length === 0) {
return;
}
setProjectRenameDraft(formatProjectLabel(
activeProjectForHeader.label?.trim()
|| formatDirectoryName(activeProjectForHeader.normalizedPath, homeDirectory)
|| activeProjectForHeader.normalizedPath,
));
setIsProjectRenameInline(true);
}, [activeProjectForHeader, homeDirectory]);
const archivedGroupKeys = projectSections.flatMap((section) =>
section.groups
.filter((group) => group.isArchivedBucket)
.map((group) => `${section.project.id}:${group.id}`),
);
if (archivedGroupKeys.length > 0) {
setCollapsedGroups((prev) => new Set([...prev, ...archivedGroupKeys]));
}
hasInitializedArchivedCollapseRef.current = true;
}, [projectSections]);
const handleSaveInlineProjectRename = React.useCallback(() => {
if (!activeProjectForHeader) {
return;
const sessionSidebarMetaById = React.useMemo(() => {
const meta = new Map<string, {
node: SessionNode;
projectId: string | null;
groupDirectory: string | null;
secondaryMeta: {
projectLabel?: string | null;
branchLabel?: string | null;
} | null;
}>();
projectSections.forEach((section) => {
const projectLabel = formatProjectLabel(
section.project.label?.trim()
|| formatDirectoryName(section.project.normalizedPath, homeDirectory)
|| section.project.normalizedPath,
);
section.groups.forEach((group) => {
const secondaryMeta = group.branch && group.branch !== projectLabel
? { projectLabel, branchLabel: group.branch }
: { projectLabel, branchLabel: null };
const visit = (nodes: SessionNode[]) => {
nodes.forEach((node) => {
meta.set(node.session.id, {
node,
projectId: section.project.id,
groupDirectory: group.directory,
secondaryMeta,
});
if (node.children.length > 0) {
visit(node.children);
}
});
};
visit(group.sessions);
});
});
return meta;
}, [projectSections, homeDirectory]);
const activeNowSessions = React.useMemo(
() => deriveActiveNowSessions(activeNowEntries, new Map(sessions.map((session) => [session.id, session]))),
[activeNowEntries, sessions],
);
useSessionPrefetch({
currentSessionId,
sortedSessions,
recentSessionIds: activeNowSessions.map((session) => session.id),
loadMessages,
});
const activitySections = React.useMemo(() => {
const toItem = (session: Session) => {
const existing = sessionSidebarMetaById.get(session.id);
const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
return {
node: existing?.node ?? { session, children: [], worktree: null },
projectId: existing?.projectId ?? null,
groupDirectory: existing?.groupDirectory ?? sessionDirectory,
secondaryMeta: existing?.secondaryMeta ?? null,
};
};
return [
{ key: 'active-now' as const, title: 'recent', items: activeNowSessions.map(toItem) },
];
}, [activeNowSessions, sessionSidebarMetaById]);
const activitySessionIds = React.useMemo(() => {
const next = new Set<string>();
activitySections.forEach((section) => {
section.items.forEach((item) => {
next.add(item.node.session.id);
});
});
return next;
}, [activitySections]);
const filteredProjectSections = React.useMemo(() => {
if (hasSessionSearchQuery || activitySessionIds.size === 0) {
return projectSections;
}
const trimmed = projectRenameDraft.trim();
if (!trimmed) {
return;
const filterNodes = (nodes: SessionNode[]): SessionNode[] => {
return nodes.flatMap((node) => {
if (activitySessionIds.has(node.session.id)) {
return [];
}
return [{
...node,
children: filterNodes(node.children),
}];
});
};
return projectSections.map((section) => ({
...section,
groups: section.groups.map((group) => ({
...group,
sessions: filterNodes(group.sessions),
})),
}));
}, [hasSessionSearchQuery, activitySessionIds, projectSections]);
const filteredSectionsForRender = React.useMemo(() => {
if (hasSessionSearchQuery || activitySessionIds.size === 0) {
return sectionsForRender;
}
renameProject(activeProjectForHeader.id, trimmed);
setIsProjectRenameInline(false);
}, [activeProjectForHeader, projectRenameDraft, renameProject]);
const sectionsByProjectId = new Map(filteredProjectSections.map((section) => [section.project.id, section]));
return sectionsForRender
.map((section) => sectionsByProjectId.get(section.project.id) ?? section)
.filter(Boolean);
}, [hasSessionSearchQuery, activitySessionIds, filteredProjectSections, sectionsForRender]);
const desktopHeaderActionButtonClass =
'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed';
@@ -729,14 +1020,6 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed';
const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass;
const headerActionIconClass = 'h-4.5 w-4.5';
const addProjectButtonClass = cn(
'inline-flex cursor-pointer items-center justify-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed',
mobileVariant
? 'h-8 w-8 text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50'
: 'h-8 w-8 text-foreground hover:bg-interactive-hover',
!isDesktopShellRuntime && 'bg-transparent hover:bg-sidebar/40',
);
const stuckProjectHeaders = useStickyProjectHeaders({
isDesktopShellRuntime,
projectSections,
@@ -750,6 +1033,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
groupDirectory?: string | null,
projectId?: string | null,
archivedBucket = false,
secondaryMeta?: { projectLabel?: string | null; branchLabel?: string | null } | null,
renderContext: 'project' | 'recent' = 'project',
): React.ReactNode => (
<SessionNodeItem
node={node}
@@ -782,8 +1067,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
copiedSessionId={copiedSessionId}
handleCopyShareUrl={handleCopyShareUrl}
handleUnshareSession={handleUnshareSession}
openMenuSessionId={openMenuSessionId}
setOpenMenuSessionId={setOpenMenuSessionId}
openSidebarMenuKey={openSidebarMenuKey}
setOpenSidebarMenuKey={setOpenSidebarMenuKey}
renamingFolderId={renamingFolderId}
getFoldersForScope={getFoldersForScope}
getSessionFolderId={getSessionFolderId}
@@ -794,6 +1079,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
handleDeleteSession={handleDeleteSession}
mobileVariant={mobileVariant}
renderSessionNode={renderSessionNode}
secondaryMeta={secondaryMeta}
renderContext={renderContext}
/>
),
[
@@ -822,8 +1109,8 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
copiedSessionId,
handleCopyShareUrl,
handleUnshareSession,
openMenuSessionId,
setOpenMenuSessionId,
openSidebarMenuKey,
setOpenSidebarMenuKey,
renamingFolderId,
getFoldersForScope,
getSessionFolderId,
@@ -898,7 +1185,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
}, [prStatusEntries]);
const renderGroupSessions = React.useCallback(
(group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean) => (
(group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean, dragHandleProps?: SortableDragHandleProps | null) => (
<SessionGroupSection
group={group}
groupKey={groupKey}
@@ -937,6 +1224,7 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
pinnedSessionIds={pinnedSessionIds}
prVisualStateByDirectoryBranch={prVisualStateByDirectoryBranch}
onToggleCollapsedGroup={toggleCollapsedGroup}
dragHandleProps={dragHandleProps}
/>
),
[
@@ -972,44 +1260,62 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
],
);
const isInlineEditing = Boolean(renamingFolderId || editingId || editingProjectId);
const topContent = !hasSessionSearchQuery ? (
<SidebarActivitySections
sections={activitySections}
renderSessionNode={renderSessionNode}
/>
) : null;
const isInlineEditing = Boolean(renamingFolderId || editingId || editingProjectDialogId);
const handleSidebarNewSession = React.useCallback(() => {
setActiveMainTab('chat');
if (mobileVariant) {
setSessionSwitcherOpen(false);
}
openNewSessionDraft();
}, [mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]);
return (
<div
ref={sessionSearchContainerRef}
className={cn(
'flex h-full flex-col text-foreground overflow-x-hidden',
'relative flex h-full flex-col text-foreground overflow-x-hidden',
mobileVariant ? '' : 'bg-transparent',
)}
>
{showDesktopSidebarChrome ? (
<div
onMouseDown={handleDesktopSidebarDragStart}
className={cn(
'app-region-drag flex h-[var(--oc-header-height,56px)] flex-shrink-0 items-center pr-3',
desktopSidebarTopPaddingClass,
)}
>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={toggleSidebar}
className={desktopSidebarToggleButtonClass}
aria-label="Close sessions"
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent>
<p>Close sessions</p>
</TooltipContent>
</Tooltip>
</div>
) : null}
<SidebarHeader
hideDirectoryControls={hideDirectoryControls}
hideProjectSelector={hideProjectSelector}
activeProjectForHeader={activeProjectForHeader}
homeDirectory={homeDirectory}
normalizedProjects={normalizedProjects}
activeProjectId={activeProjectId}
setActiveProjectIdOnly={setActiveProjectIdOnly}
isProjectRenameInline={isProjectRenameInline}
setIsProjectRenameInline={setIsProjectRenameInline}
handleStartInlineProjectRename={handleStartInlineProjectRename}
handleSaveInlineProjectRename={handleSaveInlineProjectRename}
projectRenameDraft={projectRenameDraft}
setProjectRenameDraft={setProjectRenameDraft}
removeProject={removeProject}
handleOpenDirectoryDialog={handleOpenDirectoryDialog}
addProjectButtonClass={addProjectButtonClass}
handleNewSession={handleSidebarNewSession}
headerActionIconClass={headerActionIconClass}
reserveHeaderActionsSpace={reserveHeaderActionsSpace}
stableActiveProjectIsRepo={stableActiveProjectIsRepo}
useMobileNotesPanel={useMobileNotesPanel}
projectNotesPanelOpen={projectNotesPanelOpen}
setProjectNotesPanelOpen={setProjectNotesPanelOpen}
activeProjectRefForHeader={activeProjectRefForHeader}
openMultiRunLauncher={openMultiRunLauncher}
headerActionButtonClass={headerActionButtonClass}
setNewWorktreeDialogOpen={setNewWorktreeDialogOpen}
setActiveMainTab={setActiveMainTab}
isSessionSearchOpen={isSessionSearchOpen}
setIsSessionSearchOpen={setIsSessionSearchOpen}
sessionSearchInputRef={sessionSearchInputRef}
@@ -1020,8 +1326,9 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
/>
<SidebarProjectsList
sectionsForRender={sectionsForRender}
projectSections={projectSections}
topContent={topContent}
sectionsForRender={filteredSectionsForRender}
projectSections={filteredProjectSections}
activeProjectId={activeProjectId}
showOnlyMainWorkspace={showOnlyMainWorkspace}
hasSessionSearchQuery={hasSessionSearchQuery}
@@ -1042,22 +1349,43 @@ export const SessionSidebar: React.FC<SessionSidebarProps> = ({
setActiveMainTab={setActiveMainTab}
setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraft}
createWorktreeSession={createWorktreeSession}
openNewWorktreeDialog={openNewWorktreeDialog}
openMultiRunLauncher={openMultiRunLauncher}
setEditingProjectId={setEditingProjectId}
setEditProjectTitle={setEditProjectTitle}
editingProjectId={editingProjectId}
editProjectTitle={editProjectTitle}
handleSaveProjectEdit={handleSaveProjectEdit}
handleCancelProjectEdit={handleCancelProjectEdit}
openProjectEditDialog={setEditingProjectDialogId}
removeProject={removeProject}
projectHeaderSentinelRefs={projectHeaderSentinelRefs}
settingsAutoCreateWorktree={settingsAutoCreateWorktree}
reorderProjects={reorderProjects}
getOrderedGroups={getOrderedGroups}
setGroupOrderByProject={setGroupOrderByProject}
openSidebarMenuKey={openSidebarMenuKey}
setOpenSidebarMenuKey={setOpenSidebarMenuKey}
isInlineEditing={isInlineEditing}
/>
<SidebarFooter
onOpenSettings={() => setSettingsDialogOpen(true)}
onOpenShortcuts={toggleHelpDialog}
onOpenAbout={() => setAboutDialogOpen(true)}
/>
{editingProject ? (
<ProjectEditDialog
open={Boolean(editingProject)}
onOpenChange={(open) => {
if (!open) {
setEditingProjectDialogId(null);
}
}}
projectId={editingProject.id}
projectName={editingProject.label || formatDirectoryName(editingProject.path, homeDirectory)}
projectPath={editingProject.path}
initialIcon={editingProject.icon}
initialColor={editingProject.color}
initialIconBackground={editingProject.iconBackground}
onSave={handleSaveProjectEdit}
/>
) : null}
<NewWorktreeDialog
open={newWorktreeDialogOpen}
onOpenChange={setNewWorktreeDialogOpen}