diff --git a/packages/ui/src/components/desktop/WindowsWindowControls.tsx b/packages/ui/src/components/desktop/WindowsWindowControls.tsx index 6b423955..900d5a70 100644 --- a/packages/ui/src/components/desktop/WindowsWindowControls.tsx +++ b/packages/ui/src/components/desktop/WindowsWindowControls.tsx @@ -3,8 +3,8 @@ import React, { useEffect } from 'react'; import { Icon } from '@/components/icon/Icon'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; -import { invokeDesktop } from '@/lib/desktop'; -import type { DesktopWindowControlsSide } from '@/lib/desktop'; +import { getDesktopWindowControlsOrder, invokeDesktop } from '@/lib/desktop'; +import type { DesktopWindowControlAction, DesktopWindowControlsSide } from '@/lib/desktop'; type WindowsWindowControlsProps = { visible: boolean; @@ -62,31 +62,44 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({ ? 'app-region-no-drag mr-1 flex h-8 shrink-0 items-center' : 'app-region-no-drag ml-1 flex h-12 shrink-0 items-center'; - return ( -
- - + const renderControl = (action: DesktopWindowControlAction) => { + if (action === 'minimize') { + return ( + + ); + } + + if (action === 'maximize') { + return ( + + ); + } + + return ( + ); + }; + + return ( +
+ {getDesktopWindowControlsOrder(position).map(renderControl)}
); }); diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 0485de00..c28f2b70 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -281,7 +281,6 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain' type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | 'windowControlsPosition' | 'pwaInstallName' | 'pwaOrientation' | 'mobileKeyboardMode' | 'timeFormat' | 'weekStart' | 'fontSize' | 'terminalFontSize' | 'terminalShell' | 'terminalLoginShell' | 'editorFontSize' | 'spacing' | 'inputBarOffset' | 'mermaidRendering' | 'userMessageRendering' | 'chatRenderMode' | 'messageTransport' | 'activityRenderMode' | 'collapsibleUserMessages' | 'stickyUserHeader' | 'promptNavigatorEnabled' | 'wideChatLayout' | 'codeBlockLineWrap' | 'splitAssistantMessageActions' | 'subagentReadOnlyBanner' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'fileViewerPreview' | 'reasoning' | 'showToolFileIcons' | 'showTurnChangedFiles' | 'expandedTools' | 'followUpBehavior' | 'terminalQuickKeys' | 'fileEditorKeymap' | 'persistDraft' | 'inputSpellcheck' | 'reportUsage' | 'expandedEditorToolbar'; const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPosition; labelKey: string }> = [ - { id: 'auto', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsAuto' }, { id: 'left', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsLeft' }, { id: 'right', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsRight' }, ]; diff --git a/packages/ui/src/lib/desktop.ts b/packages/ui/src/lib/desktop.ts index c1f58a93..d8b41983 100644 --- a/packages/ui/src/lib/desktop.ts +++ b/packages/ui/src/lib/desktop.ts @@ -38,8 +38,9 @@ export type SkillCatalogConfig = { gitIdentityId?: string; }; -export type DesktopWindowControlsPosition = 'auto' | 'left' | 'right'; +export type DesktopWindowControlsPosition = 'left' | 'right'; export type DesktopWindowControlsSide = 'left' | 'right'; +export type DesktopWindowControlAction = 'close' | 'minimize' | 'maximize'; export type DesktopSettings = { themeId?: string; @@ -252,6 +253,9 @@ export const getElectronPlatform = (): string | null => { /** Width of the three in-app window control buttons when placed on the left (3 × w-8). */ export const DESKTOP_WINDOW_CONTROLS_WIDTH_PX = 96; +/** Default side for in-app window controls (Windows-style, right). */ +export const DEFAULT_DESKTOP_WINDOW_CONTROLS_POSITION: DesktopWindowControlsPosition = 'right'; + /** Windows and Linux use frameless windows with in-app minimize/maximize/close controls. */ export const usesFramelessElectronChrome = (): boolean => { if (!isElectronShell()) return false; @@ -259,21 +263,36 @@ export const usesFramelessElectronChrome = (): boolean => { return platform === 'win32' || platform === 'linux'; }; -export const getDefaultDesktopWindowControlsSide = (platform: string | null = getElectronPlatform()): DesktopWindowControlsSide => { - if (platform === 'linux') { - return 'left'; +/** Normalize a stored preference; legacy `auto` maps to the right-side default. */ +export const normalizeDesktopWindowControlsPosition = ( + value: unknown, +): DesktopWindowControlsPosition | undefined => { + if (value === 'left' || value === 'right') { + return value; } - return 'right'; + // Legacy "auto" never read OS chrome config; treat it as the right default. + if (value === 'auto') { + return DEFAULT_DESKTOP_WINDOW_CONTROLS_POSITION; + } + return undefined; }; export const resolveDesktopWindowControlsSide = ( preference: DesktopWindowControlsPosition | undefined, - platform: string | null = getElectronPlatform(), ): DesktopWindowControlsSide => { - if (preference === 'left' || preference === 'right') { - return preference; - } - return getDefaultDesktopWindowControlsSide(platform); + return preference === 'left' ? 'left' : DEFAULT_DESKTOP_WINDOW_CONTROLS_POSITION; +}; + +/** + * Left matches macOS traffic-light order (close, minimize, maximize). + * Right keeps Windows order (minimize, maximize, close). + */ +export const getDesktopWindowControlsOrder = ( + side: DesktopWindowControlsSide, +): DesktopWindowControlAction[] => { + return side === 'left' + ? ['close', 'minimize', 'maximize'] + : ['minimize', 'maximize', 'close']; }; export const hasDesktopInvoke = (): boolean => { diff --git a/packages/ui/src/lib/desktopWindowControls.test.ts b/packages/ui/src/lib/desktopWindowControls.test.ts new file mode 100644 index 00000000..a38e0159 --- /dev/null +++ b/packages/ui/src/lib/desktopWindowControls.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, test } from 'bun:test'; + +import { + DEFAULT_DESKTOP_WINDOW_CONTROLS_POSITION, + getDesktopWindowControlsOrder, + normalizeDesktopWindowControlsPosition, + resolveDesktopWindowControlsSide, +} from './desktop'; + +describe('desktop window controls position', () => { + test('defaults to right', () => { + expect(DEFAULT_DESKTOP_WINDOW_CONTROLS_POSITION).toBe('right'); + expect(resolveDesktopWindowControlsSide(undefined)).toBe('right'); + expect(resolveDesktopWindowControlsSide('right')).toBe('right'); + expect(resolveDesktopWindowControlsSide('left')).toBe('left'); + }); + + test('maps legacy auto to right', () => { + expect(normalizeDesktopWindowControlsPosition('auto')).toBe('right'); + expect(normalizeDesktopWindowControlsPosition('left')).toBe('left'); + expect(normalizeDesktopWindowControlsPosition('right')).toBe('right'); + expect(normalizeDesktopWindowControlsPosition('invalid')).toEqual(undefined); + }); + + test('left uses macOS traffic-light order', () => { + expect(getDesktopWindowControlsOrder('left')).toEqual(['close', 'minimize', 'maximize']); + }); + + test('right uses Windows order', () => { + expect(getDesktopWindowControlsOrder('right')).toEqual(['minimize', 'maximize', 'close']); + }); +}); diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index da2aed99..85755baa 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -941,9 +941,8 @@ export const settingsDict = { 'settings.openchamber.sessionRetention.toast.failedDeleteCount': 'Failed to delete {count} session(s)', 'settings.openchamber.desktopNetwork.title': 'Desktop Network Access', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Window controls position', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choose where minimize, maximize, and close buttons appear. Auto follows your operating system.', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choose where minimize, maximize, and close buttons appear. Defaults to the right.', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Window controls position', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': 'Auto', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Left', 'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Right', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Start OpenChamber at login', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index 13421f11..ae93eb68 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -908,9 +908,8 @@ export const settingsDict = { "settings.openchamber.sessionRetention.toast.failedDeleteCount": "No se pudo eliminar {count} sesión(es)", "settings.openchamber.desktopNetwork.title": "Acceso de red de escritorio", "settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posición de los controles de ventana", - "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Elige dónde aparecen los botones de minimizar, maximizar y cerrar. Automático sigue el sistema operativo.", + "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Elige dónde aparecen los botones de minimizar, maximizar y cerrar. Por defecto a la derecha.", "settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posición de los controles de ventana", - "settings.openchamber.desktopNetwork.option.windowControlsAuto": "Automático", "settings.openchamber.desktopNetwork.option.windowControlsLeft": "Izquierda", "settings.openchamber.desktopNetwork.option.windowControlsRight": "Derecha", "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar OpenChamber al iniciar sesión", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index e3919233..18be1a46 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -829,9 +829,8 @@ export const settingsDict = { 'settings.openchamber.sessionRetention.toast.failedDeleteCount': 'Échec de la suppression des sessions {count}', 'settings.openchamber.desktopNetwork.title': 'Accès au réseau de bureau', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position des contrôles de fenêtre', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choisissez où apparaissent les boutons Réduire, Agrandir et Fermer. Auto suit le système d’exploitation.', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Choisissez où apparaissent les boutons Réduire, Agrandir et Fermer. Par défaut à droite.', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Position des contrôles de fenêtre', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': 'Auto', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Gauche', 'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Droite', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Démarrez OpenChamber lors de la connexion', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index d8cb2267..0680d9eb 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -941,9 +941,8 @@ export const settingsDict = { 'settings.openchamber.sessionRetention.toast.failedDeleteCount': '{count} 個の Session の削除に失敗しました', 'settings.openchamber.desktopNetwork.title': 'Desktop ネットワークアクセス', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'ウィンドウコントロールの位置', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '最小化・最大化・閉じるボタンの表示位置を選びます。自動は OS に合わせます。', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '最小化・最大化・閉じるボタンの表示位置を選びます。デフォルトは右側です。', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'ウィンドウコントロールの位置', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': '自動', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左', 'settings.openchamber.desktopNetwork.option.windowControlsRight': '右', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'ログイン時に OpenChamber を起動', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 5eb403a3..01019123 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -908,9 +908,8 @@ export const settingsDict = { 'settings.openchamber.sessionRetention.toast.failedDeleteCount': '세션 {count}개를 삭제하지 못했습니다', 'settings.openchamber.desktopNetwork.title': 'Desktop 네트워크 접속', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': '창 컨트롤 위치', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '최소화, 최대화, 닫기 버튼이 표시될 위치를 선택합니다. 자동은 운영체제를 따릅니다.', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '최소화, 최대화, 닫기 버튼이 표시될 위치를 선택합니다. 기본값은 오른쪽입니다.', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '창 컨트롤 위치', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': '자동', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': '왼쪽', 'settings.openchamber.desktopNetwork.option.windowControlsRight': '오른쪽', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '로그인 시 OpenChamber 시작', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 66734daa..2a60dfd4 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -776,9 +776,8 @@ export const settingsDict = { 'settings.openchamber.desktopNetwork.hint.openNow': 'Otwórz z innego urządzenia: ', 'settings.openchamber.desktopNetwork.title': 'Dostęp sieciowy pulpitu', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Pozycja elementów sterujących oknem', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Wybierz, gdzie mają się pojawiać przyciski minimalizacji, maksymalizacji i zamykania. Automatycznie dopasowuje się do systemu operacyjnego.', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': 'Wybierz, gdzie mają się pojawiać przyciski minimalizacji, maksymalizacji i zamykania. Domyślnie po prawej.', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Pozycja elementów sterujących oknem', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': 'Automatycznie', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Lewo', 'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Prawo', 'settings.openchamber.git.changesViewAria': 'Tryb widoku zmian Git', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index acf36555..04853cda 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -908,9 +908,8 @@ export const settingsDict = { "settings.openchamber.sessionRetention.toast.failedDeleteCount": "Não foi possível excluir {count} sessão(es)", "settings.openchamber.desktopNetwork.title": "Acesso de rede do desktop", "settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posição dos controles da janela", - "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Escolha onde os botões de minimizar, maximizar e fechar aparecem. Automático segue o sistema operacional.", + "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Escolha onde os botões de minimizar, maximizar e fechar aparecem. O padrão é à direita.", "settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posição dos controles da janela", - "settings.openchamber.desktopNetwork.option.windowControlsAuto": "Automático", "settings.openchamber.desktopNetwork.option.windowControlsLeft": "Esquerda", "settings.openchamber.desktopNetwork.option.windowControlsRight": "Direita", "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar o OpenChamber ao fazer login", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index 0671ae01..a714f93c 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -908,9 +908,8 @@ export const settingsDict = { "settings.openchamber.sessionRetention.toast.failedDeleteCount": "Не вдалося видалити сесій: {count}", "settings.openchamber.desktopNetwork.title": "Мережевий доступ десктопного застосунку", "settings.openchamber.desktopNetwork.field.windowControlsPosition": "Позиція елементів керування вікном", - "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Виберіть, де з’являються кнопки згортання, розгортання та закриття. Авто відповідає вашій операційній системі.", + "settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Виберіть, де з’являються кнопки згортання, розгортання та закриття. За замовчуванням справа.", "settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Позиція елементів керування вікном", - "settings.openchamber.desktopNetwork.option.windowControlsAuto": "Авто", "settings.openchamber.desktopNetwork.option.windowControlsLeft": "Зліва", "settings.openchamber.desktopNetwork.option.windowControlsRight": "Справа", "settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Запускати OpenChamber під час входу в систему", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index d31fc788..563e8427 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -908,9 +908,8 @@ export const settingsDict = { 'settings.openchamber.sessionRetention.toast.failedDeleteCount': '删除 {count} 个会话失败', 'settings.openchamber.desktopNetwork.title': '桌面端网络访问', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': '窗口控件位置', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '选择最小化、最大化和关闭按钮的显示位置。自动会按操作系统决定。', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '选择最小化、最大化和关闭按钮的显示位置。默认在右侧。', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '窗口控件位置', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': '自动', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左侧', 'settings.openchamber.desktopNetwork.option.windowControlsRight': '右侧', 'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登录时启动 OpenChamber', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 9d85784f..90b47898 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -905,9 +905,8 @@ 'settings.openchamber.sessionRetention.toast.failedDeleteCount': '刪除 {count} 個工作階段失敗', 'settings.openchamber.desktopNetwork.title': '桌面端網路存取', 'settings.openchamber.desktopNetwork.field.windowControlsPosition': '視窗控制項位置', - 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '選擇最小化、最大化和關閉按鈕的顯示位置。自動會依作業系統決定。', + 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '選擇最小化、最大化和關閉按鈕的顯示位置。預設在右側。', 'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '視窗控制項位置', - 'settings.openchamber.desktopNetwork.option.windowControlsAuto': '自動', 'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左側', 'settings.openchamber.desktopNetwork.option.windowControlsRight': '右側', 'settings.openchamber.desktopNetwork.field.allowLanAccessAria': '允許桌面 sidecar 區域網路存取', diff --git a/packages/ui/src/lib/persistence.ts b/packages/ui/src/lib/persistence.ts index 38bf9af9..7db5b302 100644 --- a/packages/ui/src/lib/persistence.ts +++ b/packages/ui/src/lib/persistence.ts @@ -738,10 +738,14 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => { store.setWeekStartPreference(settings.weekStartPreference); } } - if (typeof settings.desktopWindowControlsPosition === 'string' - && (settings.desktopWindowControlsPosition === 'auto' || settings.desktopWindowControlsPosition === 'left' || settings.desktopWindowControlsPosition === 'right')) { - if (settings.desktopWindowControlsPosition !== store.desktopWindowControlsPosition) { - store.setDesktopWindowControlsPosition(settings.desktopWindowControlsPosition); + if (typeof settings.desktopWindowControlsPosition === 'string') { + const nextPosition = settings.desktopWindowControlsPosition === 'left' + ? 'left' + : (settings.desktopWindowControlsPosition === 'right' || settings.desktopWindowControlsPosition === 'auto') + ? 'right' + : null; + if (nextPosition && nextPosition !== store.desktopWindowControlsPosition) { + store.setDesktopWindowControlsPosition(nextPosition); } } if (typeof settings.chatRenderMode === 'string' @@ -1364,9 +1368,16 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => { && (candidate.weekStartPreference === 'auto' || candidate.weekStartPreference === 'sunday' || candidate.weekStartPreference === 'monday')) { result.weekStartPreference = candidate.weekStartPreference; } - if (typeof candidate.desktopWindowControlsPosition === 'string' - && (candidate.desktopWindowControlsPosition === 'auto' || candidate.desktopWindowControlsPosition === 'left' || candidate.desktopWindowControlsPosition === 'right')) { - result.desktopWindowControlsPosition = candidate.desktopWindowControlsPosition; + if (typeof candidate.desktopWindowControlsPosition === 'string') { + if (candidate.desktopWindowControlsPosition === 'left') { + result.desktopWindowControlsPosition = 'left'; + } else if ( + candidate.desktopWindowControlsPosition === 'right' + || candidate.desktopWindowControlsPosition === 'auto' + ) { + // Legacy "auto" never read OS chrome config; treat as right. + result.desktopWindowControlsPosition = 'right'; + } } if (typeof candidate.chatRenderMode === 'string' && (candidate.chatRenderMode === 'sorted' || candidate.chatRenderMode === 'live')) { diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index 3c650a64..7cebc343 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -21,7 +21,7 @@ export type ActivityRenderMode = 'collapsed' | 'summary'; export type SessionRetentionAction = 'archive' | 'delete'; export type TimeFormatPreference = 'auto' | '12h' | '24h'; export type WeekStartPreference = 'auto' | 'sunday' | 'monday'; -export type DesktopWindowControlsPosition = 'auto' | 'left' | 'right'; +export type DesktopWindowControlsPosition = 'left' | 'right'; export type FileEditorKeymap = 'default' | 'vim'; function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap { @@ -971,7 +971,7 @@ export const useUIStore = create()( showExpandedEditTools: false, timeFormatPreference: 'auto', weekStartPreference: 'auto', - desktopWindowControlsPosition: 'auto', + desktopWindowControlsPosition: 'right', mermaidRenderingMode: 'svg', userMessageRenderingMode: 'markdown', collapsibleUserMessages: true, @@ -2148,7 +2148,7 @@ export const useUIStore = create()( set({ weekStartPreference: value }); }, setDesktopWindowControlsPosition: (value) => { - set({ desktopWindowControlsPosition: value }); + set({ desktopWindowControlsPosition: value === 'left' ? 'left' : 'right' }); }, setMermaidRenderingMode: (value) => { set({ mermaidRenderingMode: value }); @@ -2234,13 +2234,20 @@ export const useUIStore = create()( { name: 'ui-store', storage: createDeferredSafeJSONStorage(), - version: 11, + version: 12, migrate: (persistedState, version) => { if (!persistedState || typeof persistedState !== 'object') { return persistedState; } const state = persistedState as Record; + // v11 -> v12: drop legacy window-controls "auto" (always meant right). + if (version < 12) { + if (state.desktopWindowControlsPosition === 'auto' || state.desktopWindowControlsPosition == null) { + state.desktopWindowControlsPosition = 'right'; + } + } + // v10 -> v11: move the previous terminal font default forward. if (version < 11 && state.terminalFontSize === 13) { state.terminalFontSize = 14; diff --git a/packages/web/server/lib/opencode/settings-helpers.js b/packages/web/server/lib/opencode/settings-helpers.js index c6882388..b95e1145 100644 --- a/packages/web/server/lib/opencode/settings-helpers.js +++ b/packages/web/server/lib/opencode/settings-helpers.js @@ -190,8 +190,11 @@ export const createSettingsHelpers = (dependencies) => { } if (typeof candidate.desktopWindowControlsPosition === 'string') { const mode = candidate.desktopWindowControlsPosition.trim(); - if (mode === 'auto' || mode === 'left' || mode === 'right') { - result.desktopWindowControlsPosition = mode; + // Legacy "auto" never read OS chrome config; persist as the right default. + if (mode === 'auto' || mode === 'right') { + result.desktopWindowControlsPosition = 'right'; + } else if (mode === 'left') { + result.desktopWindowControlsPosition = 'left'; } } if (candidate.permissionAutoAccept && typeof candidate.permissionAutoAccept === 'object' && !Array.isArray(candidate.permissionAutoAccept)) { diff --git a/packages/web/server/lib/opencode/settings-helpers.test.js b/packages/web/server/lib/opencode/settings-helpers.test.js index accc81ee..20387b55 100644 --- a/packages/web/server/lib/opencode/settings-helpers.test.js +++ b/packages/web/server/lib/opencode/settings-helpers.test.js @@ -154,6 +154,21 @@ describe('settings helpers', () => { }); }); + it('normalizes desktopWindowControlsPosition and maps legacy auto to right', () => { + const helpers = createTestHelpers(); + + expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsPosition: 'left' })).toEqual({ + desktopWindowControlsPosition: 'left', + }); + expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsPosition: 'right' })).toEqual({ + desktopWindowControlsPosition: 'right', + }); + expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsPosition: 'auto' })).toEqual({ + desktopWindowControlsPosition: 'right', + }); + expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsPosition: 'center' })).toEqual({}); + }); + it('sanitizes the persisted permission auto-accept policy', () => { const helpers = createTestHelpers();