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
@@ -3,10 +3,17 @@ import { cn } from '@/lib/utils';
import type { SessionFolder } from '@/stores/useSessionFoldersStore';
import { useI18n } from '@/lib/i18n';
import { Icon } from "@/components/icon/Icon";
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import type { SessionNodeChildRenderExtras, SessionNodeRenderExtras } from './sidebar/sessionNodeItemUtils';
interface SessionFolderItemProps<TSessionNode> {
folder: SessionFolder;
/**
* Optional display label override. Flat folder rendering shows nested
* folders at the top level with a "Parent / Child" path instead of
* indentation.
*/
displayName?: string;
sessions: TSessionNode[];
/** Sub-folders that belong directly to this folder */
subFolderItems?: React.ReactNode;
@@ -46,8 +53,6 @@ interface SessionFolderItemProps<TSessionNode> {
isDropTarget?: boolean;
/** Create a new session scoped to this folder */
onNewSession?: () => void;
/** Create a new sub-folder inside this folder */
onNewSubFolder?: () => void;
/** Visual indent depth (0 = root folder, 1 = sub-folder) */
depth?: number;
/** Hide folder action buttons (rename/delete/new) */
@@ -58,6 +63,7 @@ interface SessionFolderItemProps<TSessionNode> {
const SessionFolderItemBase = <TSessionNode,>({
folder,
displayName,
sessions,
subFolderItems,
isCollapsed,
@@ -78,7 +84,6 @@ const SessionFolderItemBase = <TSessionNode,>({
droppableRef,
isDropTarget = false,
onNewSession,
onNewSubFolder,
depth = 0,
hideActions = false,
archivedBucket = false,
@@ -145,10 +150,10 @@ const SessionFolderItemBase = <TSessionNode,>({
}, [isRenaming]);
const folderIconName = isCollapsed ? 'folder' : 'folder-open';
const isSubFolder = depth > 0;
void depth;
return (
<div className={cn('oc-folder', isSubFolder && 'ml-3')}>
<div className="oc-folder">
{/* Folder header also acts as a drop zone when droppableRef is provided */}
<div
ref={droppableRef}
@@ -157,7 +162,10 @@ const SessionFolderItemBase = <TSessionNode,>({
'cursor-pointer',
isDropTarget && 'bg-primary/10 ring-1 ring-inset ring-primary/30',
)}
onClick={renaming ? undefined : onToggle}
onClick={renaming ? undefined : (event) => {
(event.currentTarget as HTMLElement).blur();
onToggle();
}}
role={renaming ? undefined : 'button'}
tabIndex={renaming ? undefined : 0}
onKeyDown={
@@ -178,7 +186,10 @@ const SessionFolderItemBase = <TSessionNode,>({
'min-w-0 flex items-center gap-1.5 pl-1.5 flex-1 transition-[padding]',
archivedBucket
? (alwaysShowActions ? 'pr-7' : 'group-hover/folder:pr-7 group-focus-within/folder:pr-7')
: '',
// Actions overlay on hover (new session, rename, delete = three
// 24px buttons anchored at the right edge); reserve room only
// while they are revealed, mirroring session-row behavior.
: (alwaysShowActions ? 'pr-20' : 'group-hover/folder:pr-20 group-focus-within/folder:pr-20'),
)}>
<Icon name={folderIconName} className={cn('h-3.5 w-3.5 flex-shrink-0', isDropTarget ? 'text-primary' : 'text-muted-foreground')} />
@@ -238,9 +249,16 @@ const SessionFolderItemBase = <TSessionNode,>({
</form>
) : (
<div className="min-w-0 flex items-center gap-1.5 flex-1">
<span className={cn('typography-ui-label font-semibold truncate', isDropTarget ? 'text-primary' : 'text-muted-foreground')}>
{folder.name}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span className={cn('typography-ui-label font-semibold truncate', isDropTarget ? 'text-primary' : 'text-muted-foreground')}>
{displayName ?? folder.name}
</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={8} className="max-w-xs">
{displayName ?? folder.name}
</TooltipContent>
</Tooltip>
<span className="typography-micro text-muted-foreground/70 flex-shrink-0">
{sessions.length}
</span>
@@ -256,14 +274,15 @@ const SessionFolderItemBase = <TSessionNode,>({
{/* Action buttons */}
{!renaming && (!hideActions || archivedBucket) ? (
<div className="flex items-center gap-0.5 px-0.5">
<div
className={cn(
'flex items-center gap-0.5 transition-opacity',
alwaysShowActions ? 'opacity-100' : 'opacity-0 group-hover/folder:opacity-100 group-focus-within/folder:opacity-100',
archivedBucket && 'absolute right-0.5 top-1/2 z-10 -translate-y-1/2 px-0',
)}
>
<div
className={cn(
'absolute right-0.5 top-1/2 z-10 flex -translate-y-1/2 items-center gap-0.5 transition-opacity',
alwaysShowActions
? 'opacity-100'
: 'opacity-0 pointer-events-none group-hover/folder:opacity-100 group-hover/folder:pointer-events-auto group-focus-within/folder:opacity-100 group-focus-within/folder:pointer-events-auto',
)}
>
<div className="flex items-center gap-0.5">
{!archivedBucket && onNewSession ? (
<button
type="button"
@@ -278,21 +297,6 @@ const SessionFolderItemBase = <TSessionNode,>({
<Icon name="add" className="h-3.5 w-3.5" />
</button>
) : null}
{/* Only allow sub-folders at depth 0 (one level deep max) */}
{!archivedBucket && onNewSubFolder && depth === 0 ? (
<button
type="button"
onClick={(event) => {
event.stopPropagation();
onNewSubFolder();
}}
className="inline-flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
aria-label={t('sessions.sidebar.folderItem.newSubfolderAria', { folderName: folder.name })}
title={t('sessions.sidebar.folderItem.newSubfolder')}
>
<Icon name="folder-add" className="h-3.5 w-3.5" />
</button>
) : null}
{!archivedBucket ? (
<button
type="button"
@@ -326,7 +330,7 @@ const SessionFolderItemBase = <TSessionNode,>({
{/* Folder body */}
{!isCollapsed ? (
<div className="pb-1 pl-2">
<div className="pb-1">
{/* Sub-folders first */}
{subFolderItems}
{/* Then sessions */}