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
@@ -1300,6 +1300,37 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
return trimmedTitle && trimmedTitle.length > 0 ? trimmedTitle : 'Untitled Session';
|
||||
}, [activeProjectLabel, currentSession?.title, currentSessionId]);
|
||||
|
||||
// Full-page surfaces (Scheduled, Archive, Worktrees, Multi-run) replace the
|
||||
// chat area; while one is open the header shows the surface identity
|
||||
// instead of the session switcher.
|
||||
const isScheduledSurfaceOpen = useUIStore((state) => state.isScheduledTasksDialogOpen);
|
||||
const isArchiveSurfaceOpen = useUIStore((state) => state.isArchivePageOpen);
|
||||
const worktreesSurfaceProjectId = useUIStore((state) => state.worktreesPageProjectId);
|
||||
const isMultiRunSurfaceOpen = useUIStore((state) => state.isMultiRunLauncherOpen);
|
||||
const worktreesSurfaceProjectLabel = useProjectsStore((state) => {
|
||||
if (!worktreesSurfaceProjectId) return null;
|
||||
const project = state.projects.find((entry) => entry.id === worktreesSurfaceProjectId);
|
||||
return project?.label?.trim() || project?.path?.split('/').pop() || null;
|
||||
});
|
||||
const activeSurfaceHeader = React.useMemo<{ title: string; subtitle: string | null } | null>(() => {
|
||||
if (isScheduledSurfaceOpen) {
|
||||
return { title: t('sessions.scheduledTasks.dialog.title'), subtitle: null };
|
||||
}
|
||||
if (isArchiveSurfaceOpen) {
|
||||
return { title: t('sessions.archivePage.title'), subtitle: null };
|
||||
}
|
||||
if (worktreesSurfaceProjectId) {
|
||||
return {
|
||||
title: t('sessions.worktreesPage.title', { project: worktreesSurfaceProjectLabel ?? '' }),
|
||||
subtitle: null,
|
||||
};
|
||||
}
|
||||
if (isMultiRunSurfaceOpen) {
|
||||
return { title: t('sessions.sidebar.header.actions.newMultiRun'), subtitle: null };
|
||||
}
|
||||
return null;
|
||||
}, [isArchiveSurfaceOpen, isMultiRunSurfaceOpen, isScheduledSurfaceOpen, t, worktreesSurfaceProjectId, worktreesSurfaceProjectLabel]);
|
||||
|
||||
|
||||
const actionDirectory = React.useMemo(() => {
|
||||
return normalize(openDirectory || activeProject?.path || '');
|
||||
@@ -2039,6 +2070,18 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
TitlebarLeftControls overlay; the header reserves matching left space
|
||||
via padding (see headerStyle) when the sidebar is collapsed. */}
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
{activeSurfaceHeader ? (
|
||||
<div className="mr-3 flex min-w-0 flex-col items-start px-1 py-0.5 -my-0.5 text-left">
|
||||
<span className="truncate typography-ui-label text-[14px] font-normal leading-tight text-foreground max-w-full">
|
||||
{activeSurfaceHeader.title}
|
||||
</span>
|
||||
{activeSurfaceHeader.subtitle ? (
|
||||
<span className="truncate typography-micro text-[10.5px] font-normal leading-tight text-muted-foreground/75 max-w-full">
|
||||
{activeSurfaceHeader.subtitle}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<SessionSwitcherDropdown>
|
||||
<button
|
||||
type="button"
|
||||
@@ -2070,6 +2113,7 @@ export const Header: React.FC<HeaderProps> = ({
|
||||
) : null}
|
||||
</button>
|
||||
</SessionSwitcherDropdown>
|
||||
)}
|
||||
|
||||
{tabs.length > 0 && (
|
||||
<div className="flex items-center gap-1 rounded-lg bg-[var(--surface-muted)]/50 p-1">
|
||||
|
||||
@@ -13,6 +13,9 @@ import { HelpDialog } from '../ui/HelpDialog';
|
||||
import { OpenCodeStatusDialog } from '../ui/OpenCodeStatusDialog';
|
||||
import { SessionSidebar } from '@/components/session/SessionSidebar';
|
||||
import { SessionDialogs } from '@/components/session/SessionDialogs';
|
||||
import { ScheduledTasksDialog } from '@/components/session/ScheduledTasksDialog';
|
||||
import { ArchiveView } from '@/components/views/ArchiveView';
|
||||
import { WorktreesView } from '@/components/views/WorktreesView';
|
||||
import { DiffWorkerProvider } from '@/contexts/DiffWorkerProvider';
|
||||
import { MultiRunLauncher } from '@/components/multirun';
|
||||
import { TerminalView } from '@/components/views/TerminalView';
|
||||
@@ -37,7 +40,6 @@ import { PlanView } from '@/components/views/PlanView';
|
||||
const DiagramView = lazyWithChunkRecovery(() => import('@/components/views/DiagramView').then(m => ({ default: m.DiagramView })));
|
||||
const SettingsView = lazyWithChunkRecovery(() => import('@/components/views/SettingsView').then(m => ({ default: m.SettingsView })));
|
||||
const SettingsWindow = lazyWithChunkRecovery(() => import('@/components/views/SettingsWindow').then(m => ({ default: m.SettingsWindow })));
|
||||
const MultiRunWindow = lazyWithChunkRecovery(() => import('@/components/views/MultiRunWindow').then(m => ({ default: m.MultiRunWindow })));
|
||||
|
||||
export const MainLayout: React.FC = () => {
|
||||
const isSidebarOpen = useUIStore((state) => state.isSidebarOpen);
|
||||
@@ -49,6 +51,32 @@ export const MainLayout: React.FC = () => {
|
||||
const isMultiRunLauncherOpen = useUIStore((state) => state.isMultiRunLauncherOpen);
|
||||
const setMultiRunLauncherOpen = useUIStore((state) => state.setMultiRunLauncherOpen);
|
||||
const multiRunLauncherPrefillPrompt = useUIStore((state) => state.multiRunLauncherPrefillPrompt);
|
||||
const isScheduledTasksPageOpen = useUIStore((state) => state.isScheduledTasksDialogOpen);
|
||||
const isArchivePageOpen = useUIStore((state) => state.isArchivePageOpen);
|
||||
const worktreesPageProjectId = useUIStore((state) => state.worktreesPageProjectId);
|
||||
// Any full-page surface replacing the chat area. While open, the chat and
|
||||
// secondary views are fully hidden (not just covered) so none of their
|
||||
// floating chrome bleeds through, and selecting a session / draft / main
|
||||
// tab anywhere closes the surface.
|
||||
const isSurfacePageOpen = isScheduledTasksPageOpen || isArchivePageOpen || Boolean(worktreesPageProjectId) || isMultiRunLauncherOpen;
|
||||
|
||||
React.useEffect(() => {
|
||||
const closeSurfacePages = () => useUIStore.getState().closeMainSurfaces();
|
||||
const unsubscribeSession = useSessionUIStore.subscribe((state, prev) => {
|
||||
const sessionSelected = Boolean(state.currentSessionId) && state.currentSessionId !== prev.currentSessionId;
|
||||
// Draft identity change covers re-opening a draft while one is
|
||||
// already open (the boolean alone never transitions then).
|
||||
const draftOpened = Boolean(state.newSessionDraft?.open) && state.newSessionDraft !== prev.newSessionDraft;
|
||||
if (sessionSelected || draftOpened) closeSurfacePages();
|
||||
});
|
||||
const unsubscribeTab = useUIStore.subscribe((state, prev) => {
|
||||
if (state.activeMainTab !== prev.activeMainTab) closeSurfacePages();
|
||||
});
|
||||
return () => {
|
||||
unsubscribeSession();
|
||||
unsubscribeTab();
|
||||
};
|
||||
}, []);
|
||||
const { isMobile } = useDeviceInfo();
|
||||
const mobilePanelsResetRef = React.useRef(false);
|
||||
|
||||
@@ -306,11 +334,11 @@ export const MainLayout: React.FC = () => {
|
||||
)}
|
||||
>
|
||||
<main className="w-full h-full overflow-hidden bg-background relative" data-page-scroll-lock="true">
|
||||
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
|
||||
<ErrorBoundary><ChatView active={isChatActive && !isSettingsDialogOpen} /></ErrorBoundary>
|
||||
<div className={cn('absolute inset-0', (!isChatActive || isSurfacePageOpen) && 'invisible')}>
|
||||
<ErrorBoundary><ChatView active={isChatActive && !isSettingsDialogOpen && !isSurfacePageOpen} /></ErrorBoundary>
|
||||
</div>
|
||||
{secondaryView && (
|
||||
<div className="absolute inset-0">
|
||||
<div className={cn('absolute inset-0', isSurfacePageOpen && 'invisible')}>
|
||||
<ErrorBoundary>{secondaryView}</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
@@ -325,6 +353,9 @@ export const MainLayout: React.FC = () => {
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
<ErrorBoundary><ScheduledTasksDialog /></ErrorBoundary>
|
||||
<ErrorBoundary><ArchiveView /></ErrorBoundary>
|
||||
<ErrorBoundary><WorktreesView /></ErrorBoundary>
|
||||
{/* Always mount SessionSidebar on mobile to match desktop behavior.
|
||||
Conditional mount (mobileLeftDrawerVisible && ...) caused a
|
||||
data-loading cascade on every drawer open: paginated sessions
|
||||
@@ -395,14 +426,31 @@ export const MainLayout: React.FC = () => {
|
||||
<div className="flex flex-1 min-h-0 overflow-hidden" data-page-scroll-lock="true">
|
||||
<div className="relative flex flex-1 min-h-0 min-w-0 overflow-hidden" data-page-scroll-lock="true">
|
||||
<main className="flex-1 overflow-hidden bg-background relative" data-page-scroll-lock="true">
|
||||
<div className={cn('absolute inset-0', !isChatActive && 'invisible')}>
|
||||
<ErrorBoundary><ChatView active={isChatActive && !isSettingsDialogOpen} /></ErrorBoundary>
|
||||
<div className={cn('absolute inset-0', (!isChatActive || isSurfacePageOpen) && 'invisible')}>
|
||||
<ErrorBoundary><ChatView active={isChatActive && !isSettingsDialogOpen && !isSurfacePageOpen} /></ErrorBoundary>
|
||||
</div>
|
||||
{secondaryView && (
|
||||
<div className="absolute inset-0">
|
||||
<div className={cn('absolute inset-0', isSurfacePageOpen && 'invisible')}>
|
||||
<ErrorBoundary>{secondaryView}</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
{isMultiRunLauncherOpen && (
|
||||
<div className="absolute inset-0 z-10 bg-background">
|
||||
<ErrorBoundary>
|
||||
{/* isWindowed: the app Header already shows the surface
|
||||
title, so skip the launcher's own title bar. */}
|
||||
<MultiRunLauncher
|
||||
isWindowed
|
||||
initialPrompt={multiRunLauncherPrefillPrompt}
|
||||
onCreated={() => setMultiRunLauncherOpen(false)}
|
||||
onCancel={() => setMultiRunLauncherOpen(false)}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
<ErrorBoundary><ScheduledTasksDialog /></ErrorBoundary>
|
||||
<ErrorBoundary><ArchiveView /></ErrorBoundary>
|
||||
<ErrorBoundary><WorktreesView /></ErrorBoundary>
|
||||
</main>
|
||||
<ContextPanel />
|
||||
</div>
|
||||
@@ -422,13 +470,6 @@ export const MainLayout: React.FC = () => {
|
||||
onOpenChange={setSettingsDialogOpen}
|
||||
/>
|
||||
</React.Suspense>
|
||||
<React.Suspense fallback={null}>
|
||||
<MultiRunWindow
|
||||
open={isMultiRunLauncherOpen}
|
||||
onOpenChange={setMultiRunLauncherOpen}
|
||||
initialPrompt={multiRunLauncherPrefillPrompt}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export const ProjectEditDialog: React.FC<ProjectEditDialogProps> = ({
|
||||
<ScrollableOverlay outerClassName="max-h-[min(90vh,48rem)]" className="w-full bg-background">
|
||||
<div className="w-full p-3 sm:p-6 sm:pt-8">
|
||||
{open && project ? (
|
||||
<ProjectSettingsPanel project={project} onIdentitySave={onSave} />
|
||||
<ProjectSettingsPanel project={project} onIdentitySave={onSave} showWorktrees={false} />
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollableOverlay>
|
||||
|
||||
Reference in New Issue
Block a user