feat: implement macOS-specific title bar adjustments and enhance RightPanel with toggle functionality

This commit is contained in:
Anish Sarkar 2026-05-19 18:57:06 +05:30
parent ee3a6dc45f
commit cd4e5ae7f2
5 changed files with 322 additions and 200 deletions

View file

@ -7,6 +7,7 @@ import { setActiveSearchSpaceId } from './active-search-space';
const isDev = !app.isPackaged; const isDev = !app.isPackaged;
const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string; const HOSTED_FRONTEND_URL = process.env.HOSTED_FRONTEND_URL as string;
const isMac = process.platform === 'darwin';
let mainWindow: BrowserWindow | null = null; let mainWindow: BrowserWindow | null = null;
let isQuitting = false; let isQuitting = false;
@ -35,7 +36,12 @@ export function createMainWindow(initialPath = '/dashboard'): BrowserWindow {
webviewTag: false, webviewTag: false,
}, },
show: false, show: false,
titleBarStyle: 'hiddenInset', ...(isMac
? {
titleBarStyle: 'hidden' as const,
trafficLightPosition: { x: 12, y: 10 },
}
: {}),
}); });
mainWindow.once('ready-to-show', () => { mainWindow.once('ready-to-show', () => {

View file

@ -3,7 +3,7 @@
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { PanelRight } from "lucide-react"; import { PanelRight } from "lucide-react";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { startTransition, useEffect, type MouseEvent } from "react"; import { type MouseEvent, startTransition, useEffect } from "react";
import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom"; import { closeReportPanelAtom, reportPanelAtom } from "@/atoms/chat/report-panel.atom";
import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom"; import { citationPanelAtom, closeCitationPanelAtom } from "@/atoms/citation/citation-panel.atom";
import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms"; import { documentsSidebarOpenAtom } from "@/atoms/documents/ui.atoms";
@ -12,6 +12,7 @@ import { rightPanelCollapsedAtom, rightPanelTabAtom } from "@/atoms/layout/right
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl"; import { closeHitlEditPanelAtom, hitlEditPanelAtom } from "@/features/chat-messages/hitl";
import { cn } from "@/lib/utils";
import { DocumentsSidebar } from "../sidebar"; import { DocumentsSidebar } from "../sidebar";
const EditorPanelContent = dynamic( const EditorPanelContent = dynamic(
@ -51,6 +52,8 @@ interface RightPanelProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
}; };
showCollapseButton?: boolean;
showTopBorder?: boolean;
} }
function isKeyboardClick(event: MouseEvent) { function isKeyboardClick(event: MouseEvent) {
@ -80,13 +83,66 @@ function CollapseButton({ onClick }: { onClick: () => void }) {
); );
} }
interface RightPanelToggleButtonProps {
className?: string;
iconClassName?: string;
disabled?: boolean;
}
export function RightPanelToggleButton({
className,
iconClassName,
disabled = false,
}: RightPanelToggleButtonProps) {
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom);
const hitlEditState = useAtomValue(hitlEditPanelAtom);
const citationState = useAtomValue(citationPanelAtom);
const reportOpen = reportState.isOpen && !!reportState.reportId;
const editorOpen =
editorState.isOpen &&
(editorState.kind === "document" ? !!editorState.documentId : !!editorState.localFilePath);
const hitlEditOpen = hitlEditState.isOpen && !!hitlEditState.onSave;
const citationOpen = citationState.isOpen && citationState.chunkId != null;
const hasContent = documentsOpen || reportOpen || editorOpen || hitlEditOpen || citationOpen;
const label = collapsed ? "Expand panel" : "Collapse panel";
if (!hasContent) return null;
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={disabled}
onClick={() => {
if (disabled) return;
startTransition(() => setCollapsed((value) => !value));
}}
className={cn(
"h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
className
)}
>
<PanelRight className={cn("h-4 w-4", iconClassName)} />
<span className="sr-only">{label}</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">{label}</TooltipContent>
</Tooltip>
);
}
/** /**
* Absolutely positioned expand button renders at top-right of the main * Absolutely positioned expand button renders at top-right of the main
* container so it occupies the same screen position as the collapse button * container so it occupies the same screen position as the collapse button
* inside the Documents header. * inside the Documents header.
*/ */
export function RightPanelExpandButton() { export function RightPanelExpandButton() {
const [collapsed, setCollapsed] = useAtom(rightPanelCollapsedAtom); const [collapsed] = useAtom(rightPanelCollapsedAtom);
const documentsOpen = useAtomValue(documentsSidebarOpenAtom); const documentsOpen = useAtomValue(documentsSidebarOpenAtom);
const reportState = useAtomValue(reportPanelAtom); const reportState = useAtomValue(reportPanelAtom);
const editorState = useAtomValue(editorPanelAtom); const editorState = useAtomValue(editorPanelAtom);
@ -104,24 +160,7 @@ export function RightPanelExpandButton() {
return ( return (
<div className="flex shrink-0 items-center px-0.5"> <div className="flex shrink-0 items-center px-0.5">
<Tooltip> <RightPanelToggleButton className="-m-0.5" />
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
tabIndex={-1}
onClick={(event) => {
if (isKeyboardClick(event)) return;
startTransition(() => setCollapsed(false));
}}
className="h-8 w-8 shrink-0 -m-0.5 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
>
<PanelRight className="h-4 w-4" />
<span className="sr-only">Expand panel</span>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">Expand panel</TooltipContent>
</Tooltip>
</div> </div>
); );
} }
@ -134,7 +173,11 @@ const PANEL_WIDTHS = {
citation: 560, citation: 560,
} as const; } as const;
export function RightPanel({ documentsPanel }: RightPanelProps) { export function RightPanel({
documentsPanel,
showCollapseButton = true,
showTopBorder = false,
}: RightPanelProps) {
const [activeTab] = useAtom(rightPanelTabAtom); const [activeTab] = useAtom(rightPanelTabAtom);
const reportState = useAtomValue(reportPanelAtom); const reportState = useAtomValue(reportPanelAtom);
const closeReport = useSetAtom(closeReportPanelAtom); const closeReport = useSetAtom(closeReportPanelAtom);
@ -208,14 +251,19 @@ export function RightPanel({ documentsPanel }: RightPanelProps) {
} }
const targetWidth = PANEL_WIDTHS[effectiveTab]; const targetWidth = PANEL_WIDTHS[effectiveTab];
const collapseButton = <CollapseButton onClick={() => setCollapsed(true)} />; const collapseButton = showCollapseButton ? (
<CollapseButton onClick={() => setCollapsed(true)} />
) : null;
if (!isVisible) return null; if (!isVisible) return null;
return ( return (
<aside <aside
style={{ width: targetWidth }} style={{ width: targetWidth }}
className="flex h-full shrink-0 flex-col border-l bg-panel text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out" className={cn(
"flex h-full shrink-0 flex-col border-l bg-panel text-sidebar-foreground overflow-hidden transition-[width] duration-200 ease-out",
showTopBorder && "border-t"
)}
> >
<div className="relative flex-1 min-h-0 overflow-hidden"> <div className="relative flex-1 min-h-0 overflow-hidden">
{effectiveTab === "sources" && documentsOpen && documentsPanel && ( {effectiveTab === "sources" && documentsOpen && documentsPanel && (

View file

@ -9,6 +9,7 @@ import { Spinner } from "@/components/ui/spinner";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import type { InboxItem } from "@/hooks/use-inbox"; import type { InboxItem } from "@/hooks/use-inbox";
import { useIsMobile } from "@/hooks/use-mobile"; import { useIsMobile } from "@/hooks/use-mobile";
import { useElectronAPI } from "@/hooks/use-platform";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { SidebarProvider, useSidebarState } from "../../hooks"; import { SidebarProvider, useSidebarState } from "../../hooks";
import { import {
@ -19,7 +20,11 @@ import {
import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types"; import type { ChatItem, NavItem, PageUsage, SearchSpace, User } from "../../types/layout.types";
import { Header } from "../header"; import { Header } from "../header";
import { IconRail } from "../icon-rail"; import { IconRail } from "../icon-rail";
import { RightPanel, RightPanelExpandButton } from "../right-panel/RightPanel"; import {
RightPanel,
RightPanelExpandButton,
RightPanelToggleButton,
} from "../right-panel/RightPanel";
import { import {
AllPrivateChatsSidebarContent, AllPrivateChatsSidebarContent,
AllSharedChatsSidebarContent, AllSharedChatsSidebarContent,
@ -28,6 +33,7 @@ import {
MobileSidebar, MobileSidebar,
MobileSidebarTrigger, MobileSidebarTrigger,
Sidebar, Sidebar,
SidebarCollapseButton,
} from "../sidebar"; } from "../sidebar";
import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel"; import { SidebarSlideOutPanel } from "../sidebar/SidebarSlideOutPanel";
import { TabBar } from "../tabs/TabBar"; import { TabBar } from "../tabs/TabBar";
@ -45,6 +51,36 @@ const DocumentTabContent = dynamic(
} }
); );
function MacDesktopTitleBar({
isSidebarCollapsed,
onToggleSidebar,
disableRightPanelToggle = false,
}: {
isSidebarCollapsed: boolean;
onToggleSidebar: () => void;
disableRightPanelToggle?: boolean;
}) {
return (
<div className="flex h-9 shrink-0 items-center bg-rail px-2 [app-region:drag] [-webkit-app-region:drag]">
<div className="ml-[72px] flex h-full items-center [app-region:no-drag] [-webkit-app-region:no-drag]">
<SidebarCollapseButton
isCollapsed={isSidebarCollapsed}
onToggle={onToggleSidebar}
className="h-6 w-6 rounded-md"
iconClassName="h-3.5 w-3.5"
/>
</div>
<div className="ml-auto flex h-full items-center [app-region:no-drag] [-webkit-app-region:no-drag]">
<RightPanelToggleButton
disabled={disableRightPanelToggle}
className="h-6 w-6 rounded-md"
iconClassName="h-3.5 w-3.5"
/>
</div>
</div>
);
}
// Per-tab data source // Per-tab data source
interface TabDataSource { interface TabDataSource {
items: InboxItem[]; items: InboxItem[];
@ -130,22 +166,28 @@ function MainContentPanel({
isChatPage, isChatPage,
onTabSwitch, onTabSwitch,
onNewChat, onNewChat,
showRightPanelExpandButton = true,
showTopBorder = false,
children, children,
}: { }: {
isChatPage: boolean; isChatPage: boolean;
onTabSwitch?: (tab: Tab) => void; onTabSwitch?: (tab: Tab) => void;
onNewChat?: () => void; onNewChat?: () => void;
showRightPanelExpandButton?: boolean;
showTopBorder?: boolean;
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const activeTab = useAtomValue(activeTabAtom); const activeTab = useAtomValue(activeTabAtom);
const isDocumentTab = activeTab?.type === "document"; const isDocumentTab = activeTab?.type === "document";
return ( return (
<div className="relative isolate flex flex-1 flex-col min-w-0"> <div
className={cn("relative isolate flex flex-1 flex-col min-w-0", showTopBorder && "border-t")}
>
<TabBar <TabBar
onTabSwitch={onTabSwitch} onTabSwitch={onTabSwitch}
onNewChat={onNewChat} onNewChat={onNewChat}
rightActions={<RightPanelExpandButton />} rightActions={showRightPanelExpandButton ? <RightPanelExpandButton /> : null}
className="min-w-0" className="min-w-0"
/> />
<div className="relative flex flex-1 flex-col bg-panel overflow-hidden min-w-0"> <div className="relative flex flex-1 flex-col bg-panel overflow-hidden min-w-0">
@ -221,6 +263,8 @@ export function LayoutShell({
onTabSwitch, onTabSwitch,
}: LayoutShellProps) { }: LayoutShellProps) {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const electronAPI = useElectronAPI();
const isMacDesktop = electronAPI?.versions.platform === "darwin";
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed); const { isCollapsed, setIsCollapsed, toggleCollapsed } = useSidebarState(defaultCollapsed);
const { const {
@ -388,13 +432,16 @@ export function LayoutShell({
return ( return (
<SidebarProvider value={sidebarContextValue}> <SidebarProvider value={sidebarContextValue}>
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
<div <div className={cn("flex h-screen w-full flex-col overflow-hidden bg-rail", className)}>
className={cn( {isMacDesktop ? (
"flex h-screen w-full gap-2 px-2 py-0 overflow-hidden bg-rail", <MacDesktopTitleBar
className isSidebarCollapsed={isCollapsed}
)} onToggleSidebar={toggleCollapsed}
> disableRightPanelToggle={useWorkspacePanel}
<div className="hidden md:flex overflow-hidden border-r -mr-2 pr-2 bg-rail"> />
) : null}
<div className="flex min-h-0 flex-1 w-full gap-2 px-2 py-0 overflow-hidden">
<div className="hidden md:flex overflow-hidden -mr-2 pr-2 bg-rail">
<IconRail <IconRail
searchSpaces={searchSpaces} searchSpaces={searchSpaces}
activeSearchSpaceId={activeSearchSpaceId} activeSearchSpaceId={activeSearchSpaceId}
@ -414,7 +461,12 @@ export function LayoutShell({
</div> </div>
{/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */} {/* Sidebar + slide-out panels share one container; overflow visible so panels can overlay main content. Negative right margin closes the flex gap so the sidebar sits flush against the main panel, separated only by a border. */}
<div className="relative hidden md:flex shrink-0 z-20 -mr-2 border-r bg-panel"> <div
className={cn(
"relative hidden md:flex shrink-0 z-20 -mr-2 bg-panel",
isMacDesktop ? "rounded-tl-xl border-t border-r border-l" : "border-r"
)}
>
<Sidebar <Sidebar
searchSpace={searchSpace} searchSpace={searchSpace}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
@ -444,7 +496,8 @@ export function LayoutShell({
theme={theme} theme={theme}
setTheme={setTheme} setTheme={setTheme}
renderUserProfile={false} renderUserProfile={false}
className="flex shrink-0" renderCollapseButton={!isMacDesktop}
className={cn("flex shrink-0", isMacDesktop && "rounded-tl-xl")}
isLoadingChats={isLoadingChats} isLoadingChats={isLoadingChats}
sidebarWidth={sidebarWidth} sidebarWidth={sidebarWidth}
isResizing={isResizing} isResizing={isResizing}
@ -529,6 +582,7 @@ export function LayoutShell({
<DesktopWorkspaceRegion> <DesktopWorkspaceRegion>
{useWorkspacePanel ? ( {useWorkspacePanel ? (
<WorkspacePanel <WorkspacePanel
className={isMacDesktop ? "border-t" : undefined}
viewportClassName={workspacePanelViewportClassName} viewportClassName={workspacePanelViewportClassName}
contentClassName={workspacePanelContentClassName} contentClassName={workspacePanelContentClassName}
> >
@ -541,6 +595,8 @@ export function LayoutShell({
isChatPage={isChatPage} isChatPage={isChatPage}
onTabSwitch={onTabSwitch} onTabSwitch={onTabSwitch}
onNewChat={onNewChat} onNewChat={onNewChat}
showRightPanelExpandButton={!isMacDesktop}
showTopBorder={isMacDesktop}
> >
{children} {children}
</MainContentPanel> </MainContentPanel>
@ -552,12 +608,15 @@ export function LayoutShell({
open: documentsPanel.open, open: documentsPanel.open,
onOpenChange: documentsPanel.onOpenChange, onOpenChange: documentsPanel.onOpenChange,
}} }}
showCollapseButton={!isMacDesktop}
showTopBorder={isMacDesktop}
/> />
) : null} ) : null}
</> </>
)} )}
</DesktopWorkspaceRegion> </DesktopWorkspaceRegion>
</div> </div>
</div>
</TooltipProvider> </TooltipProvider>
</SidebarProvider> </SidebarProvider>
); );

View file

@ -95,6 +95,7 @@ interface SidebarProps {
sidebarWidth?: number; sidebarWidth?: number;
isResizing?: boolean; isResizing?: boolean;
renderUserProfile?: boolean; renderUserProfile?: boolean;
renderCollapseButton?: boolean;
} }
export function Sidebar({ export function Sidebar({
@ -132,6 +133,7 @@ export function Sidebar({
sidebarWidth = SIDEBAR_MIN_WIDTH, sidebarWidth = SIDEBAR_MIN_WIDTH,
isResizing = false, isResizing = false,
renderUserProfile = true, renderUserProfile = true,
renderCollapseButton = true,
}: SidebarProps) { }: SidebarProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null); const [openDropdownChatId, setOpenDropdownChatId] = useState<number | null>(null);
@ -176,6 +178,7 @@ export function Sidebar({
onManageMembers={onManageMembers} onManageMembers={onManageMembers}
/> />
</div> </div>
{renderCollapseButton ? (
<div className={cn("shrink-0", isCollapsed && "mx-auto")}> <div className={cn("shrink-0", isCollapsed && "mx-auto")}>
<SidebarCollapseButton <SidebarCollapseButton
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
@ -183,6 +186,7 @@ export function Sidebar({
disableTooltip={disableTooltips} disableTooltip={disableTooltips}
/> />
</div> </div>
) : null}
</div> </div>
<div className="flex flex-col gap-0.5 py-1.5"> <div className="flex flex-col gap-0.5 py-1.5">
@ -383,10 +387,7 @@ function SidebarUsageFooter({
if (isCollapsed) return null; if (isCollapsed) return null;
const containerClass = cn( const containerClass = cn("px-3 py-3 space-y-3", hasNavSectionAbove && "border-t");
"px-3 py-3 space-y-3",
hasNavSectionAbove && "border-t"
);
if (isAnonymous) { if (isAnonymous) {
return ( return (

View file

@ -6,17 +6,22 @@ import { Button } from "@/components/ui/button";
import { ShortcutKbd } from "@/components/ui/shortcut-kbd"; import { ShortcutKbd } from "@/components/ui/shortcut-kbd";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { usePlatformShortcut } from "@/hooks/use-platform-shortcut"; import { usePlatformShortcut } from "@/hooks/use-platform-shortcut";
import { cn } from "@/lib/utils";
interface SidebarCollapseButtonProps { interface SidebarCollapseButtonProps {
isCollapsed: boolean; isCollapsed: boolean;
onToggle: () => void; onToggle: () => void;
disableTooltip?: boolean; disableTooltip?: boolean;
className?: string;
iconClassName?: string;
} }
export function SidebarCollapseButton({ export function SidebarCollapseButton({
isCollapsed, isCollapsed,
onToggle, onToggle,
disableTooltip = false, disableTooltip = false,
className,
iconClassName,
}: SidebarCollapseButtonProps) { }: SidebarCollapseButtonProps) {
const t = useTranslations("sidebar"); const t = useTranslations("sidebar");
const { shortcutKeys } = usePlatformShortcut(); const { shortcutKeys } = usePlatformShortcut();
@ -26,9 +31,12 @@ export function SidebarCollapseButton({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onToggle} onClick={onToggle}
className="h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground" className={cn(
"h-8 w-8 shrink-0 text-muted-foreground hover:bg-accent hover:text-accent-foreground",
className
)}
> >
<PanelLeft className="h-4 w-4" /> <PanelLeft className={cn("h-4 w-4", iconClassName)} />
<span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span> <span className="sr-only">{isCollapsed ? t("expand_sidebar") : t("collapse_sidebar")}</span>
</Button> </Button>
); );