From 7920dfcb4bf54441ba9702193ab96dd0bbc209dd Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 19 Oct 2026 13:24:33 -0700 Subject: [PATCH] fix(app): give the collapsed sidebar toggle room next to the traffic lights (#56988) --- packages/app/e2e/regression/tabs.spec.ts | 28 ++++++++ .../app/e2e/utils/macos-window.fixture.tsx | 65 +++++++++++++++++++ packages/app/e2e/utils/macos-window.html | 18 +++++ packages/app/src/shell/sidebar-toggle.tsx | 15 +++-- packages/app/vite.config.ts | 1 + 5 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 packages/app/e2e/utils/macos-window.fixture.tsx create mode 100644 packages/app/e2e/utils/macos-window.html diff --git a/packages/app/e2e/regression/tabs.spec.ts b/packages/app/e2e/regression/tabs.spec.ts index d62e8e13e21..01c20b7822f 100644 --- a/packages/app/e2e/regression/tabs.spec.ts +++ b/packages/app/e2e/regression/tabs.spec.ts @@ -435,6 +435,34 @@ for (const [screen, href] of [ }) } +// macOS 26 traffic lights end 74px from the window edge. The collapsed toggle starts 16px past them, as the horizontal +// titlebar's first control does, and a fullscreen window has no traffic lights to clear. +for (const [window, fullscreen, start] of [ + ["windowed", false, 90], + ["fullscreen", true, 18], +] as const) { + test(`collapsed sidebar toggle clears the macOS traffic lights (${window})`, async ({ page }) => { + await mockWorkspace(page, { + name: "Tabs", + sessions: [a], + seed: { settings: { appearance: { tabLayout: "vertical", sidebarOpen: false } }, tabs: [a.id] }, + }) + await page.goto( + `/e2e/utils/macos-window.html?${new URLSearchParams({ server: SERVER, route: sessionHref(a.id), fullscreen: String(fullscreen) })}`, + ) + await expectSessionTitle(page, a.title) + + const toggle = await page.locator("main").getByRole("button", { name: "Toggle sidebar", exact: true }).boundingBox() + const project = await page.locator('[data-session-title] [data-slot="session-project-trigger"]').boundingBox() + + if (!toggle || !project) throw new Error("collapsed sidebar header has no bounding boxes") + expect(toggle.x).toBe(start) + // The header's contents follow the toggle on its center line. + expect(project.x).toBe(start + 32) + expect(project.y + project.height / 2).toBe(toggle.y + toggle.height / 2) + }) +} + test("vertical tab gaps hover and select the nearest row", async ({ page }) => { await mockWorkspace(page, { name: "Tabs", diff --git a/packages/app/e2e/utils/macos-window.fixture.tsx b/packages/app/e2e/utils/macos-window.fixture.tsx new file mode 100644 index 00000000000..e96f4c04aa4 --- /dev/null +++ b/packages/app/e2e/utils/macos-window.fixture.tsx @@ -0,0 +1,65 @@ +import { MemoryRouter, createMemoryHistory } from "@solidjs/router" +import { render } from "solid-js/web" +import type { Bridge } from "@opencode/gui-extensions/sdk/bridge" +import { AppBaseProviders, AppInterface } from "../../src/app" +import { PlatformProvider, type Platform } from "../../src/runtime/platform/platform" + +// The app in a macOS desktop window at `route`, so its layout leaves room for the native traffic lights. A fullscreen +// window has none. +export function mount(input: { server: string; route: string; fullscreen: boolean }) { + const root = document.getElementById("root") + + if (!root) throw new Error("Missing fixture root") + const history = createMemoryHistory() + history.set({ value: input.route }) + render(() => { + const unused = async () => { + throw new Error("Unexpected fixture action") + } + + // No main-process extensions run in this fixture. + const bridge: Bridge = { + packaged: false, + call: unused, + subscribe: async () => ({ available: false }), + on: () => () => undefined, + embed: () => undefined, + capture: async () => undefined, + runMenubarItem: () => undefined, + configure: () => undefined, + manager: { + list: async () => [], + enable: async () => undefined, + disable: async () => undefined, + reload: async () => undefined, + install: async () => undefined, + remove: async () => undefined, + source: async () => "", + asset: () => "", + }, + } + + const platform: Platform = { + platform: "desktop", + os: "macos", + windowID: "macos-window-test", + windowFullscreen: () => input.fullscreen, + openExternal: () => undefined, + openDirectoryPickerDialog: async () => null, + notify: async () => undefined, + restart: unused, + extensions: bridge, + } + + return ( + + + } + /> + + + ) + }, root) +} diff --git a/packages/app/e2e/utils/macos-window.html b/packages/app/e2e/utils/macos-window.html new file mode 100644 index 00000000000..62b53e494f3 --- /dev/null +++ b/packages/app/e2e/utils/macos-window.html @@ -0,0 +1,18 @@ + + + + + + +
+ + + diff --git a/packages/app/src/shell/sidebar-toggle.tsx b/packages/app/src/shell/sidebar-toggle.tsx index c0b564cd46e..61ccc8260ab 100644 --- a/packages/app/src/shell/sidebar-toggle.tsx +++ b/packages/app/src/shell/sidebar-toggle.tsx @@ -9,8 +9,13 @@ import { useLanguage } from "@/runtime/i18n/language" import { usePlatform } from "@/runtime/platform/platform" import { useSettings } from "@/settings/model" -// Native controls: 14px left inset, two 20px button pitches, and a 14px button. -export const macTrafficLightsBaseWidth = 68 +// The desktop window sets `trafficLightPosition: { x: 14, y: 14 }`. macOS 26 and later draw 14px buttons on a 23px +// pitch, so the zoom button ends 74px from the window edge and the buttons center 21px from the top. macOS 15 and earlier +// end at 68px and center at 22px. +export const macTrafficLightsBaseWidth = 74 + +// Matches the horizontal titlebar, whose first control starts 16px past the traffic lights. +const macTrafficLightsGap = 16 // The toggle's 28px plus a 4px gap before the header contents. const slotWidth = 32 @@ -65,9 +70,11 @@ function useCollapsedSlot() { return { visible: () => desktop() && !windows() && settings.appearance.tabLayout() !== "horizontal", open: settings.appearance.sidebarOpen, - // Keep the toggle clear of the traffic lights, which end 68px from the window edge. + // Lays the toggle out as if the traffic lights were the header's first control. width: () => - mac() ? macTrafficLightsBaseWidth / (platform.webviewZoom?.() ?? 1) - contentStart + slotWidth : slotWidth, + mac() + ? macTrafficLightsBaseWidth / (platform.webviewZoom?.() ?? 1) + macTrafficLightsGap - contentStart + slotWidth + : slotWidth, } } diff --git a/packages/app/vite.config.ts b/packages/app/vite.config.ts index 87b1ce7a66b..de39bd9aea4 100644 --- a/packages/app/vite.config.ts +++ b/packages/app/vite.config.ts @@ -43,6 +43,7 @@ export default defineConfig({ "e2e/utils/settings-wsl.html", "e2e/utils/app-direction.html", "e2e/utils/windows-menu.html", + "e2e/utils/macos-window.html", ], }, }