authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-22 14:00:30-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 14:00:30-07:00
logb2d17b7108766a989374d670ff12de39b6c4d033
treec555eb0670a5cbff9244901e35f9e9be88a9c033
parentfb1034546f989c11568ef1c4b5801e6a5372a07d
signature Signed by PGP key B5690EEEBB952194

Add main sidebar toggle (#3497)

Co-authored-by: Julius Marminge <julius@mac.lan> Co-authored-by: codex <codex@users.noreply.github.com>

17 files changed, 247 insertions(+), 74 deletions(-)

apps/server/src/keybindings.test.ts+1
......@@ -198,6 +198,7 @@ it.layer(NodeServices.layer)("keybindings", (it) => {
198198 assert.equal(defaultsByCommand.get("thread.jump.1"), "mod+1");
199199 assert.equal(defaultsByCommand.get("thread.jump.9"), "mod+9");
200200 assert.equal(defaultsByCommand.get("modelPicker.toggle"), "mod+shift+m");
201 assert.equal(defaultsByCommand.get("sidebar.toggle"), "mod+b");
201202 assert.equal(defaultsByCommand.get("rightPanel.toggle"), "mod+alt+b");
202203 assert.equal(defaultsByCommand.get("terminal.splitVertical"), "mod+shift+d");
203204 assert.equal(defaultsByCommand.get("modelPicker.jump.1"), "mod+1");
apps/web/src/components/AppSidebarLayout.tsx+54-3
......@@ -1,14 +1,64 @@
1import { useEffect, type ReactNode } from "react";
1import { useAtomValue } from "@effect/atom-react";
2import { useEffect, type CSSProperties, type ReactNode } from "react";
23import { useNavigate } from "@tanstack/react-router";
34
5import { isElectron } from "../env";
6import { resolveShortcutCommand, shortcutLabelForCommand } from "../keybindings";
7import { isMacPlatform } from "../lib/utils";
8import { primaryServerKeybindingsAtom } from "../state/server";
49import ThreadSidebar from "./Sidebar";
5import { Sidebar, SidebarProvider, SidebarRail } from "./ui/sidebar";
10import { Sidebar, SidebarProvider, SidebarRail, SidebarTrigger, useSidebar } from "./ui/sidebar";
11import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
612
713const THREAD_SIDEBAR_WIDTH_STORAGE_KEY = "chat_thread_sidebar_width";
814const THREAD_SIDEBAR_MIN_WIDTH = 13 * 16;
915const THREAD_MAIN_CONTENT_MIN_WIDTH = 40 * 16;
16const MACOS_TRAFFIC_LIGHTS_LEFT_INSET = "90px";
17
18function SidebarControl() {
19 const keybindings = useAtomValue(primaryServerKeybindingsAtom);
20 const { toggleSidebar } = useSidebar();
21 const shortcutLabel = shortcutLabelForCommand(keybindings, "sidebar.toggle");
22
23 useEffect(() => {
24 const onKeyDown = (event: KeyboardEvent) => {
25 if (event.defaultPrevented) return;
26 if (resolveShortcutCommand(event, keybindings) !== "sidebar.toggle") return;
27
28 event.preventDefault();
29 event.stopPropagation();
30 toggleSidebar();
31 };
32
33 window.addEventListener("keydown", onKeyDown);
34 return () => window.removeEventListener("keydown", onKeyDown);
35 }, [keybindings, toggleSidebar]);
36
37 return (
38 <div
39 className="pointer-events-none fixed left-[var(--workspace-controls-left)] top-[var(--workspace-controls-top)] z-50 flex h-[var(--workspace-topbar-height)] items-center"
40 data-sidebar-control=""
41 >
42 <Tooltip>
43 <TooltipTrigger
44 render={
45 <SidebarTrigger className="pointer-events-auto" aria-label="Toggle main sidebar" />
46 }
47 />
48 <TooltipPopup side="bottom">
49 Toggle main sidebar{shortcutLabel ? ` (${shortcutLabel})` : ""}
50 </TooltipPopup>
51 </Tooltip>
52 </div>
53 );
54}
55
1056export function AppSidebarLayout({ children }: { children: ReactNode }) {
1157 const navigate = useNavigate();
58 const macosWindowControlsStyle =
59 isElectron && isMacPlatform(navigator.platform)
60 ? ({ "--workspace-controls-left": MACOS_TRAFFIC_LIGHTS_LEFT_INSET } as CSSProperties)
61 : undefined;
1262
1363 useEffect(() => {
1464 const onMenuAction = window.desktopBridge?.onMenuAction;
......@@ -28,7 +78,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
2878 }, [navigate]);
2979
3080 return (
31 <SidebarProvider className="h-dvh! min-h-0!" defaultOpen>
81 <SidebarProvider className="h-dvh! min-h-0!" defaultOpen style={macosWindowControlsStyle}>
3282 <Sidebar
3383 side="left"
3484 collapsible="offcanvas"
......@@ -44,6 +94,7 @@ export function AppSidebarLayout({ children }: { children: ReactNode }) {
4494 <SidebarRail />
4595 </Sidebar>
4696 {children}
97 <SidebarControl />
4798 </SidebarProvider>
4899 );
49100}
apps/web/src/components/ChatView.tsx+3-1
......@@ -128,6 +128,7 @@ import PlanSidebar from "./PlanSidebar";
128128import ThreadTerminalDrawer from "./ThreadTerminalDrawer";
129129import { ChevronDownIcon, TriangleAlertIcon, WifiOffIcon } from "lucide-react";
130130import { cn, randomHex } from "~/lib/utils";
131import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
131132import { stackedThreadToast, toastManager } from "./ui/toast";
132133import { decodeProjectScriptKeybindingRule } from "~/lib/projectScriptKeybindings";
133134import { type NewProjectScriptInput } from "./ProjectScriptsControl";
......@@ -4677,7 +4678,7 @@ function ChatViewContent(props: ChatViewProps) {
46774678 <header
46784679 data-chat-header
46794680 className={cn(
4680 "border-b border-border",
4681 "border-b border-border transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none",
46814682 isElectron
46824683 ? cn(
46834684 "workspace-topbar drag-region relative px-3 sm:px-5",
......@@ -4686,6 +4687,7 @@ function ChatViewContent(props: ChatViewProps) {
46864687 "wco:pr-[var(--workspace-native-controls-inset)]",
46874688 )
46884689 : "workspace-topbar pl-[calc(env(safe-area-inset-left)+0.75rem)] pr-[calc(env(safe-area-inset-right)+0.75rem)] sm:pl-[calc(env(safe-area-inset-left)+1.25rem)] sm:pr-[calc(env(safe-area-inset-right)+1.25rem)]",
4690 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
46894691 )}
46904692 >
46914693 {!rightPanelOpen ? panelLayoutControls : null}
apps/web/src/components/NoActiveThreadState.tsx+4-3
......@@ -1,7 +1,8 @@
11import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "./ui/empty";
2import { SidebarInset, SidebarTrigger } from "./ui/sidebar";
2import { SidebarInset } from "./ui/sidebar";
33import { isElectron } from "../env";
44import { cn } from "~/lib/utils";
5import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
56
67export function NoActiveThreadState() {
78 return (
......@@ -9,8 +10,9 @@ export function NoActiveThreadState() {
910 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
1011 <header
1112 className={cn(
12 "border-b border-border px-3 sm:px-5",
13 "border-b border-border px-3 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
1314 isElectron ? "workspace-topbar drag-region" : "workspace-topbar",
15 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
1416 )}
1517 >
1618 {isElectron ? (
......@@ -19,7 +21,6 @@ export function NoActiveThreadState() {
1921 </span>
2022 ) : (
2123 <div className="flex items-center gap-2">
22 <SidebarTrigger className="size-7 shrink-0 md:hidden" />
2324 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
2425 No active thread
2526 </span>
apps/web/src/components/Sidebar.tsx+53-53
......@@ -70,7 +70,7 @@ import {
7070 type SidebarThreadSortOrder,
7171} from "@t3tools/contracts/settings";
7272import { isElectron } from "../env";
73import { APP_STAGE_LABEL, APP_VERSION } from "../branding";
73import { APP_STAGE_LABEL } from "../branding";
7474import { useOpenPrLink } from "../lib/openPullRequestLink";
7575import { isTerminalFocused } from "../lib/terminalFocus";
7676import { isMacPlatform } from "../lib/utils";
......@@ -187,12 +187,12 @@ import {
187187 resolveProjectStatusIndicator,
188188 resolveSidebarNewThreadSeedContext,
189189 resolveSidebarNewThreadEnvMode,
190 resolveSidebarStageBadgeLabel,
190191 resolveThreadRowClassName,
191192 resolveThreadStatusPill,
192193 orderItemsByPreferredIds,
193194 shouldClearThreadSelectionOnMouseDown,
194195 sortProjectsForSidebar,
195 resolveSidebarStageBadgeLabel,
196196 useThreadJumpHintVisibility,
197197 ThreadStatusPill,
198198} from "./Sidebar.logic";
......@@ -2452,22 +2452,6 @@ const SidebarProjectListRow = memo(function SidebarProjectListRow(props: Sidebar
24522452 );
24532453});
24542454
2455function T3Wordmark() {
2456 return (
2457 <svg
2458 aria-label="T3"
2459 className="h-2.5 w-auto shrink-0 text-foreground"
2460 viewBox="15.5309 37 94.3941 56.96"
2461 xmlns="http://www.w3.org/2000/svg"
2462 >
2463 <path
2464 d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
2465 fill="currentColor"
2466 />
2467 </svg>
2468 );
2469}
2470
24712455type SortableProjectHandleProps = Pick<
24722456 ReturnType<typeof useSortable>,
24732457 "attributes" | "listeners" | "setActivatorNodeRef"
......@@ -2664,48 +2648,64 @@ const SidebarChromeHeader = memo(function SidebarChromeHeader({
26642648}: {
26652649 isElectron: boolean;
26662650}) {
2651 return isElectron ? (
2652 <SidebarHeader className="@container/sidebar-header drag-region h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0">
2653 <SidebarTrigger className="md:hidden" />
2654 <SidebarBrand />
2655 </SidebarHeader>
2656 ) : (
2657 <SidebarHeader className="@container/sidebar-header h-[var(--workspace-topbar-height)] shrink-0 flex-row items-center px-3 py-0 md:px-0">
2658 <SidebarTrigger className="md:hidden" />
2659 <SidebarBrand />
2660 </SidebarHeader>
2661 );
2662});
2663
2664function SidebarBrand() {
2665 const stageLabel = useSidebarStageLabel();
2666
2667 return (
2668 <Link
2669 aria-label="Go to threads"
2670 className="sidebar-brand ml-[var(--workspace-titlebar-content-left)] h-7 w-fit min-w-0 shrink-0 items-center gap-1 overflow-hidden rounded-md text-foreground outline-hidden ring-ring focus-visible:ring-2"
2671 to="/"
2672 >
2673 <T3Wordmark />
2674 <span className="truncate text-sm font-medium tracking-tight text-muted-foreground">
2675 Code
2676 </span>
2677 <span className="sidebar-brand-stage shrink-0 items-center whitespace-nowrap rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60">
2678 {stageLabel}
2679 </span>
2680 </Link>
2681 );
2682}
2683
2684function useSidebarStageLabel() {
26672685 const primaryServerVersion =
26682686 useAtomValue(primaryServerConfigAtom)?.environment.serverVersion ?? null;
2669 const stageBadgeLabel = resolveSidebarStageBadgeLabel({
2687
2688 return resolveSidebarStageBadgeLabel({
26702689 primaryServerVersion,
26712690 fallbackStageLabel: APP_STAGE_LABEL,
26722691 });
2673 const wordmark = (
2674 <div className="flex items-center gap-2">
2675 <SidebarTrigger className="shrink-0 md:hidden" />
2676 <Tooltip>
2677 <TooltipTrigger
2678 render={
2679 <Link
2680 aria-label="Go to threads"
2681 className="ml-1 flex min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md outline-hidden ring-ring transition-colors hover:text-foreground focus-visible:ring-2"
2682 to="/"
2683 >
2684 <T3Wordmark />
2685 <span className="truncate text-sm font-medium tracking-tight text-muted-foreground">
2686 Code
2687 </span>
2688 <span className="rounded-full bg-muted/50 px-1.5 py-0.5 text-[8px] font-medium uppercase tracking-[0.18em] text-muted-foreground/60">
2689 {stageBadgeLabel}
2690 </span>
2691 </Link>
2692 }
2693 />
2694 <TooltipPopup side="bottom" sideOffset={2}>
2695 Version {APP_VERSION}
2696 </TooltipPopup>
2697 </Tooltip>
2698 </div>
2699 );
2692}
27002693
2701 return isElectron ? (
2702 <SidebarHeader className="drag-region h-[52px] flex-row items-center gap-2 px-4 py-0 pl-[90px] wco:h-[env(titlebar-area-height)] wco:pl-[calc(env(titlebar-area-x)+1em)]">
2703 {wordmark}
2704 </SidebarHeader>
2705 ) : (
2706 <SidebarHeader className="gap-3 px-3 py-2 sm:gap-2.5 sm:px-4 sm:py-3">{wordmark}</SidebarHeader>
2694function T3Wordmark() {
2695 return (
2696 <svg
2697 aria-label="T3"
2698 className="h-2.5 w-auto shrink-0 text-foreground"
2699 viewBox="15.5309 37 94.3941 56.96"
2700 xmlns="http://www.w3.org/2000/svg"
2701 >
2702 <path
2703 d="M33.4509 93V47.56H15.5309V37H64.3309V47.56H46.4109V93H33.4509ZM86.7253 93.96C82.832 93.96 78.9653 93.4533 75.1253 92.44C71.2853 91.3733 68.032 89.88 65.3653 87.96L70.4053 78.04C72.5386 79.5867 75.0186 80.8133 77.8453 81.72C80.672 82.6267 83.5253 83.08 86.4053 83.08C89.6586 83.08 92.2186 82.44 94.0853 81.16C95.952 79.88 96.8853 78.12 96.8853 75.88C96.8853 73.7467 96.0586 72.0667 94.4053 70.84C92.752 69.6133 90.0853 69 86.4053 69H80.4853V60.44L96.0853 42.76L97.5253 47.4H68.1653V37H107.365V45.4L91.8453 63.08L85.2853 59.32H89.0453C95.9253 59.32 101.125 60.8667 104.645 63.96C108.165 67.0533 109.925 71.0267 109.925 75.88C109.925 79.0267 109.099 81.9867 107.445 84.76C105.792 87.48 103.259 89.6933 99.8453 91.4C96.432 93.1067 92.0586 93.96 86.7253 93.96Z"
2704 fill="currentColor"
2705 />
2706 </svg>
27072707 );
2708});
2708}
27092709
27102710const SidebarChromeFooter = memo(function SidebarChromeFooter() {
27112711 const navigate = useNavigate();
apps/web/src/components/chat/ChatHeader.tsx-2
......@@ -14,7 +14,6 @@ import ProjectScriptsControl, {
1414 type NewProjectScriptInput,
1515 type ProjectScriptActionResult,
1616} from "../ProjectScriptsControl";
17import { SidebarTrigger } from "../ui/sidebar";
1817import { OpenInPicker } from "./OpenInPicker";
1918import { usePrimaryEnvironmentId } from "../../state/environments";
2019import { cn } from "~/lib/utils";
......@@ -80,7 +79,6 @@ export const ChatHeader = memo(function ChatHeader({
8079 return (
8180 <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2 sm:gap-3">
8281 <div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
83 <SidebarTrigger className="size-7 shrink-0 md:hidden" />
8482 <Tooltip>
8583 <TooltipTrigger
8684 render={
apps/web/src/components/ui/sidebar.test.tsx+33
......@@ -6,7 +6,9 @@ import {
66 SidebarMenuButton,
77 SidebarMenuSubButton,
88 SidebarProvider,
9 SidebarTrigger,
910} from "./sidebar";
11import { resolveSidebarState } from "./sidebarState";
1012
1113function renderSidebarButton(className?: string) {
1214 return renderToStaticMarkup(
......@@ -17,6 +19,37 @@ function renderSidebarButton(className?: string) {
1719}
1820
1921describe("sidebar interactive cursors", () => {
22 it("uses mobile sheet visibility for the shared responsive state", () => {
23 expect(resolveSidebarState({ isMobile: true, open: true, openMobile: false })).toBe(
24 "collapsed",
25 );
26 expect(resolveSidebarState({ isMobile: true, open: false, openMobile: true })).toBe("expanded");
27 expect(resolveSidebarState({ isMobile: false, open: true, openMobile: false })).toBe(
28 "expanded",
29 );
30 });
31
32 it("exposes collapsed state for shared titlebar inset styling", () => {
33 const html = renderToStaticMarkup(
34 <SidebarProvider defaultOpen={false}>
35 <div />
36 </SidebarProvider>,
37 );
38
39 expect(html).toContain('data-sidebar-state="collapsed"');
40 });
41
42 it("keeps the sidebar trigger interactive inside Electron drag regions", () => {
43 const html = renderToStaticMarkup(
44 <SidebarProvider>
45 <SidebarTrigger />
46 </SidebarProvider>,
47 );
48
49 expect(html).toContain("[-webkit-app-region:no-drag]");
50 expect(html).toContain("size-[var(--workspace-titlebar-control-size)]!");
51 });
52
2053 it("uses a pointer cursor for menu buttons by default", () => {
2154 const html = renderSidebarButton();
2255
apps/web/src/components/ui/sidebar.tsx+18-5
......@@ -19,6 +19,7 @@ import { Skeleton } from "~/components/ui/skeleton";
1919import { Tooltip, TooltipPopup, TooltipTrigger } from "~/components/ui/tooltip";
2020import { useIsMobile } from "~/hooks/useMediaQuery";
2121import { getLocalStorageItem, setLocalStorageItem } from "~/hooks/useLocalStorage";
22import { resolveSidebarState, type ResponsiveSidebarState } from "./sidebarState";
2223import * as Schema from "effect/Schema";
2324
2425const SIDEBAR_COOKIE_NAME = "sidebar_state";
......@@ -29,7 +30,7 @@ const SIDEBAR_WIDTH_ICON = "3rem";
2930const SIDEBAR_RESIZE_DEFAULT_MIN_WIDTH = 16 * 16;
3031
3132type SidebarContextProps = {
32 state: "expanded" | "collapsed";
33 state: ResponsiveSidebarState;
3334 open: boolean;
3435 setOpen: (open: boolean) => void;
3536 openMobile: boolean;
......@@ -85,6 +86,11 @@ function useSidebar() {
8586 return context;
8687}
8788
89function useSidebarVisibility() {
90 const { isMobile, open, openMobile } = useSidebar();
91 return isMobile ? openMobile : open;
92}
93
8894function SidebarProvider({
8995 defaultOpen = true,
9096 open: openProp,
......@@ -132,7 +138,7 @@ function SidebarProvider({
132138
133139 // We add a state so that we can do data-state="expanded" or "collapsed".
134140 // This makes it easier to style the sidebar with Tailwind classes.
135 const state = open ? "expanded" : "collapsed";
141 const state = resolveSidebarState({ isMobile, open, openMobile });
136142
137143 const contextValue = React.useMemo<SidebarContextProps>(
138144 () => ({
......@@ -154,6 +160,7 @@ function SidebarProvider({
154160 "group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
155161 className,
156162 )}
163 data-sidebar-state={state}
157164 data-slot="sidebar-wrapper"
158165 style={
159166 {
......@@ -310,13 +317,18 @@ function Sidebar({
310317}
311318
312319function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<typeof Button>) {
313 const { toggleSidebar, openMobile } = useSidebar();
320 const { toggleSidebar } = useSidebar();
321 const isOpen = useSidebarVisibility();
314322
315323 return (
316324 <Button
317 className={cn("size-7", className)}
325 className={cn(
326 "size-[var(--workspace-titlebar-control-size)]! [-webkit-app-region:no-drag]",
327 className,
328 )}
318329 data-sidebar="trigger"
319330 data-slot="sidebar-trigger"
331 aria-pressed={isOpen}
320332 onClick={(event) => {
321333 onClick?.(event);
322334 toggleSidebar();
......@@ -325,7 +337,7 @@ function SidebarTrigger({ className, onClick, ...props }: React.ComponentProps<t
325337 variant="ghost"
326338 {...props}
327339 >
328 {openMobile ? <PanelLeftCloseIcon /> : <PanelLeftIcon />}
340 {isOpen ? <PanelLeftCloseIcon /> : <PanelLeftIcon />}
329341 <span className="sr-only">Toggle Sidebar</span>
330342 </Button>
331343 );
......@@ -1004,4 +1016,5 @@ export {
10041016 SidebarSeparator,
10051017 SidebarTrigger,
10061018 useSidebar,
1019 useSidebarVisibility,
10071020};
apps/web/src/components/ui/sidebarState.ts created+9
......@@ -0,0 +1,9 @@
1export type ResponsiveSidebarState = "expanded" | "collapsed";
2
3export function resolveSidebarState(input: {
4 isMobile: boolean;
5 open: boolean;
6 openMobile: boolean;
7}): ResponsiveSidebarState {
8 return (input.isMobile ? input.openMobile : input.open) ? "expanded" : "collapsed";
9}
apps/web/src/index.css+33
......@@ -7,13 +7,24 @@
77:root {
88 --workspace-topbar-height: 52px;
99 --workspace-controls-top: 0px;
10 --workspace-controls-left: calc(env(safe-area-inset-left) + 0.75rem);
1011 --workspace-controls-right: calc(env(safe-area-inset-right) + 0.75rem);
1112 --workspace-native-controls-inset: 0px;
13 --workspace-titlebar-control-size: 1.75rem;
14 --workspace-titlebar-control-gap: 0.75rem;
15}
16
17[data-slot="sidebar-wrapper"] {
18 --workspace-titlebar-content-left: calc(
19 var(--workspace-controls-left) + var(--workspace-titlebar-control-size) +
20 var(--workspace-titlebar-control-gap)
21 );
1222}
1323
1424.wco {
1525 --workspace-topbar-height: env(titlebar-area-height, 52px);
1626 --workspace-controls-top: env(titlebar-area-y, 0px);
27 --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem);
1728 --workspace-controls-right: calc(
1829 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem
1930 );
......@@ -90,6 +101,28 @@
90101}
91102
92103@layer components {
104 .sidebar-brand {
105 display: none;
106 }
107
108 .sidebar-brand-stage {
109 display: none;
110 }
111
112 @media (min-width: 48rem) {
113 @container sidebar-header (min-width: 13.5rem) {
114 .sidebar-brand {
115 display: flex;
116 }
117 }
118
119 @container sidebar-header (min-width: 15.75rem) {
120 .sidebar-brand-stage {
121 display: inline-flex;
122 }
123 }
124 }
125
93126 .workspace-topbar {
94127 display: flex;
95128 height: var(--workspace-topbar-height);
apps/web/src/keybindings.test.ts+5
......@@ -85,6 +85,7 @@ function compile(bindings: TestBinding[]): ResolvedKeybindingsConfig {
8585}
8686
8787const DEFAULT_BINDINGS = compile([
88 { shortcut: modShortcut("b"), command: "sidebar.toggle" },
8889 { shortcut: modShortcut("j"), command: "terminal.toggle" },
8990 { shortcut: modShortcut("b", { altKey: true }), command: "rightPanel.toggle" },
9091 {
......@@ -312,6 +313,10 @@ describe("shortcutLabelForCommand", () => {
312313 });
313314
314315 it("returns effective labels for non-terminal commands", () => {
316 assert.strictEqual(
317 shortcutLabelForCommand(DEFAULT_BINDINGS, "sidebar.toggle", "MacIntel"),
318 "⌘B",
319 );
315320 assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "chat.new", "MacIntel"), "⇧⌘O");
316321 assert.strictEqual(shortcutLabelForCommand(DEFAULT_BINDINGS, "diff.toggle", "Linux"), "Ctrl+D");
317322 assert.strictEqual(
apps/web/src/routes/_chat.index.tsx+9-3
......@@ -4,10 +4,12 @@ import { LinkIcon, PlusIcon } from "lucide-react";
44import { NoActiveThreadState } from "../components/NoActiveThreadState";
55import { Button } from "../components/ui/button";
66import { Empty, EmptyDescription, EmptyHeader, EmptyTitle } from "../components/ui/empty";
7import { SidebarInset, SidebarTrigger } from "../components/ui/sidebar";
7import { SidebarInset } from "../components/ui/sidebar";
88import { useEnvironments } from "../state/environments";
99import { APP_DISPLAY_NAME } from "~/branding";
1010import { hasCloudPublicConfig } from "~/cloud/publicConfig";
11import { cn } from "~/lib/utils";
12import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
1113
1214function ChatIndexRouteView() {
1315 const { authGateState } = Route.useRouteContext();
......@@ -30,9 +32,13 @@ function HostedStaticOnboardingState() {
3032 return (
3133 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
3234 <div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
33 <header className="border-b border-border px-3 py-2 sm:px-5 sm:py-3">
35 <header
36 className={cn(
37 "border-b border-border px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5 sm:py-3",
38 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
39 )}
40 >
3441 <div className="flex items-center gap-2">
35 <SidebarTrigger className="size-7 shrink-0 md:hidden" />
3642 <span className="text-sm font-medium text-foreground md:text-muted-foreground/60">
3743 {APP_DISPLAY_NAME}
3844 </span>
apps/web/src/routes/settings.tsx+15-4
......@@ -11,8 +11,10 @@ import { useCallback, useEffect, useState } from "react";
1111
1212import { useSettingsRestore } from "../components/settings/SettingsPanels";
1313import { Button } from "../components/ui/button";
14import { SidebarInset, SidebarTrigger } from "../components/ui/sidebar";
14import { SidebarInset } from "../components/ui/sidebar";
1515import { isElectron } from "../env";
16import { cn } from "~/lib/utils";
17import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
1618
1719function RestoreDefaultsButton({ onRestored }: { onRestored: () => void }) {
1820 const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);
......@@ -64,9 +66,13 @@ function SettingsContentLayout() {
6466 <SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground isolate">
6567 <div className="flex min-h-0 min-w-0 flex-1 flex-col bg-background text-foreground">
6668 {!isElectron && (
67 <header className="border-b border-border px-3 py-2 sm:px-5">
69 <header
70 className={cn(
71 "border-b border-border px-3 py-2 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none sm:px-5",
72 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
73 )}
74 >
6875 <div className="flex min-h-7 items-center gap-2 sm:min-h-6">
69 <SidebarTrigger className="size-7 shrink-0 md:hidden" />
7076 <span className="text-sm font-medium text-foreground">Settings</span>
7177 {showRestoreDefaults ? (
7278 <div className="ms-auto flex items-center gap-2">
......@@ -78,7 +84,12 @@ function SettingsContentLayout() {
7884 )}
7985
8086 {isElectron && (
81 <div className="drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]">
87 <div
88 className={cn(
89 "drag-region flex h-[52px] shrink-0 items-center border-b border-border px-5 transition-[padding-left] duration-200 ease-linear motion-reduce:transition-none wco:h-[env(titlebar-area-height)] wco:pr-[calc(100vw-env(titlebar-area-width)-env(titlebar-area-x)+1em)]",
90 COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS,
91 )}
92 >
8293 <span className="text-xs font-medium tracking-wide text-muted-foreground/70">
8394 Settings
8495 </span>
apps/web/src/workspaceTitlebar.ts created+2
......@@ -0,0 +1,2 @@
1export const COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS =
2 "[[data-sidebar-state=collapsed]_&]:pl-[var(--workspace-titlebar-content-left)]";
packages/contracts/src/keybindings.test.ts+6
......@@ -29,6 +29,12 @@ it.effect("parses keybinding rules", () =>
2929 });
3030 assert.strictEqual(parsed.command, "terminal.toggle");
3131
32 const parsedSidebarToggle = yield* decode(KeybindingRule, {
33 key: "mod+b",
34 command: "sidebar.toggle",
35 });
36 assert.strictEqual(parsedSidebarToggle.command, "sidebar.toggle");
37
3238 const parsedRightPanelToggle = yield* decode(KeybindingRule, {
3339 key: "mod+alt+b",
3440 command: "rightPanel.toggle",
packages/contracts/src/keybindings.ts+1
......@@ -48,6 +48,7 @@ export const MODEL_PICKER_KEYBINDING_COMMANDS = [
4848export type ModelPickerKeybindingCommand = (typeof MODEL_PICKER_KEYBINDING_COMMANDS)[number];
4949
5050const STATIC_KEYBINDING_COMMANDS = [
51 "sidebar.toggle",
5152 "terminal.toggle",
5253 "terminal.split",
5354 "terminal.splitVertical",
packages/shared/src/keybindings.ts+1
......@@ -19,6 +19,7 @@ type WhenToken =
1919 | { type: "rparen" };
2020
2121export const DEFAULT_KEYBINDINGS: ReadonlyArray<KeybindingRule> = [
22 { key: "mod+b", command: "sidebar.toggle" },
2223 { key: "mod+j", command: "terminal.toggle" },
2324 { key: "mod+alt+b", command: "rightPanel.toggle" },
2425 { key: "mod+d", command: "terminal.split", when: "terminalFocus" },