Skip to content

Commit 7bd566c

Browse files
authored
Hide macOS window controls with sidebar state (#355)
- Add desktop IPC to toggle native window button visibility on macOS - Sync visibility from the chat sidebar open state and restore it on cleanup - Remove extra left padding now that the title bar controls are hidden
1 parent 1bee696 commit 7bd566c

6 files changed

Lines changed: 59 additions & 5 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
5656
const CONFIRM_CHANNEL = "desktop:confirm";
5757
const SET_THEME_CHANNEL = "desktop:set-theme";
5858
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
59+
const SET_WINDOW_BUTTON_VISIBILITY_CHANNEL = "desktop:set-window-button-visibility";
5960
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
6061
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
6162
const MENU_ACTION_CHANNEL = "desktop:menu-action";
@@ -1173,6 +1174,21 @@ function registerIpcHandlers(): void {
11731174
// applies the value through a CSS custom-property.
11741175
});
11751176

1177+
ipcMain.removeHandler(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL);
1178+
ipcMain.handle(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL, async (event, rawVisible: unknown) => {
1179+
if (process.platform !== "darwin" || typeof rawVisible !== "boolean") {
1180+
return;
1181+
}
1182+
1183+
const owner =
1184+
BrowserWindow.fromWebContents(event.sender) ?? BrowserWindow.getFocusedWindow() ?? mainWindow;
1185+
if (!owner || owner.isDestroyed()) {
1186+
return;
1187+
}
1188+
1189+
owner.setWindowButtonVisibility(rawVisible);
1190+
});
1191+
11761192
ipcMain.removeHandler(CONTEXT_MENU_CHANNEL);
11771193
ipcMain.handle(
11781194
CONTEXT_MENU_CHANNEL,

apps/desktop/src/preload.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const PICK_FOLDER_CHANNEL = "desktop:pick-folder";
55
const CONFIRM_CHANNEL = "desktop:confirm";
66
const SET_THEME_CHANNEL = "desktop:set-theme";
77
const SET_SIDEBAR_OPACITY_CHANNEL = "desktop:set-sidebar-opacity";
8+
const SET_WINDOW_BUTTON_VISIBILITY_CHANNEL = "desktop:set-window-button-visibility";
89
const CONTEXT_MENU_CHANNEL = "desktop:context-menu";
910
const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
1011
const MENU_ACTION_CHANNEL = "desktop:menu-action";
@@ -37,6 +38,8 @@ contextBridge.exposeInMainWorld("desktopBridge", {
3738
confirm: (message) => ipcRenderer.invoke(CONFIRM_CHANNEL, message),
3839
setTheme: (theme) => ipcRenderer.invoke(SET_THEME_CHANNEL, theme),
3940
setSidebarOpacity: (opacity) => ipcRenderer.invoke(SET_SIDEBAR_OPACITY_CHANNEL, opacity),
41+
setWindowButtonVisibility: (visible) =>
42+
ipcRenderer.invoke(SET_WINDOW_BUTTON_VISIBILITY_CHANNEL, visible),
4043
showContextMenu: (items, position) => ipcRenderer.invoke(CONTEXT_MENU_CHANNEL, items, position),
4144
openExternal: (url: string) => ipcRenderer.invoke(OPEN_EXTERNAL_CHANNEL, url),
4245
onMenuAction: (listener) => {

apps/web/src/components/ChatView.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4700,9 +4700,7 @@ export default function ChatView({ threadId, onMinimize }: ChatViewProps) {
47004700
<header
47014701
className={cn(
47024702
"border-b border-border px-3 sm:px-5",
4703-
isElectron
4704-
? "drag-region flex h-[52px] items-center pl-[90px] sm:pl-[90px]"
4705-
: "py-2 sm:py-3",
4703+
isElectron ? "drag-region flex h-[52px] items-center" : "py-2 sm:py-3",
47064704
)}
47074705
>
47084706
<ChatHeader

apps/web/src/components/home/ChatHomeEmptyState.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export function ChatHomeEmptyState() {
216216
)}
217217

218218
{isElectron && (
219-
<div className="drag-region flex h-[52px] shrink-0 items-center justify-between gap-3 border-b border-border px-4 pl-[90px] sm:px-5 sm:pl-[90px]">
219+
<div className="drag-region flex h-[52px] shrink-0 items-center justify-between gap-3 border-b border-border px-4 sm:px-5">
220220
<div className="flex min-w-0 items-center gap-2">
221221
<SidebarTrigger className="size-7 shrink-0" />
222222
<span className="truncate text-xs font-medium tracking-[0.14em] text-muted-foreground/70 uppercase">

apps/web/src/routes/_chat.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { CommandPalette } from "../components/CommandPalette";
88
import { ScreenshotTool, ScreenshotButton } from "../components/ScreenshotTool";
99
import { WorktreeCleanupDialog } from "../components/WorktreeCleanupDialog";
1010
import { useHandleNewThread } from "../hooks/useHandleNewThread";
11+
import { readDesktopBridge } from "../lib/runtimeBridge";
1112
import { isTerminalFocused } from "../lib/terminalFocus";
1213
import { isMacPlatform } from "../lib/utils";
1314
import { serverConfigQueryOptions } from "../lib/serverReactQuery";
@@ -19,7 +20,7 @@ import { useScreenshotStore } from "../screenshotStore";
1920
import { useStore } from "../store";
2021
import { resolveSidebarNewThreadEnvMode } from "~/components/Sidebar.logic";
2122
import { useAppSettings } from "~/appSettings";
22-
import { Sidebar, SidebarProvider, SidebarRail } from "~/components/ui/sidebar";
23+
import { Sidebar, SidebarProvider, SidebarRail, useSidebar } from "~/components/ui/sidebar";
2324
import { useAutoDeleteMergedThreads } from "~/hooks/useAutoDeleteMergedThreads";
2425
import { useClientMode } from "~/hooks/useClientMode";
2526
import { isMobileShell } from "../env";
@@ -215,6 +216,40 @@ function ChatRouteGlobalShortcuts() {
215216
return null;
216217
}
217218

219+
function ChatDesktopWindowButtonsSync() {
220+
const { open } = useSidebar();
221+
222+
useEffect(() => {
223+
if (typeof navigator === "undefined" || !isMacPlatform(navigator.platform)) {
224+
return;
225+
}
226+
227+
const bridge = readDesktopBridge();
228+
if (!bridge || typeof bridge.setWindowButtonVisibility !== "function") {
229+
return;
230+
}
231+
232+
void bridge.setWindowButtonVisibility(open);
233+
}, [open]);
234+
235+
useEffect(() => {
236+
if (typeof navigator === "undefined" || !isMacPlatform(navigator.platform)) {
237+
return;
238+
}
239+
240+
const bridge = readDesktopBridge();
241+
if (!bridge || typeof bridge.setWindowButtonVisibility !== "function") {
242+
return;
243+
}
244+
245+
return () => {
246+
void bridge.setWindowButtonVisibility(true);
247+
};
248+
}, []);
249+
250+
return null;
251+
}
252+
218253
function ChatRouteLayout() {
219254
const navigate = useNavigate();
220255
const { settings } = useAppSettings();
@@ -266,6 +301,7 @@ function ChatRouteLayout() {
266301
/>
267302
<div className="relative z-10 min-h-dvh">
268303
<SidebarProvider defaultOpen={clientMode !== "mobile"}>
304+
<ChatDesktopWindowButtonsSync />
269305
<ChatRouteGlobalShortcuts />
270306
<CommandPalette />
271307
<WorktreeCleanupDialog />

packages/contracts/src/ipc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export interface DesktopBridge {
238238
confirm: (message: string) => Promise<boolean>;
239239
setTheme: (theme: DesktopTheme) => Promise<void>;
240240
setSidebarOpacity: (opacity: number) => Promise<void>;
241+
setWindowButtonVisibility: (visible: boolean) => Promise<void>;
241242
showContextMenu: <T extends string>(
242243
items: readonly ContextMenuItem<T>[],
243244
position?: { x: number; y: number },

0 commit comments

Comments
 (0)