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
@@ -8,6 +8,12 @@ type Args = {
|
||||
isInlineEditing: boolean;
|
||||
showDeletionDialog: boolean;
|
||||
foldersMap: Record<string, SessionFolder[]>;
|
||||
/**
|
||||
* Selection scope is the project id (flat per-project session list); this
|
||||
* map resolves it to the project's folder scopes (root + worktrees). When
|
||||
* the scope is missing here it is treated as a plain directory scope.
|
||||
*/
|
||||
folderScopesByProject: Map<string, Array<{ scopeKey: string; directory: string | null }>>;
|
||||
addSessionsToFolder: (scopeKey: string, folderId: string, sessionIds: string[]) => void;
|
||||
removeSessionsFromFolders: (scopeKey: string, sessionIds: string[]) => void;
|
||||
createFolderAndStartRename: (scopeKey: string, parentId?: string | null) => { id: string } | null;
|
||||
@@ -39,6 +45,7 @@ export const useSidebarBulkActions = (args: Args) => {
|
||||
isInlineEditing,
|
||||
showDeletionDialog,
|
||||
foldersMap,
|
||||
folderScopesByProject,
|
||||
addSessionsToFolder,
|
||||
removeSessionsFromFolders,
|
||||
createFolderAndStartRename,
|
||||
@@ -89,38 +96,74 @@ export const useSidebarBulkActions = (args: Args) => {
|
||||
return null;
|
||||
}, [hasSelection, selectedIds, selectionScopeKey]);
|
||||
|
||||
const bulkScopeFolders = React.useMemo(() => {
|
||||
// The selection scope is a project id; folders live per directory scope
|
||||
// (project root + each worktree). Resolve all of them, in project order.
|
||||
const selectionFolderScopes = React.useMemo<string[]>(() => {
|
||||
if (!derivedSelectionScope) return [];
|
||||
return foldersMap[derivedSelectionScope] ?? [];
|
||||
}, [foldersMap, derivedSelectionScope]);
|
||||
const projectScopes = folderScopesByProject.get(derivedSelectionScope);
|
||||
if (projectScopes && projectScopes.length > 0) {
|
||||
return projectScopes.map((scope) => scope.scopeKey);
|
||||
}
|
||||
// Fallback: the scope is already a directory (e.g. VS Code workspaces).
|
||||
return [derivedSelectionScope];
|
||||
}, [derivedSelectionScope, folderScopesByProject]);
|
||||
|
||||
const bulkScopeFolders = React.useMemo(() => {
|
||||
return selectionFolderScopes.flatMap((scope) => foldersMap[scope] ?? []);
|
||||
}, [foldersMap, selectionFolderScopes]);
|
||||
|
||||
const resolveFolderScope = React.useCallback((folderId: string): string | null => {
|
||||
for (const scope of selectionFolderScopes) {
|
||||
if ((foldersMap[scope] ?? []).some((folder) => folder.id === folderId)) return scope;
|
||||
}
|
||||
return null;
|
||||
}, [foldersMap, selectionFolderScopes]);
|
||||
|
||||
const bulkCanRemoveFromFolder = React.useMemo(() => {
|
||||
if (!derivedSelectionScope || !hasSelection) return false;
|
||||
const scopeFolders = foldersMap[derivedSelectionScope] ?? [];
|
||||
for (const folder of scopeFolders) {
|
||||
for (const id of folder.sessionIds) {
|
||||
if (selectedIds.has(id)) return true;
|
||||
if (!hasSelection) return false;
|
||||
for (const scope of selectionFolderScopes) {
|
||||
for (const folder of foldersMap[scope] ?? []) {
|
||||
for (const id of folder.sessionIds) {
|
||||
if (selectedIds.has(id)) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [foldersMap, derivedSelectionScope, hasSelection, selectedIds]);
|
||||
}, [foldersMap, selectionFolderScopes, hasSelection, selectedIds]);
|
||||
|
||||
const moveSelectionToFolder = React.useCallback((targetScope: string, folderId: string) => {
|
||||
const ids = Array.from(selectedIds);
|
||||
// Clear memberships in every other scope first — the store only dedupes
|
||||
// within one scope, and a session must live in a single folder.
|
||||
for (const scope of selectionFolderScopes) {
|
||||
if (scope === targetScope) continue;
|
||||
removeSessionsFromFolders(scope, ids);
|
||||
}
|
||||
addSessionsToFolder(targetScope, folderId, ids);
|
||||
}, [addSessionsToFolder, removeSessionsFromFolders, selectedIds, selectionFolderScopes]);
|
||||
|
||||
const handleBulkMoveToFolder = React.useCallback((folderId: string) => {
|
||||
if (!derivedSelectionScope || !hasSelection) return;
|
||||
addSessionsToFolder(derivedSelectionScope, folderId, Array.from(selectedIds));
|
||||
}, [addSessionsToFolder, selectedIds, derivedSelectionScope, hasSelection]);
|
||||
if (!hasSelection) return;
|
||||
const targetScope = resolveFolderScope(folderId);
|
||||
if (!targetScope) return;
|
||||
moveSelectionToFolder(targetScope, folderId);
|
||||
}, [hasSelection, moveSelectionToFolder, resolveFolderScope]);
|
||||
|
||||
const handleBulkCreateFolderAndMove = React.useCallback(() => {
|
||||
if (!derivedSelectionScope || !hasSelection) return;
|
||||
const newFolder = createFolderAndStartRename(derivedSelectionScope);
|
||||
const targetScope = selectionFolderScopes[0];
|
||||
if (!targetScope || !hasSelection) return;
|
||||
const newFolder = createFolderAndStartRename(targetScope);
|
||||
if (!newFolder) return;
|
||||
addSessionsToFolder(derivedSelectionScope, newFolder.id, Array.from(selectedIds));
|
||||
}, [addSessionsToFolder, createFolderAndStartRename, selectedIds, derivedSelectionScope, hasSelection]);
|
||||
moveSelectionToFolder(targetScope, newFolder.id);
|
||||
}, [createFolderAndStartRename, hasSelection, moveSelectionToFolder, selectionFolderScopes]);
|
||||
|
||||
const handleBulkRemoveFromFolder = React.useCallback(() => {
|
||||
if (!derivedSelectionScope || !hasSelection) return;
|
||||
removeSessionsFromFolders(derivedSelectionScope, Array.from(selectedIds));
|
||||
}, [removeSessionsFromFolders, selectedIds, derivedSelectionScope, hasSelection]);
|
||||
if (!hasSelection) return;
|
||||
const ids = Array.from(selectedIds);
|
||||
for (const scope of selectionFolderScopes) {
|
||||
removeSessionsFromFolders(scope, ids);
|
||||
}
|
||||
}, [removeSessionsFromFolders, selectedIds, selectionFolderScopes, hasSelection]);
|
||||
|
||||
const executeBulkDelete = React.useCallback(async () => {
|
||||
const ids = Array.from(selectedIds);
|
||||
|
||||
Reference in New Issue
Block a user