fix(ui): keep settings sidebar open, center content, and fix worktree refresh (#1058)

* fix(ui): keep settings sidebar open, center content, and fix worktree refresh

- Prevent settings nav from collapsing on split pages (Magic Prompts, Agents, etc.)

- Center settings content with max-w-3xl for better layout on wide screens

- Widen settings window from 960px to 1200px max-width

- Fix source branch dropdown overflow with max-height and scrolling

- Add worktreeRefreshNonce to trigger immediate sidebar refresh after worktree creation

* ref(ui): address Greptile review — remove dead isNavCollapsed code and split worktree effect

- Remove isNavCollapsed variable and all dead branches (resize handle, renderSettingsNav param, SETTINGS_NAV_RAIL_WIDTH)

- Split SessionSidebar effect so worktreeRefreshNonce only triggers discoverWorktrees, not refreshGlobalSessions

- Both type-check and lint pass

* fix(ui): refine settings and worktree dropdown behavior

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Shyamalan Kannan
2026-04-28 14:52:57 +03:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 4f51abddf4
commit 6ab363f8df
4 changed files with 35 additions and 48 deletions
@@ -1593,7 +1593,7 @@ export function NewWorktreeDialog({
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" /> <RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={6} className="w-[320px] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}> <DropdownMenuContent align="start" sideOffset={6} portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={existingBranchDropdownContentRef}>
<Command shouldFilter={false}> <Command shouldFilter={false}>
<CommandInput <CommandInput
placeholder={t('session.newWorktree.searchBranches')} placeholder={t('session.newWorktree.searchBranches')}
@@ -1833,7 +1833,7 @@ export function NewWorktreeDialog({
<RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" /> <RiArrowDownSLine className="h-4 w-4 shrink-0 text-muted-foreground" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[320px] p-0" ref={sourceBranchDropdownContentRef}> <DropdownMenuContent align="start" portalToBody className="w-[min(42rem,calc(100vw-2rem))] p-0 max-h-[min(var(--available-height),24rem)] flex flex-col overflow-hidden" ref={sourceBranchDropdownContentRef}>
<Command shouldFilter={false}> <Command shouldFilter={false}>
<CommandInput <CommandInput
placeholder={t('session.newWorktree.searchBranches')} placeholder={t('session.newWorktree.searchBranches')}
@@ -93,6 +93,7 @@ type ContentProps = {
align?: "start" | "center" | "end"; align?: "start" | "center" | "end";
side?: "top" | "right" | "bottom" | "left"; side?: "top" | "right" | "bottom" | "left";
alignOffset?: number; alignOffset?: number;
portalToBody?: boolean;
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string; className?: string;
children?: React.ReactNode; children?: React.ReactNode;
@@ -105,6 +106,7 @@ function DropdownMenuContent({
align, align,
side, side,
alignOffset, alignOffset,
portalToBody = false,
style, style,
children, children,
onCloseAutoFocus, onCloseAutoFocus,
@@ -114,7 +116,7 @@ function DropdownMenuContent({
void onCloseAutoFocus void onCloseAutoFocus
return ( return (
<BaseMenu.Portal container={portalContext?.portalContainer || undefined}> <BaseMenu.Portal container={portalToBody ? undefined : portalContext?.portalContainer || undefined}>
<BaseMenu.Positioner <BaseMenu.Positioner
sideOffset={sideOffset} sideOffset={sideOffset}
align={align} align={align}
@@ -71,7 +71,6 @@ import {
// Same constraints as main sidebar // Same constraints as main sidebar
const SETTINGS_NAV_MIN_WIDTH = 176; const SETTINGS_NAV_MIN_WIDTH = 176;
const SETTINGS_NAV_MAX_WIDTH = 280; const SETTINGS_NAV_MAX_WIDTH = 280;
const SETTINGS_NAV_RAIL_WIDTH = 48;
type MobileStage = 'nav' | 'page-sidebar' | 'page-content'; type MobileStage = 'nav' | 'page-sidebar' | 'page-content';
@@ -372,8 +371,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
return getSettingsPageMeta(settingsSlug); return getSettingsPageMeta(settingsSlug);
}, [settingsSlug]); }, [settingsSlug]);
// Collapse main nav to icon rail when active page has its own sidebar // Nav is always open (collapsed state removed)
const isNavCollapsed = !isMobile && activePageMeta?.kind === 'split';
const openChamberSectionBySlug: Partial<Record<SettingsPageSlug, OpenChamberSection>> = React.useMemo(() => ({ const openChamberSectionBySlug: Partial<Record<SettingsPageSlug, OpenChamberSection>> = React.useMemo(() => ({
appearance: 'visual', appearance: 'visual',
@@ -544,7 +542,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
setMobileStage('page-sidebar'); setMobileStage('page-sidebar');
}, []); }, []);
const renderSettingsNav = (collapsed: boolean) => { const renderSettingsNav = () => {
return ( return (
<div className="flex h-full flex-col overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
{/* Scrollable nav items */} {/* Scrollable nav items */}
@@ -556,7 +554,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
if (!Icon) return null; if (!Icon) return null;
return ( return (
<Tooltip key={page.slug} delayDuration={collapsed ? 100 : 600}> <Tooltip key={page.slug} delayDuration={600}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button" type="button"
@@ -569,12 +567,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
)} )}
> >
<Icon className="h-4 w-4 shrink-0" /> <Icon className="h-4 w-4 shrink-0" />
<span <span className="flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150 opacity-100">
className={cn(
'flex items-center gap-1.5 whitespace-nowrap overflow-hidden transition-opacity duration-150',
collapsed ? 'opacity-0' : 'opacity-100'
)}
>
<span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span> <span className="typography-ui-label font-normal truncate">{getPageTitle(page.slug)}</span>
{(page.slug === 'voice' || page.slug === 'tunnel') && ( {(page.slug === 'voice' || page.slug === 'tunnel') && (
<span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10"> <span className="shrink-0 typography-micro px-1 rounded leading-none pb-px text-[var(--status-warning)] bg-[var(--status-warning)]/10">
@@ -584,24 +577,14 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
</span> </span>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
{collapsed && (
<TooltipContent side="right" sideOffset={8}>
{getPageTitle(page.slug)}
</TooltipContent>
)}
</Tooltip> </Tooltip>
); );
})} })}
</div> </div>
</div> </div>
{/* Footer — hidden when collapsed via overflow on parent */} {/* Footer */}
<div <div className="overflow-hidden transition-opacity duration-150 opacity-100">
className={cn(
'overflow-hidden transition-opacity duration-150',
collapsed ? 'opacity-0 pointer-events-none' : 'opacity-100'
)}
>
<div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5"> <div className="border-t border-border bg-sidebar px-2 py-1 space-y-0.5">
{!runtimeCtx.isVSCode && ( {!runtimeCtx.isVSCode && (
<Tooltip delayDuration={300}> <Tooltip delayDuration={300}>
@@ -636,7 +619,7 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
return ( return (
<div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}> <div className={cn('flex-1 min-h-0 overflow-hidden', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')}>
<div className="flex h-full min-h-0 flex-col"> <div className="flex h-full min-h-0 flex-col">
<ErrorBoundary>{renderSettingsNav(false)}</ErrorBoundary> <ErrorBoundary>{renderSettingsNav()}</ErrorBoundary>
</div> </div>
</div> </div>
); );
@@ -686,16 +669,20 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
<div className={cn('w-[264px] min-w-[264px] border-r', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')} style={{ borderColor: 'var(--interactive-border)' }}> <div className={cn('w-[264px] min-w-[264px] border-r', runtimeCtx.isVSCode ? 'bg-background' : 'bg-sidebar')} style={{ borderColor: 'var(--interactive-border)' }}>
<ErrorBoundary>{renderPageSidebar(settingsSlug, {})}</ErrorBoundary> <ErrorBoundary>{renderPageSidebar(settingsSlug, {})}</ErrorBoundary>
</div> </div>
<div className="flex-1 overflow-hidden bg-background"> <div className="flex-1 overflow-auto scrollbar-none bg-background">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary> <div className="mx-auto w-full max-w-3xl">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
</div>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className="h-full overflow-hidden bg-background"> <div className="h-full overflow-auto scrollbar-none bg-background">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary> <div className="mx-auto w-full max-w-3xl">
<ErrorBoundary>{renderPageContent(settingsSlug)}</ErrorBoundary>
</div>
</div> </div>
); );
}; };
@@ -792,28 +779,26 @@ export const SettingsView: React.FC<SettingsViewProps> = ({ onClose, forceMobile
: runtimeCtx.isVSCode : runtimeCtx.isVSCode
? 'bg-background' ? 'bg-background'
: 'bg-sidebar', : 'bg-sidebar',
isResizing && !isNavCollapsed ? '' : 'transition-[width,min-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]' isResizing ? '' : 'transition-[width,min-width] duration-200 ease-[cubic-bezier(0.25,0.1,0.25,1)]'
)} )}
style={{ style={{
width: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`, width: `${navWidth}px`,
minWidth: isNavCollapsed ? `${SETTINGS_NAV_RAIL_WIDTH}px` : `${navWidth}px`, minWidth: `${navWidth}px`,
borderColor: 'var(--interactive-border)', borderColor: 'var(--interactive-border)',
}} }}
> >
{!isNavCollapsed && ( <div
<div className={cn(
className={cn( 'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize',
'absolute right-0 top-0 z-20 h-full w-[6px] -mr-[3px] cursor-col-resize', isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20'
isResizing ? 'bg-primary/30' : 'bg-transparent hover:bg-primary/20' )}
)} onPointerDown={handlePointerDown}
onPointerDown={handlePointerDown} role="separator"
role="separator" aria-orientation="vertical"
aria-orientation="vertical" aria-label={t('settings.view.actions.resizeNavigation')}
aria-label={t('settings.view.actions.resizeNavigation')} />
/>
)}
<ErrorBoundary> <ErrorBoundary>
{renderSettingsNav(isNavCollapsed)} {renderSettingsNav()}
</ErrorBoundary> </ErrorBoundary>
</div> </div>
@@ -41,7 +41,7 @@ export const SettingsWindow: React.FC<SettingsWindowProps> = ({ open, onOpenChan
aria-describedby={descriptionId} aria-describedby={descriptionId}
className={cn( className={cn(
'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]', 'fixed z-50 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
'w-[90vw] max-w-[960px] h-[85vh] max-h-[900px]', 'w-[90vw] max-w-[1200px] h-[85vh] max-h-[900px]',
'rounded-xl border shadow-none overflow-hidden', 'rounded-xl border shadow-none overflow-hidden',
'bg-background' 'bg-background'
)} )}