Merge pull request #1560 from BradGroux/fix/titlebar-preference-1531

fix: honor the macOS titlebar double-click preference
This commit is contained in:
Brad Groux 2026-09-07 18:59:58 -05:00 • committed by GitHub
commit 74bdd6798d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 163 additions and 34 deletions

View file

@ -0,0 +1,40 @@
import { describe, expect, it, vi } from 'vitest';
import { applyTitlebarAction, resolveTitlebarAction } from '../titlebar-action.js';
describe('system titlebar action', () => {
it.each([
['Maximize', 'zoom'],
['Fill', 'zoom'],
['Minimize', 'minimize'],
['None', 'none'],
['', 'zoom'],
['future-value', 'none'],
] as const)('maps macOS preference %s to %s', (preference, action) => {
expect(resolveTitlebarAction('darwin', preference)).toBe(action);
});
it.each(['linux', 'win32'] as const)('preserves maximize on %s', (platform) => {
expect(resolveTitlebarAction(platform, 'Minimize')).toBe('zoom');
});
it('applies only the selected action and restores a zoomed window', () => {
let maximized = false;
const window = {
isMaximized: () => maximized,
maximize: vi.fn(() => {
maximized = true;
}),
unmaximize: vi.fn(() => {
maximized = false;
}),
minimize: vi.fn(),
};
applyTitlebarAction(window as never, 'none');
expect(window.maximize).not.toHaveBeenCalled();
expect(window.minimize).not.toHaveBeenCalled();
expect(applyTitlebarAction(window as never, 'zoom')).toEqual({ maximized: true });
expect(applyTitlebarAction(window as never, 'zoom')).toEqual({ maximized: false });
applyTitlebarAction(window as never, 'minimize');
expect(window.minimize).toHaveBeenCalledOnce();
expect(window.maximize).toHaveBeenCalledOnce();
expect(window.unmaximize).toHaveBeenCalledOnce();
});
});

View file

@ -26,7 +26,7 @@ import {
type DesktopBridgeResponse,
type DesktopConnectionConfigRequest,
type DesktopConnectionValidationResult,
type DesktopWindowToggleMaximizeResult,
type DesktopWindowTitlebarActionResult,
} from '../shared/desktop-bridge-contracts.js';
type MaybePromise<T> = T | Promise<T>;
@ -38,7 +38,7 @@ export type DesktopBridgeHandlerMap = {
};
export interface DesktopWindowControls {
toggleMaximize(): DesktopWindowToggleMaximizeResult;
performTitlebarAction(): DesktopWindowTitlebarActionResult;
}
async function remoteConnectionDestinationError(serverUrl: string): Promise<string | null> {
@ -291,7 +291,7 @@ export function createDesktopBridgeHandlers(
await shell.openExternal(url);
return undefined;
},
toggleWindowMaximize: () => windowControls?.toggleMaximize() ?? { maximized: false },
performTitlebarAction: () => windowControls?.performTitlebarAction() ?? { maximized: false },
};
}

View file

@ -7,6 +7,7 @@ import {
safeStorage,
shell,
screen,
systemPreferences,
} from 'electron';
import path from 'node:path';
import { mkdirSync } from 'node:fs';
@ -15,6 +16,7 @@ import { createRequire } from 'node:module';
import { DESKTOP_APP_ID, DESKTOP_APP_NAME, DESKTOP_MIN_WINDOW } from './app-metadata.js';
import { registerDesktopBridge } from './bridge.js';
import { DesktopCommandDispatcher } from './commands.js';
import { applyTitlebarAction, resolveTitlebarAction } from './titlebar-action.js';
import { sendAcknowledgedRendererCommand } from './renderer-commands.js';
import { extractDeepLinkFromArgv, parseDesktopDeepLink } from './deep-links.js';
import { configureDesktopMenu, dispatchDesktopMenuCommand } from './menu.js';
@ -347,18 +349,16 @@ async function boot(): Promise<void> {
commandDispatcher,
updateService,
{
toggleMaximize: () => {
const window = activeMainWindow();
if (!window) {
return { maximized: false };
}
if (window.isMaximized()) {
window.unmaximize();
} else {
window.maximize();
}
return { maximized: window.isMaximized() };
},
performTitlebarAction: () =>
applyTitlebarAction(
activeMainWindow(),
resolveTitlebarAction(
process.platform,
process.platform === 'darwin'
? systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string')
: ''
)
),
}
);
refreshDesktopMenu();

View file

@ -0,0 +1,33 @@
import type { BrowserWindow } from 'electron';
export type TitlebarAction = 'zoom' | 'minimize' | 'none';
export function resolveTitlebarAction(
platform: NodeJS.Platform,
preference: string
): TitlebarAction {
if (platform !== 'darwin') return 'zoom';
switch (preference) {
case '': // Unset macOS preference uses the standard zoom behavior.
case 'Maximize':
case 'Fill':
return 'zoom';
case 'Minimize':
return 'minimize';
default:
return 'none';
}
}
export function applyTitlebarAction(
window: BrowserWindow | null,
action: TitlebarAction
): { maximized: boolean } {
if (!window) return { maximized: false };
if (action === 'minimize') window.minimize();
if (action === 'zoom') {
if (window.isMaximized()) window.unmaximize();
else window.maximize();
}
return { maximized: window.isMaximized() };
}

View file

@ -16,7 +16,7 @@ import type {
DesktopSetupDiagnostics,
DesktopSupportSnapshot,
DesktopUpdateStatus,
DesktopWindowToggleMaximizeResult,
DesktopWindowTitlebarActionResult,
DesktopWorkProductExportRequest,
DesktopWorkProductExportResult,
} from '../shared/desktop-bridge-contracts.js';
@ -37,7 +37,7 @@ const DESKTOP_BRIDGE_METHODS = {
performNotificationAction: { channel: 'desktop:perform-notification-action' },
exportWorkProduct: { channel: 'desktop:export-work-product' },
openExternal: { channel: 'desktop:open-external' },
toggleWindowMaximize: { channel: 'desktop:toggle-window-maximize' },
performTitlebarAction: { channel: 'desktop:perform-titlebar-action' },
} as const;
const DESKTOP_BRIDGE_EVENTS = {
@ -90,7 +90,7 @@ export interface VeritasDesktopApi {
request: DesktopWorkProductExportRequest
): Promise<DesktopWorkProductExportResult>;
openExternal(url: string): Promise<void>;
toggleWindowMaximize(): Promise<DesktopWindowToggleMaximizeResult>;
performTitlebarAction(): Promise<DesktopWindowTitlebarActionResult>;
onSetupProgress(listener: BridgeEventListener<'setupProgress'>): () => void;
onCommunicationCheck(listener: BridgeEventListener<'communicationCheck'>): () => void;
onServerStatus(listener: (status: DesktopStatusSnapshot) => void): () => void;
@ -177,9 +177,9 @@ const api: VeritasDesktopApi = {
),
openExternal: (url: string) =>
invokeDesktop<void>(DESKTOP_BRIDGE_METHODS.openExternal.channel, { url }),
toggleWindowMaximize: () =>
invokeDesktop<DesktopWindowToggleMaximizeResult>(
DESKTOP_BRIDGE_METHODS.toggleWindowMaximize.channel
performTitlebarAction: () =>
invokeDesktop<DesktopWindowTitlebarActionResult>(
DESKTOP_BRIDGE_METHODS.performTitlebarAction.channel
),
onSetupProgress: (listener) => onDesktopEvent('setupProgress', listener),
onCommunicationCheck: (listener) => onDesktopEvent('communicationCheck', listener),

View file

@ -204,7 +204,7 @@ export interface DesktopWorkProductExportResult {
warnings: string[];
}
export interface DesktopWindowToggleMaximizeResult {
export interface DesktopWindowTitlebarActionResult {
maximized: boolean;
}
@ -295,9 +295,9 @@ export const DESKTOP_BRIDGE_METHODS = {
dangerous: true,
validator: 'openExternal',
},
toggleWindowMaximize: {
performTitlebarAction: {
capability: 'shell',
channel: 'desktop:toggle-window-maximize',
channel: 'desktop:perform-titlebar-action',
desktopOnly: true,
dangerous: false,
},
@ -317,7 +317,7 @@ export const DESKTOP_BRIDGE_METHOD_NAMES = [
'performNotificationAction',
'exportWorkProduct',
'openExternal',
'toggleWindowMaximize',
'performTitlebarAction',
] as const;
export type DesktopBridgeMethod = (typeof DESKTOP_BRIDGE_METHOD_NAMES)[number];
@ -425,7 +425,7 @@ export interface DesktopBridgeRequestMap {
performNotificationAction: DesktopNotificationActionRequest;
exportWorkProduct: DesktopWorkProductExportRequest;
openExternal: OpenExternalRequest;
toggleWindowMaximize: undefined;
performTitlebarAction: undefined;
}
export interface DesktopBridgeResponseMap {
@ -442,7 +442,7 @@ export interface DesktopBridgeResponseMap {
performNotificationAction: DesktopNotificationActionResult;
exportWorkProduct: DesktopWorkProductExportResult;
openExternal: undefined;
toggleWindowMaximize: DesktopWindowToggleMaximizeResult;
performTitlebarAction: DesktopWindowTitlebarActionResult;
}
export interface DesktopBridgeEventPayloadMap {

View file

@ -326,3 +326,8 @@ application menu; View provides text zoom and full screen.
Saved window bounds are fitted to the current display work areas at launch or
reopen. If a monitor was disconnected, the window returns to the primary display.
Maximized windows retain their previous normal bounds for unmaximizing.
The custom header follows the macOS title-bar double-click preference (zoom/fill,
minimize, or no action). Configure it in [Desktop & Dock settings](https://support.apple.com/guide/mac-help/change-desktop-dock-settings-mchlp1119/mac).
The native gate records the current preference and verifies its action without
changing the operator's system preferences.

View file

@ -159,3 +159,33 @@ export async function verifyNativeWindowMenu(app, page) {
.toEqual(normalBounds);
return { page: reopened, roles: items, normalBounds };
}
export async function verifyConfiguredTitlebarAction(app, page) {
const preference = await app.evaluate(({ systemPreferences }) =>
systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string')
);
await app.evaluate(({ BrowserWindow }) => {
const window = BrowserWindow.getAllWindows()[0];
window.restore();
window.unmaximize();
});
const state = () =>
app.evaluate(({ BrowserWindow }) => ({
maximized: BrowserWindow.getAllWindows()[0].isMaximized(),
minimized: BrowserWindow.getAllWindows()[0].isMinimized(),
}));
await expect.poll(state).toEqual({ maximized: false, minimized: false });
await page.getByRole('navigation', { name: 'Main navigation' }).dispatchEvent('dblclick');
const expected = {
maximized: ['', 'Maximize', 'Fill'].includes(preference),
minimized: preference === 'Minimize',
};
await expect.poll(state).toEqual(expected);
await app.evaluate(({ BrowserWindow }) => {
const window = BrowserWindow.getAllWindows()[0];
window.restore();
window.unmaximize();
window.focus();
});
return { preference: preference || 'system default', expected };
}

View file

@ -6,7 +6,11 @@ import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { expect } from '@playwright/test';
import { createNativeSession } from './session.mjs';
import { verifyNativeMenuCommands, verifyNativeWindowMenu } from './menu-commands.mjs';
import {
verifyNativeMenuCommands,
verifyNativeWindowMenu,
verifyConfiguredTitlebarAction,
} from './menu-commands.mjs';
import {
fileDigest,
evidenceFailures,
@ -653,6 +657,7 @@ async function checkSeededRendererFailures() {
}
try {
await launch();
report.titlebarAction = await verifyConfiguredTitlebarAction(app, page);
report.menuCommands = await verifyNativeMenuCommands(app, page);
const windowMenu = await verifyNativeWindowMenu(app, page);
page = windowMenu.page;

View file

@ -266,7 +266,7 @@ function renderDesktopBoard() {
Object.defineProperty(window, 'veritasDesktop', {
configurable: true,
value: {
toggleWindowMaximize: vi.fn(),
performTitlebarAction: vi.fn(),
},
});
window.localStorage.setItem('veritas.desktop.rightRailOpen', 'false');

View file

@ -1,5 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, screen, waitFor, within } from '@testing-library/react';
import { cleanup, screen, waitFor, within, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ViewProvider } from '@/contexts/ViewContext';
@ -143,7 +143,7 @@ function renderDesktopHeaderChrome(options: { withBottomPanel?: boolean } = {})
Object.defineProperty(window, 'veritasDesktop', {
configurable: true,
value: {
toggleWindowMaximize: vi.fn(),
performTitlebarAction: vi.fn(),
},
});
document.documentElement.dataset.client = 'desktop';
@ -381,10 +381,26 @@ describe('layout chrome Mantine migration', () => {
expect(container.querySelector('.lucide-panel-right-close')).toBeNull();
});
it('sends titlebar double clicks only from the header background', () => {
renderDesktopHeaderChrome();
const action = (
window as unknown as { veritasDesktop: { performTitlebarAction: ReturnType<typeof vi.fn> } }
).veritasDesktop.performTitlebarAction;
fireEvent.doubleClick(screen.getByRole('button', { name: 'New Task' }));
fireEvent.doubleClick(screen.getByRole('button', { name: 'Settings' }));
const input = document.createElement('input');
screen.getByRole('navigation', { name: 'Main navigation' }).append(input);
fireEvent.doubleClick(input);
expect(action).not.toHaveBeenCalled();
input.remove();
fireEvent.doubleClick(screen.getByRole('navigation', { name: 'Main navigation' }));
expect(action).toHaveBeenCalledOnce();
});
it('uses the filled brand treatment with white text for the active desktop navigation item', () => {
Object.defineProperty(window, 'veritasDesktop', {
configurable: true,
value: { toggleWindowMaximize: vi.fn() },
value: { performTitlebarAction: vi.fn() },
});
document.documentElement.dataset.client = 'desktop';
window.history.replaceState({}, '', '/drift');

View file

@ -423,9 +423,9 @@ export function Header({
}
void (
window as Window & {
veritasDesktop?: { toggleWindowMaximize?: () => Promise<{ maximized: boolean }> };
veritasDesktop?: { performTitlebarAction?: () => Promise<{ maximized: boolean }> };
}
).veritasDesktop?.toggleWindowMaximize?.();
).veritasDesktop?.performTitlebarAction?.();
},
[isDesktopClient]
);