fix(mac): align traffic lights in sidebar chrome (#1092)

This commit is contained in:
paisley
2026-06-02 17:57:27 +08:00
committed by GitHub
parent d27faa5a00
commit e47fce4ae4
11 changed files with 210 additions and 14 deletions

View File

@@ -28,6 +28,8 @@ import {
} from '../utils/openclaw-workspace';
import { autoInstallCliIfNeeded, generateCompletionCache, installCompletionToProfile } from '../utils/openclaw-cli';
import { isQuitting, setQuitting } from './app-state';
import { getMacTrafficLightPosition, syncMacTrafficLightPosition } from './traffic-light-layout';
import { getSetting } from '../utils/store';
import { applyProxySettings } from './proxy';
import { syncLaunchAtStartupSettingFromStore } from './launch-at-startup';
import {
@@ -43,7 +45,6 @@ import {
} from './quit-lifecycle';
import { createSignalQuitHandler } from './signal-quit';
import { acquireProcessInstanceFileLock } from './process-instance-lock';
import { getSetting } from '../utils/store';
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled, trimBundledOpenClawSkillsAndConfigs } from '../utils/skill-config';
import { startHostApiServer } from '../api/server';
@@ -187,7 +188,9 @@ function createWindow(): BrowserWindow {
webviewTag: true, // Enable <webview> for embedding OpenClaw Control UI
},
titleBarStyle: isMac ? 'hiddenInset' : useCustomTitleBar ? 'hidden' : 'default',
trafficLightPosition: isMac ? { x: 16, y: 16 } : undefined,
trafficLightPosition: isMac
? getMacTrafficLightPosition(false)
: undefined,
frame: isMac || !useCustomTitleBar,
show: false,
});
@@ -261,6 +264,12 @@ function createMainWindow(): BrowserWindow {
return;
}
if (process.platform === 'darwin') {
void getSetting('sidebarCollapsed').then((sidebarCollapsed) => {
syncMacTrafficLightPosition(win, sidebarCollapsed);
});
}
const action = consumeMainWindowReady(mainWindowFocusState);
if (action === 'focus') {
focusWindow(win);

View File

@@ -7,6 +7,7 @@ import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, extname, basename, resolve, sep, relative } from 'node:path';
import crypto from 'node:crypto';
import { syncMacTrafficLightPosition } from './traffic-light-layout';
import { GatewayManager } from '../gateway/manager';
import { ClawHubService, ClawHubSearchParams, ClawHubInstallParams, ClawHubUninstallParams } from '../gateway/clawhub';
import {
@@ -2305,6 +2306,13 @@ function registerUsageHandlers(): void {
* Window control handlers (for custom title bar on Windows)
*/
function registerWindowHandlers(mainWindow: BrowserWindow): void {
ipcMain.handle('window:syncTrafficLightPosition', (_, sidebarCollapsed: unknown) => {
if (typeof sidebarCollapsed !== 'boolean') {
return;
}
syncMacTrafficLightPosition(mainWindow, sidebarCollapsed);
});
ipcMain.handle('window:minimize', () => {
mainWindow.minimize();
});

View File

@@ -0,0 +1,40 @@
import { release } from 'node:os';
import type { BrowserWindow } from 'electron';
const MAC_SIDEBAR_CHROME_HEIGHT = 28;
const MAC_TRAFFIC_LIGHT_GAP = 8;
const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT = 16;
const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE = 14;
function getMacTrafficLightFrameHeight(darwinMajor: number): number {
return darwinMajor >= 25
? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE
: MAC_TRAFFIC_LIGHT_FRAME_HEIGHT;
}
function getMacTrafficLightChromeOffset(buttonFrameHeight: number): number {
return Math.floor((MAC_SIDEBAR_CHROME_HEIGHT - buttonFrameHeight) / 2);
}
export function getMacTrafficLightPosition(sidebarCollapsed: boolean): { x: number; y: number } {
const darwinMajor = Number.parseInt(release().split('.')[0] ?? '0', 10);
const buttonFrameHeight = getMacTrafficLightFrameHeight(darwinMajor);
const offset = getMacTrafficLightChromeOffset(buttonFrameHeight);
if (sidebarCollapsed) {
return { x: MAC_TRAFFIC_LIGHT_GAP, y: Math.max(MAC_TRAFFIC_LIGHT_GAP, offset) };
}
return { x: offset + 1, y: offset };
}
export function syncMacTrafficLightPosition(
win: BrowserWindow,
sidebarCollapsed: boolean,
): void {
if (process.platform !== 'darwin' || win.isDestroyed()) {
return;
}
win.setWindowButtonPosition(getMacTrafficLightPosition(sidebarCollapsed));
}

View File

@@ -50,6 +50,7 @@ const electronAPI = {
'window:maximize',
'window:close',
'window:isMaximized',
'window:syncTrafficLightPosition',
// Settings
'settings:get',
'settings:set',

View File

@@ -87,7 +87,8 @@
"node-machine-id": "^1.1.12",
"posthog-node": "^5.28.0",
"tar": "^6.2.1",
"ws": "^8.19.0"
"ws": "^8.19.0",
"yaml": "^2.9.0"
},
"devDependencies": {
"@buape/carbon": "0.16.0",

9
pnpm-lock.yaml generated
View File

@@ -29,6 +29,9 @@ importers:
ws:
specifier: ^8.19.0
version: 8.20.0
yaml:
specifier: ^2.9.0
version: 2.9.0
devDependencies:
'@buape/carbon':
specifier: 0.16.0
@@ -4018,10 +4021,6 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.5.0:
resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==}
engines: {node: '>=18'}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
@@ -10820,8 +10819,6 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.5.0: {}
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:

58
shared/sidebar-layout.ts Normal file
View File

@@ -0,0 +1,58 @@
/** macOS sidebar title strip height — matches VS Code compact title bar (px). */
export const MAC_SIDEBAR_CHROME_HEIGHT = 28;
/** Visible traffic-light diameter on macOS (px). */
export const MAC_TRAFFIC_LIGHT_BUTTON_SIZE = 12;
/** Native edge-to-edge gap between traffic light buttons (px). */
export const MAC_TRAFFIC_LIGHT_GAP = 8;
/** Frame height VS Code uses when vertically centering traffic lights (pre-Tahoe). */
export const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT = 16;
/** Frame height on macOS Tahoe and newer. */
export const MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE = 14;
/** macOS close + minimize + zoom button group width (px). */
export const MAC_TRAFFIC_LIGHT_GROUP_WIDTH =
3 * MAC_TRAFFIC_LIGHT_BUTTON_SIZE + 2 * MAC_TRAFFIC_LIGHT_GAP;
/**
* Collapsed sidebar rail width (px): four spacing units plus three buttons.
* 4 × 8 + 3 × 12 = 68
*/
export const SIDEBAR_COLLAPSED_WIDTH =
4 * MAC_TRAFFIC_LIGHT_GAP + 3 * MAC_TRAFFIC_LIGHT_BUTTON_SIZE;
export function getMacTrafficLightFrameHeight(darwinMajor: number): number {
return darwinMajor >= 25
? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT_TAHOE
: MAC_TRAFFIC_LIGHT_FRAME_HEIGHT;
}
/** VS Code-style inset that vertically centers traffic lights in the chrome strip. */
export function getMacTrafficLightChromeOffset(
chromeHeight = MAC_SIDEBAR_CHROME_HEIGHT,
buttonFrameHeight = MAC_TRAFFIC_LIGHT_FRAME_HEIGHT,
): number {
return Math.floor((chromeHeight - buttonFrameHeight) / 2);
}
export function getMacTrafficLightPosition(options: {
sidebarCollapsed: boolean;
chromeHeight?: number;
buttonFrameHeight?: number;
}): { x: number; y: number } {
const chromeHeight = options.chromeHeight ?? MAC_SIDEBAR_CHROME_HEIGHT;
const buttonFrameHeight = options.buttonFrameHeight ?? MAC_TRAFFIC_LIGHT_FRAME_HEIGHT;
const offset = getMacTrafficLightChromeOffset(chromeHeight, buttonFrameHeight);
if (options.sidebarCollapsed) {
// Collapsed rail: keep uniform 8px grid on left / inter-dot / right.
const spacing = MAC_TRAFFIC_LIGHT_GAP;
return { x: spacing, y: Math.max(spacing, offset) };
}
// Expanded: VS Code aligns left inset with vertical inset (x = offset + 1).
return { x: offset + 1, y: offset };
}

View File

@@ -5,6 +5,7 @@
import { Outlet } from 'react-router-dom';
import { Sidebar } from './Sidebar';
import { TitleBar } from './TitleBar';
import { MAC_SIDEBAR_CHROME_HEIGHT } from '../../../shared/sidebar-layout';
import { cn } from '@/lib/utils';
export function MainLayout() {
@@ -37,7 +38,8 @@ export function MainLayout() {
<div
data-testid="mac-main-drag-region"
aria-hidden="true"
className="drag-region absolute inset-x-0 top-0 z-10 h-7"
className="drag-region absolute inset-x-0 top-0 z-10"
style={{ height: MAC_SIDEBAR_CHROME_HEIGHT }}
/>
)}
<Outlet />

View File

@@ -37,6 +37,8 @@ import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { hostApiFetch } from '@/lib/host-api';
import { invokeIpc } from '@/lib/api-client';
import { SIDEBAR_COLLAPSED_WIDTH, MAC_SIDEBAR_CHROME_HEIGHT } from '../../../shared/sidebar-layout';
import { useTranslation } from 'react-i18next';
import logoSvg from '@/assets/logo.svg';
@@ -149,6 +151,11 @@ export function Sidebar() {
const agents = useAgentsStore((s) => s.agents);
const fetchAgents = useAgentsStore((s) => s.fetchAgents);
useEffect(() => {
if (!isMac) return;
void invokeIpc('window:syncTrafficLightPosition', sidebarCollapsed);
}, [isMac, sidebarCollapsed]);
const navigate = useNavigate();
const isOnChat = useLocation().pathname === '/';
@@ -332,13 +339,21 @@ export function Sidebar() {
'relative flex min-h-0 shrink-0 flex-col overflow-hidden bg-surface-sidebar',
isResizing ? 'transition-none' : 'transition-[width] duration-300',
)}
style={{ width: sidebarCollapsed ? 64 : sidebarWidth }}
style={{ width: sidebarCollapsed ? SIDEBAR_COLLAPSED_WIDTH : sidebarWidth }}
>
{isMac && (
<div
aria-hidden="true"
data-testid="mac-sidebar-chrome"
className="drag-region shrink-0"
style={{ height: MAC_SIDEBAR_CHROME_HEIGHT }}
/>
)}
{/* Top Header Toggle */}
<div
className={cn(
'flex items-center p-2 h-12',
isMac && 'drag-region h-[4.75rem] items-end pt-10',
'flex shrink-0 items-center p-2 h-8',
sidebarCollapsed ? 'justify-center' : 'justify-between',
)}
>
@@ -351,6 +366,7 @@ export function Sidebar() {
</div>
)}
<Button
data-testid="sidebar-collapse-toggle"
variant="ghost"
size="icon"
className={cn(

View File

@@ -1,4 +1,8 @@
import { expect, test } from './fixtures/electron';
import {
MAC_SIDEBAR_CHROME_HEIGHT,
SIDEBAR_COLLAPSED_WIDTH,
} from '../../shared/sidebar-layout';
test.describe('macOS frameless chrome', () => {
test.skip(process.platform !== 'darwin', 'macOS drag-region chrome only');
@@ -17,7 +21,19 @@ test.describe('macOS frameless chrome', () => {
const box = await mainDragRegion.boundingBox();
expect(box).not.toBeNull();
expect(box!.width).toBeGreaterThan(200);
expect(box!.height).toBeGreaterThanOrEqual(24);
expect(box!.height).toBe(MAC_SIDEBAR_CHROME_HEIGHT);
const sidebarChrome = page.getByTestId('mac-sidebar-chrome');
await expect(sidebarChrome).toBeVisible();
await expect(sidebarChrome).toHaveCSS('-webkit-app-region', 'drag');
const chromeBox = await sidebarChrome.boundingBox();
expect(chromeBox).not.toBeNull();
expect(chromeBox!.height).toBe(MAC_SIDEBAR_CHROME_HEIGHT);
const sidebar = page.getByTestId('sidebar');
await page.getByTestId('sidebar-collapse-toggle').click();
await expect.poll(async () => (await sidebar.boundingBox())?.width ?? 0).toBe(SIDEBAR_COLLAPSED_WIDTH);
const chatPage = page.getByTestId('chat-page');
await expect(chatPage).toBeVisible();

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import {
getMacTrafficLightChromeOffset,
getMacTrafficLightFrameHeight,
getMacTrafficLightPosition,
MAC_TRAFFIC_LIGHT_FRAME_HEIGHT,
MAC_TRAFFIC_LIGHT_GAP,
MAC_TRAFFIC_LIGHT_GROUP_WIDTH,
MAC_SIDEBAR_CHROME_HEIGHT,
SIDEBAR_COLLAPSED_WIDTH,
} from '../../shared/sidebar-layout';
describe('macOS traffic light layout', () => {
it('matches VS Code vertical centering in the chrome strip', () => {
const offset = getMacTrafficLightChromeOffset(
MAC_SIDEBAR_CHROME_HEIGHT,
MAC_TRAFFIC_LIGHT_FRAME_HEIGHT,
);
expect(offset).toBe(6);
const expanded = getMacTrafficLightPosition({
sidebarCollapsed: false,
buttonFrameHeight: MAC_TRAFFIC_LIGHT_FRAME_HEIGHT,
});
expect(expanded).toEqual({ x: 7, y: 6 });
});
it('uses a uniform spacing grid when the sidebar is collapsed', () => {
const collapsed = getMacTrafficLightPosition({
sidebarCollapsed: true,
buttonFrameHeight: MAC_TRAFFIC_LIGHT_FRAME_HEIGHT,
});
const spacing = MAC_TRAFFIC_LIGHT_GAP;
expect(collapsed.x).toBe(spacing);
expect(collapsed.y).toBeGreaterThanOrEqual(spacing);
const leftInset = collapsed.x;
const rightInset = SIDEBAR_COLLAPSED_WIDTH - (collapsed.x + MAC_TRAFFIC_LIGHT_GROUP_WIDTH);
expect(leftInset).toBe(spacing);
expect(rightInset).toBe(spacing);
});
it('uses a smaller frame height on Tahoe and newer', () => {
expect(getMacTrafficLightFrameHeight(24)).toBe(MAC_TRAFFIC_LIGHT_FRAME_HEIGHT);
expect(getMacTrafficLightFrameHeight(25)).toBe(14);
});
});