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:
committed by
GitHub
parent
5787ea5d49
commit
1291cde5c2
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user