diff --git a/frontend/src/renderer/components/CenterPanelShell.tsx b/frontend/src/renderer/components/CenterPanelShell.tsx index 5e2003e0dc..6097020d3b 100644 --- a/frontend/src/renderer/components/CenterPanelShell.tsx +++ b/frontend/src/renderer/components/CenterPanelShell.tsx @@ -1,10 +1,11 @@ import type { ReactNode } from "react"; import { cn } from "../lib/utils"; import { useWindowFullScreen } from "../hooks/useWindowFullScreen"; -import { isMacPlatform } from "../lib/platform"; +import { isLinuxPlatform, isMacPlatform } from "../lib/platform"; import { useUiStore } from "../stores/ui-store"; const isMac = isMacPlatform(); +const isLinux = isLinuxPlatform(); /** * Shared inset center panel: sidebar-colored outer frame with a bordered inner @@ -12,8 +13,8 @@ const isMac = isMacPlatform(); * and settings. Chrome lives in `styles.css` (`center-panel-shell` + * `center-panel-surface`). * - * `titlebarAlign` (default true) pulls Board/Terminal titles into the macOS - * traffic-light band. + * `titlebarAlign` (default true) pulls Board/Terminal titles up level with the + * fixed TitlebarNav cluster (macOS + Linux). */ export function CenterPanelShell({ className, @@ -28,7 +29,7 @@ export function CenterPanelShell({ }) { const isSidebarOpen = useUiStore((state) => state.isSidebarOpen); const isFullScreen = useWindowFullScreen(); - const align = titlebarAlign && isMac; + const align = titlebarAlign && (isMac || isLinux); const titlebarClearance = align && !isSidebarOpen; return ( @@ -36,6 +37,9 @@ export function CenterPanelShell({ className={cn( "center-panel-shell", align && "center-panel-shell--mac", + // Linux has no traffic lights, so the sidebar-closed title clears the + // cluster at its own left edge rather than the macOS offset. + isLinux && "center-panel-shell--linux", titlebarClearance && "center-panel-shell--titlebar-clearance", titlebarClearance && isFullScreen && "center-panel-shell--titlebar-clearance-fullscreen", align && isFullScreen && "center-panel-shell--fullscreen", diff --git a/frontend/src/renderer/components/Sidebar.tsx b/frontend/src/renderer/components/Sidebar.tsx index c7f4cfbcad..23c955ada0 100644 --- a/frontend/src/renderer/components/Sidebar.tsx +++ b/frontend/src/renderer/components/Sidebar.tsx @@ -40,7 +40,6 @@ import { SidebarRail, SidebarMenuSub, SidebarMenuSubItem, - SidebarTrigger, useSidebar, } from "./ui/sidebar"; import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; @@ -51,12 +50,13 @@ import { useUiStore } from "../stores/ui-store"; import { ConfirmDialog } from "./ConfirmDialog"; import { CreateProjectFlow, type CreateProjectInput } from "./CreateProjectFlow"; import { ResizeHandle } from "./ResizeHandle"; -import { isMacPlatform, isWindowsPlatform } from "../lib/platform"; +import { isLinuxPlatform, isMacPlatform } from "../lib/platform"; -// On macOS the native traffic lights and fixed TitlebarNav occupy the titlebar -// above this surface. Win/Linux hang the sidebar under their shell chrome. +// macOS + Linux paint framed chrome: the fixed TitlebarNav cluster carries the +// sidebar toggle + history arrows above this surface. Windows hangs the sidebar +// under its custom titlebar. const isMac = isMacPlatform(); -const isWindows = isWindowsPlatform(); +const isLinux = isLinuxPlatform(); const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined; // Shared styling for the per-project hover action buttons (dashboard, @@ -262,24 +262,6 @@ export function Sidebar({ nightly )} - {!isMac && !isWindows && ( - - - - - - {isCollapsed ? "Expand sidebar · ⌘B" : "Collapse sidebar · ⌘B"} - - - )} @@ -341,7 +323,7 @@ export function Sidebar({ {/* Always-present daemon status mirror for the smoke suite: no visible diff --git a/frontend/src/renderer/components/TitlebarNav.tsx b/frontend/src/renderer/components/TitlebarNav.tsx index ac364962f6..594958137a 100644 --- a/frontend/src/renderer/components/TitlebarNav.tsx +++ b/frontend/src/renderer/components/TitlebarNav.tsx @@ -1,14 +1,17 @@ import { useCanGoBack, useRouter } from "@tanstack/react-router"; import { ArrowLeft, ArrowRight, PanelLeft } from "lucide-react"; import { useEffect, useState } from "react"; -import { isMacPlatform } from "../lib/platform"; +import { isLinuxPlatform, isMacPlatform } from "../lib/platform"; import { useUiStore } from "../stores/ui-store"; const isMac = isMacPlatform(); +const isLinux = isLinuxPlatform(); const noDragStyle = isMac ? ({ WebkitAppRegion: "no-drag" } as React.CSSProperties) : undefined; -// macOS-only sidebar chrome cluster (sidebar toggle + history arrows). It stays -// fixed while the sidebar expands, collapses, or appears as a hover preview. +// Sidebar chrome cluster (sidebar toggle + history arrows). It stays fixed while +// the sidebar expands, collapses, or appears as a hover preview. macOS pins it +// beside the traffic lights; Linux has no traffic lights, so it sits at the +// sidebar's top-left. (Windows keeps these controls in its own titlebar.) // The installed router has no useCanGoForward, and deriving one as // `__TSR_index < history.length - 1` (the upstream hook's approach) is wrong // here: window.history.length also counts entries the router never created — @@ -46,12 +49,19 @@ export function TitlebarNav({ const canGoBack = useCanGoBack(); const canGoForward = useCanGoForward(); - if (!isMac) return null; + if (!isMac && !isLinux) return null; - // Stay pinned beside the traffic lights. Nudge down slightly so the - // toggle/arrows share a centerline with the native dots (y: 12). - const leftClass = isFullScreen ? "left-titlebar-cluster-left-fullscreen" : "left-titlebar-cluster-left"; - const topClass = isFullScreen ? "top-0" : "top-0.5"; + // macOS: pinned beside the traffic lights, nudged down so the toggle/arrows + // share a centerline with the native dots (y: 12). Linux: no traffic lights, + // so it sits at the sidebar's top-left within the reserved titlebar band. + const leftClass = !isMac + ? "left-1.5" + : isFullScreen + ? "left-titlebar-cluster-left-fullscreen" + : "left-titlebar-cluster-left"; + // Linux: match the framed board titlebar's y (mac inset 2px + surface border + // 1px) so the cluster shares its centerline with the project title. + const topClass = !isMac ? "top-0.75" : isFullScreen ? "top-0" : "top-0.5"; return (
{ expect(usesBoardActionsInPanel()).toBe(false); }); - it("uses the framed app topbar on Linux", () => { + it("hides the shell topbar on Linux and keeps board actions in the panel", () => { spoofPlatform("Linux x86_64"); expect(isLinuxPlatform()).toBe(true); expect(isWindowsPlatform()).toBe(false); expect(usesFramedAppTopbar()).toBe(true); - expect(hidesShellTopbar()).toBe(false); - expect(usesBoardActionsInPanel()).toBe(false); + expect(hidesShellTopbar()).toBe(true); + expect(usesBoardActionsInPanel()).toBe(true); }); }); diff --git a/frontend/src/renderer/lib/platform.ts b/frontend/src/renderer/lib/platform.ts index 381749cebc..77eb9ca563 100644 --- a/frontend/src/renderer/lib/platform.ts +++ b/frontend/src/renderer/lib/platform.ts @@ -31,11 +31,14 @@ export function usesFramedAppTopbar(): boolean { } /** - * macOS: shell does not mount ShellTopbar (full-height inset panel + drag - * strip). SessionView mounts the same topbar in-panel for session actions. + * macOS + Linux: shell does not mount ShellTopbar (full-height inset panel). + * The sidebar toggle + history arrows live in the fixed TitlebarNav cluster and + * board/session actions mount in-panel, so both platforms share the framed + * chrome. (macOS additionally paints a traffic-light drag strip.) Windows keeps + * the ShellTopbar under its custom titlebar. */ export function hidesShellTopbar(): boolean { - return isMacPlatform(); + return isMacPlatform() || isLinuxPlatform(); } /** diff --git a/frontend/src/renderer/routes/_shell.tsx b/frontend/src/renderer/routes/_shell.tsx index 765df24726..aea7d29229 100644 --- a/frontend/src/renderer/routes/_shell.tsx +++ b/frontend/src/renderer/routes/_shell.tsx @@ -133,9 +133,6 @@ function ShellLayout() { : routeParams.sessionId ? workspaces.find((workspace) => workspace.sessions.some((session) => session.id === routeParams.sessionId))?.id : undefined; - const isSessionRoute = - Boolean(matchRoute({ to: "/projects/$projectId/sessions/$sessionId", fuzzy: true })) || - Boolean(matchRoute({ to: "/sessions/$sessionId", fuzzy: true })); // First-launch root board only (no projects in scope). const isWelcomeBoard = Boolean(matchRoute({ to: "/" })) && workspaces.length === 0; const isSettingsRoute = @@ -547,15 +544,14 @@ function ShellLayout() { } as CSSProperties } > - {/* Hang the fixed sidebar below shell chrome on Win/Linux. macOS - keeps a full-height sidebar beneath the fixed titlebar controls. */} + {/* macOS + Linux reserve a titlebar band for the fixed TitlebarNav + cluster above a full-height sidebar; Windows hangs the sidebar + below its custom titlebar. */}