feat(ui): sidebar redesign — project zones, grouping modes, full-page surfaces (#2480)

* checkpoint: flatten sidebar core (flat sessions, zones, single mode, folders flat)

* checkpoint: sidebar nav + scheduled/archive full-page surfaces, recent zone header

* checkpoint: worktrees management surface via project menu

* checkpoint: docs, i18n, validation for sidebar redesign

* checkpoint: unified row/zone geometry, recent backfill, tooltips everywhere, primary spinner

* checkpoint: branch icon marker, date moved to rich tooltip, recent back to pure time window

* checkpoint: PR state on branch markers + tooltip, no reserved right space, aligned show-more, instant tooltips

* checkpoint: reserve hover-action space so title text is not overlapped

* checkpoint: tighten hover-action reserve

* checkpoint: color-only unread emphasis to avoid title reflow

* checkpoint: drop new-subfolder action, folder actions overlay on hover

* checkpoint: fix sticky project headers (sticky on trigger div), stuck elevation

* checkpoint: full-bleed semibold zone headers, no top scroll fade

* checkpoint: headers without background tint (typography-only emphasis)

* checkpoint: recent header flush with scroll top (no pre-stick bump)

* checkpoint: shared tooltip provider with grouping (instant handoff between rows)

* checkpoint: blur pointer-click focus so hover chrome hides on mouse-leave

* checkpoint: tooltip closeDelay bridges inter-row gap

* checkpoint: nav above controls, merged view dropdown, project-scoped bulk selection, cross-worktree folders

* checkpoint: folder header tooltip with full path name

* checkpoint: true page surfaces (hidden chat, header title, close-on-select), multirun page, run-now jump, folders on top, controls row polish

* checkpoint: rename mode — dual-instance outside-click fix, no vertical shift

* checkpoint: session grouping mode toggle (by-worktree default, flat option)

* checkpoint: worktree header — hover padding reserve + delete worktree action

* checkpoint: frosted sticky header backing under desktop vibrancy

* checkpoint: align worktree sub-header with project header icon column

* checkpoint: restore worktree group DnD reorder; dense vibrancy header tint (Chromium mask+backdrop-filter)

* checkpoint: vibrancy — drop scroller mask so backdrop-filter samples rows (Chromium backdrop-root limitation)

* checkpoint: vibrancy headers use opaque sidebar tone (Electron transparent-window backdrop-filter bug)

* checkpoint: nav collapsed to one row (New session + surface icons), mirrors Add project row

* checkpoint: raise sticky zone headers above row action layers (z-20)

* checkpoint: New session as full-width CTA + single quiet toolbar row

* checkpoint: New session row back to quiet text form above the toolbar

* checkpoint: align toolbar left icon with New session icon column

* checkpoint: hoverless flat header controls (color-only hover states)

* checkpoint: sticky project headers toggle in view dropdown (default on)

* checkpoint: full-page surfaces adapted — archive directory filter panel, scheduled master-detail, multirun without duplicate title bar

* checkpoint: multirun joins mutually exclusive surface set

* checkpoint: surfaces leave via navigation only (no close/cancel buttons), robust close-on-new-session, drop dead MultiRunWindow

* checkpoint: no scheduled header description, aligned empty-worktree note, collapse/expand covers worktree groups

* checkpoint: overlay scrollbar above sticky zone headers

* checkpoint: archive delete icons reveal on hover with padding shift

* checkpoint: new worktrees surface at top of the worktree list

* checkpoint: no grab cursor on worktree headers

* checkpoint: worktrees page list-only with inline action, no worktrees in edit dialog, drop menu ellipsis

* checkpoint: worktrees page uses full content width

* checkpoint: worktrees header — 'in' instead of em dash, no description

* checkpoint: drop legacy OpenCode badge from worktree list, tooltip without OpenCode mention

* review: bound pr summary cache
This commit is contained in:
Bohdan Triapitsyn
2026-07-28 12:04:39 +03:00
committed by GitHub
parent 5787ea5d49
commit 1291cde5c2
56 changed files with 1902 additions and 1044 deletions
@@ -17,14 +17,15 @@ import { useUIStore } from '@/stores/useUIStore';
import { getDeferredSafeStorage } from '@/stores/utils/safeStorage';
import { useGitStore, useGitAllBranches, useGitRepoStatusMap } from '@/stores/useGitStore';
import { isVSCodeRuntime } from '@/lib/desktop';
import { Icon } from '@/components/icon/Icon';
import { TooltipProvider } from '@/components/ui/tooltip';
import { NewWorktreeDialog } from './NewWorktreeDialog';
import { ScheduledTasksDialog } from './ScheduledTasksDialog';
import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore';
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
import { useArchivedAutoFolders } from './sidebar/hooks/useArchivedAutoFolders';
import { useGroupOrdering } from './sidebar/hooks/useGroupOrdering';
import { useSessionSidebarSections } from './sidebar/hooks/useSessionSidebarSections';
import { ProjectSessionSelectionEffect } from './sidebar/hooks/useProjectSessionSelection';
import { useGroupOrdering } from './sidebar/hooks/useGroupOrdering';
import { useSessionGrouping } from './sidebar/hooks/useSessionGrouping';
import { useSessionSearchEffects } from './sidebar/hooks/useSessionSearchEffects';
import { useSessionActions } from './sidebar/hooks/useSessionActions';
@@ -34,11 +35,11 @@ import { useProjectSessionLists } from './sidebar/hooks/useProjectSessionLists';
import { useAuthoritativeSessionCleanup } from './sidebar/hooks/useAuthoritativeSessionCleanup';
import { createSessionOwnershipIndex } from './sidebar/sessionOwnership';
import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders';
import { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog';
import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { SessionGroupSection } from './sidebar/SessionGroupSection';
import { SidebarHeader } from './sidebar/SidebarHeader';
import { SidebarNav } from './sidebar/SidebarNav';
import { SidebarActivitySections } from './sidebar/SidebarActivitySections';
import { SidebarFooter } from './sidebar/SidebarFooter';
import { SidebarProjectsList } from './sidebar/SidebarProjectsList';
@@ -86,10 +87,13 @@ import {
resolveGlobalSessionDirectory,
useGlobalSessionsStore,
} from '@/stores/useGlobalSessionsStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useNotificationStore } from '@/sync/notification-store';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { getGitHubPrStatusKey, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore';
import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents';
import { buildSessionBootstrapDemands } from './sidebar/sessionBootstrapDemands';
import { recordWorktreesSeen } from './sidebar/worktreeFirstSeen';
import { getRuntimeKey } from '@/lib/runtime-switch';
import { streamPerfCount, streamPerfMark } from '@/stores/utils/streamDebug';
@@ -103,32 +107,6 @@ const PROJECT_ACTIVE_SESSION_STORAGE_KEY = 'oc.sessions.activeSessionByProject';
// mixed contexts and is intentionally not migrated.
const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents.v3';
type PrVisualState = 'draft' | 'open' | 'blocked' | 'merged' | 'closed';
type PrIndicator = {
visualState: PrVisualState;
number: number;
url: string | null;
state: 'open' | 'closed' | 'merged';
draft: boolean;
title: string | null;
base: string | null;
head: string | null;
checks: {
state: 'success' | 'failure' | 'pending' | 'unknown';
total: number;
success: number;
failure: number;
pending: number;
} | null;
canMerge: boolean | null;
mergeableState: string | null;
repo: {
owner: string;
repo: string;
} | null;
};
const buildKnownSessionDirectories = (
projects: Array<{ path: string }>,
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
@@ -230,6 +208,57 @@ const SidebarBootstrapDemandEffect: React.FC<{
return null;
};
// Aggregated activity/attention dot for a collapsed project header. Only
// mounted while the project is collapsed, so the per-status-event scans stay
// rare and bounded by the project's directory count.
const ProjectAggregateStatusIndicator: React.FC<{ directories: Array<string | null> }> = ({ directories }) => {
const { t } = useI18n();
const directorySet = React.useMemo(() => {
const set = new Set<string>();
directories.forEach((directory) => {
const normalized = normalizePath(directory)?.toLowerCase();
if (normalized) set.add(normalized);
});
return set;
}, [directories]);
const hasBusySession = useGlobalSessionStatusStore(React.useCallback((state) => {
for (const entry of state.statusById.values()) {
if (entry.status.type !== 'busy' && entry.status.type !== 'retry') continue;
const directory = normalizePath(entry.directory)?.toLowerCase();
if (directory && directorySet.has(directory)) return true;
}
return false;
}, [directorySet]));
const hasUnseenNotification = useNotificationStore(React.useCallback((state) => {
for (const [directory, count] of Object.entries(state.index.project.unseenCount)) {
if (!count) continue;
const normalized = normalizePath(directory)?.toLowerCase();
if (normalized && directorySet.has(normalized)) return true;
}
return false;
}, [directorySet]));
if (hasBusySession) {
return (
<Icon
name="loader-4"
className="h-3 w-3 animate-spin text-primary"
aria-label={t('sessions.sidebar.session.status.active')}
/>
);
}
if (hasUnseenNotification) {
return (
<span
className="h-1.5 w-1.5 rounded-full bg-[var(--status-info)]"
aria-label={t('sessions.sidebar.session.status.unread')}
title={t('sessions.sidebar.session.status.unread')}
/>
);
}
return null;
};
const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
isVisible = true,
mobileVariant = false,
@@ -247,7 +276,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const [sessionSearchQuery, setSessionSearchQuery] = React.useState('');
const sessionSearchContainerRef = React.useRef<HTMLDivElement | null>(null);
const sessionSearchInputRef = React.useRef<HTMLInputElement | null>(null);
const retriedNoPrStatusKeysRef = React.useRef<Set<string>>(new Set());
const [editingId, setEditingId] = React.useState<string | null>(null);
const [editTitle, setEditTitle] = React.useState('');
const [editingProjectDialogId, setEditingProjectDialogId] = React.useState<string | null>(null);
@@ -351,6 +379,8 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const setAboutDialogOpen = useUIStore((state) => state.setAboutDialogOpen);
const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen);
const setScheduledTasksDialogOpen = useUIStore((state) => state.setScheduledTasksDialogOpen);
const setArchivePageOpen = useUIStore((state) => state.setArchivePageOpen);
const setWorktreesPageProjectId = useUIStore((state) => state.setWorktreesPageProjectId);
const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher);
const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks);
const showDeletionDialog = useUIStore((state) => state.showDeletionDialog);
@@ -434,6 +464,9 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
// is driven by the openchamber:navigate event, so switch to chat explicitly
// (a no-op in the expanded side-by-side layout, which is always showing chat).
const openNewSessionDraftFromTree = React.useCallback<typeof openNewSessionDraft>((options) => {
// Starting a draft always leaves any full-page surface, even when a
// draft was already open (no store transition fires in that case).
useUIStore.getState().closeMainSurfaces();
openNewSessionDraft(options);
if (isVSCode) {
window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'chat' } }));
@@ -575,6 +608,9 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
}
}
const allWorktrees = [...worktreesByProject.values()].flat();
// Newly appearing worktrees sort to the top of their project's
// worktree list (see worktreeFirstSeen.ts).
recordWorktreesSeen(allWorktrees.map((worktree) => worktree.path), Date.now());
// Skip update if nothing changed — see worktreeMapsEqual JSDoc.
if (!worktreeMapsEqual(worktreesByProject, currentByProject)) {
@@ -898,9 +934,21 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
});
}, []);
// Collapse/expand covers both levels: projects and their worktree groups.
const projectSectionsRef = React.useRef<typeof projectSections>([]);
const collapseAllProjects = React.useCallback(() => {
ignoreIntersectionUntil.current = Date.now() + 150;
setVisibleSessionCountByGroup(new Map());
setCollapsedGroups(() => {
const allGroupKeys = new Set<string>();
projectSectionsRef.current.forEach((section) => {
section.groups.forEach((group) => {
if (!group.isMain) allGroupKeys.add(`${section.project.id}:${group.id}`);
});
});
return allGroupKeys;
});
setCollapsedProjects(() => {
const allIds = new Set(projects.map((p) => p.id));
try {
@@ -916,6 +964,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const expandAllProjects = React.useCallback(() => {
ignoreIntersectionUntil.current = Date.now() + 150;
setVisibleSessionCountByGroup(new Map());
setCollapsedGroups(new Set());
setCollapsedProjects(() => {
const empty = new Set<string>();
try {
@@ -1131,7 +1180,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
deleteFolderConfirm,
bulkDeleteConfirm,
collapsedGroups,
groupOrderByProject,
};
const previousSidebarRenderSourcesRef = React.useRef<typeof sidebarRenderSources | null>(null);
const previousSidebarRenderSources = previousSidebarRenderSourcesRef.current;
@@ -1191,6 +1239,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
projectSections,
groupSearchDataByGroup,
sectionsForRender,
flatSectionsForRender,
searchMatchCount,
} = useSessionSidebarSections({
normalizedProjects: sortedProjects,
@@ -1208,6 +1257,8 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
foldersMap,
});
projectSectionsRef.current = projectSections;
const searchEmptyState = React.useMemo(() => (
<div className="py-6 text-center text-muted-foreground">
<p className="typography-ui-label font-semibold">{t('sessions.sidebar.empty.noMatches.title')}</p>
@@ -1344,41 +1395,30 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
);
// Web/desktop route archived sessions to the Archive page; only the VS Code
// compact webview keeps inline archived buckets behind its toggle.
const showInlineArchived = isVSCode && showArchivedSessions;
// 'by-worktree' renders the worktree-grouped sections (parallel-work
// overview); 'flat' renders the merged per-project list. VS Code has no
// worktree groups, so both resolve to the same shape — use flat there.
const sessionGroupingMode = useSessionDisplayStore((state) => state.sessionGroupingMode);
const useGroupedSections = sessionGroupingMode === 'by-worktree' && !isVSCode;
const sectionsForSidebarRender = React.useMemo(() => {
return showArchivedSessions
? sectionsForRender
: sectionsForRender.map((section) => ({
...section,
groups: section.groups.filter((group) => !group.isArchivedBucket),
}));
}, [sectionsForRender, showArchivedSessions]);
const prLookup = React.useMemo(() => {
if (!isVisible) {
return {
keys: EMPTY_STRING_ARRAY,
displayKeyByLookupKey: new Map<string, string>(),
};
}
const keys = new Set<string>();
const displayKeyByLookupKey = new Map<string, string>();
sectionsForSidebarRender.forEach((section) => {
section.groups.forEach((group) => {
const directory = normalizePath(group.directory ?? null);
const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim();
if (!directory || !branch) {
return;
}
const lookupKey = getGitHubPrStatusKey(directory, branch);
keys.add(lookupKey);
displayKeyByLookupKey.set(lookupKey, `${directory}::${branch}`);
});
});
return { keys: [...keys], displayKeyByLookupKey };
}, [gitBranches, isVisible, sectionsForSidebarRender]);
const prVisualSummaryMap = usePrVisualSummaryByKeys(prLookup.keys);
const source = useGroupedSections ? sectionsForRender : flatSectionsForRender;
return showInlineArchived
? source
: source.map((section) => (
section.groups.some((group) => group.isArchivedBucket)
? { ...section, groups: section.groups.filter((group) => !group.isArchivedBucket) }
: section
));
}, [flatSectionsForRender, sectionsForRender, showInlineArchived, useGroupedSections]);
// Discover/refresh PR status for expanded projects' worktree branches so
// session rows can tint their branch marker and show PR state in tooltips.
// The data source is the worktree-grouped projectSections (data layer), not
// the flat display sections.
const retriedNoPrStatusKeysRef = React.useRef<Set<string>>(new Set());
React.useEffect(() => {
if (!isVisible || !githubAuthChecked || !githubAuthStatus?.connected || !github) {
return;
@@ -1387,12 +1427,15 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
const targetsByKey = new Map<string, { directory: string; branch: string }>();
const now = Date.now();
sectionsForSidebarRender.forEach((section) => {
projectSections.forEach((section) => {
if (collapsedProjects.has(section.project.id)) {
return;
}
section.groups.forEach((group) => {
if (group.isArchivedBucket || group.isMain) {
return;
}
const directory = normalizePath(group.directory ?? null);
const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim();
if (!directory || !branch) {
@@ -1452,8 +1495,8 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
githubAuthStatus?.connected,
isVisible,
gitBranches,
projectSections,
refreshPrStatusTargets,
sectionsForSidebarRender,
setPrStatusParams,
]);
@@ -1529,6 +1572,29 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
),
);
// Selection scope is the project id; bulk folder actions need the project's
// directory scopes (root + worktrees) to resolve folders across worktrees.
const folderScopesByProject = React.useMemo(() => {
const map = new Map<string, Array<{ scopeKey: string; directory: string | null }>>();
flatSectionsForRender.forEach((section) => {
const flatGroup = section.groups.find((group) => !group.isArchivedBucket);
if (flatGroup?.folderScopes && flatGroup.folderScopes.length > 0) {
map.set(section.project.id, flatGroup.folderScopes);
}
});
return map;
}, [flatSectionsForRender]);
const renderProjectStatusIndicator = React.useCallback((_projectId: string, groups: SessionGroup[]) => {
const directories: Array<string | null> = [];
groups.forEach((group) => {
if (group.isArchivedBucket) return;
directories.push(group.directory);
group.folderScopes?.forEach((scope) => directories.push(scope.directory));
});
return <ProjectAggregateStatusIndicator directories={directories} />;
}, []);
const toggleCollapsedGroup = React.useCallback((key: string) => {
resetGroupSessionLimit(key);
setCollapsedGroups((prev) => {
@@ -1539,31 +1605,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
});
}, [resetGroupSessionLimit]);
const prVisualStateByDirectoryBranch = React.useMemo(() => {
const result = new Map<string, PrIndicator>();
for (const [key, summary] of prVisualSummaryMap) {
const displayKey = prLookup.displayKeyByLookupKey.get(key);
if (!displayKey) {
continue;
}
result.set(displayKey, {
visualState: summary.visualState as PrVisualState,
number: summary.number,
url: summary.url,
state: summary.prState as 'open' | 'closed' | 'merged',
draft: summary.draft,
title: summary.title,
base: summary.base,
head: summary.head,
checks: summary.checks as PrIndicator['checks'],
canMerge: summary.canMerge,
mergeableState: summary.mergeableState,
repo: summary.repo,
});
}
return result;
}, [prLookup.displayKeyByLookupKey, prVisualSummaryMap]);
const renderGroupSessions = React.useCallback(
(
group: SessionGroup,
@@ -1579,6 +1620,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
groupKey={groupKey}
projectId={projectId}
hideGroupLabel={hideGroupLabel}
dragHandleProps={dragHandleProps}
compactBodyPadding={compactBodyPadding}
hasSessionSearchQuery={hasSessionSearchQuery}
normalizedSessionSearchQuery={normalizedSessionSearchQuery}
@@ -1593,8 +1635,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
showDeletionDialog={showDeletionDialog}
setDeleteFolderConfirm={setDeleteFolderConfirm}
renderSessionNode={renderSessionNode}
projectRepoStatus={projectRepoStatus}
lastRepoStatus={lastRepoStatusRef.current}
showMoreGroupSessions={showMoreGroupSessions}
resetGroupSessionLimit={resetGroupSessionLimit}
mobileVariant={mobileVariant}
@@ -1616,9 +1656,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
editingId={editingId}
editTitle={editTitle}
openSidebarMenuKey={openSidebarMenuKey}
prVisualStateByDirectoryBranch={prVisualStateByDirectoryBranch}
onToggleCollapsedGroup={toggleCollapsedGroup}
dragHandleProps={dragHandleProps}
scrollContainerRef={scrollContainerRef}
/>
),
@@ -1635,7 +1673,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
deleteFolder,
showDeletionDialog,
renderSessionNode,
projectRepoStatus,
showMoreGroupSessions,
resetGroupSessionLimit,
mobileVariant,
@@ -1655,7 +1692,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
editingId,
editTitle,
openSidebarMenuKey,
prVisualStateByDirectoryBranch,
toggleCollapsedGroup,
],
);
@@ -1694,6 +1730,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
isInlineEditing,
showDeletionDialog,
foldersMap,
folderScopesByProject,
addSessionsToFolder,
removeSessionsFromFolders,
createFolderAndStartRename,
@@ -1710,6 +1747,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
}, [mobileVariant, openMultiRunLauncher, setActiveMainTab, setSessionSwitcherOpen]);
const handleOpenNewSessionDraftFromHeader = React.useCallback(() => {
useUIStore.getState().closeMainSurfaces();
setActiveMainTab('chat');
if (mobileVariant) {
setSessionSwitcherOpen(false);
@@ -1718,6 +1756,13 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
}, [mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]);
return (
// One shared tooltip provider for the whole sidebar: session tooltips open
// instantly, and moving between rows hands the tooltip over (grouping)
// instead of replaying the exit/enter animation for each row.
// closeDelay bridges the small gap between rows: the tooltip survives the
// pointer crossing row margins, and the grouping timeout hands it over to
// the next row without an exit/enter cycle.
<TooltipProvider delay={0} closeDelay={150} timeout={600}>
<div
ref={sessionSearchContainerRef}
className={cn(
@@ -1751,13 +1796,24 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
recentSessions={recentSessions}
prefetchSession={sync.prefetchSession}
/>
{!hideDirectoryControls && !isVSCode ? (
<SidebarNav onNewSession={handleOpenNewSessionDraftFromHeader} />
) : null}
<SidebarHeader
hideDirectoryControls={hideDirectoryControls}
showRecentControls={!isVSCode}
handleOpenDirectoryDialog={handleOpenDirectoryDialog}
openNewSessionDraft={handleOpenNewSessionDraftFromHeader}
onOpenScheduled={() => {
if (mobileVariant) setSessionSwitcherOpen(false);
setScheduledTasksDialogOpen(true);
}}
onOpenMultiRun={handleOpenMultiRunFromHeader}
canOpenMultiRun={projects.length > 0}
openMultiRunLauncher={handleOpenMultiRunFromHeader}
onOpenArchive={() => {
if (mobileVariant) setSessionSwitcherOpen(false);
setArchivePageOpen(true);
}}
headerActionIconClass={headerActionIconClass}
headerActionButtonClass={headerActionButtonClass}
isSessionSearchOpen={isSessionSearchOpen}
@@ -1769,7 +1825,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
searchMatchCount={searchMatchCount}
collapseAllProjects={collapseAllProjects}
expandAllProjects={expandAllProjects}
openScheduledTasksDialog={() => setScheduledTasksDialogOpen(true)}
selectionModeEnabled={selectionModeEnabled}
onToggleSelectionMode={handleToggleSelectionMode}
/>
@@ -1799,6 +1854,10 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
setSessionSwitcherOpen={setSessionSwitcherOpen}
openNewSessionDraft={openNewSessionDraftFromTree}
openNewWorktreeDialog={openNewWorktreeDialog}
openWorktreesPage={(projectId) => {
if (mobileVariant) setSessionSwitcherOpen(false);
setWorktreesPageProjectId(projectId);
}}
openProjectEditDialog={setEditingProjectDialogId}
removeProject={removeProject}
projectHeaderSentinelRefs={projectHeaderSentinelRefs}
@@ -1806,6 +1865,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
projectSortOrder={projectSortOrder}
getOrderedGroups={getOrderedGroups}
setGroupOrderByProject={setGroupOrderByProject}
renderProjectStatusIndicator={renderProjectStatusIndicator}
openSidebarMenuKey={openSidebarMenuKey}
setOpenSidebarMenuKey={setOpenSidebarMenuKey}
isInlineEditing={isInlineEditing}
@@ -1875,8 +1935,6 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
}}
/>
<ScheduledTasksDialog />
<SessionDeleteConfirmDialog
value={deleteSessionConfirm}
setValue={setDeleteSessionConfirm}
@@ -1899,6 +1957,7 @@ const SessionSidebarComponent: React.FC<SessionSidebarProps> = ({
onConfirm={confirmBulkDelete}
/>
</div>
</TooltipProvider>
);
};