fix(ui): move window controls position setting to Appearance (#2406)

* fix(ui): move window controls position setting to Appearance

Keep desktop chrome layout with other visual settings; General retains
startup, tray, network, and access controls. Update settings search page
mapping so the control still highlights correctly.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(ui): align Linux window controls and stop actions overlap

Size left-side window controls to match the titlebar icon row and drop
negative margins so the reserved titlebar width stays accurate. This keeps
the project-actions chevron from colliding with the New session title.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

* fix(ui): stack window-controls description above chips

Show the Appearance window-controls helper copy full-width under the
section title, with Auto/Left/Right chips beneath it.

Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>

---------
Authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Serhii Dziupin
2026-07-24 21:27:38 +03:00
committed by GitHub
parent ea157c9aa8
commit 484fe8bc18
7 changed files with 207 additions and 200 deletions
@@ -48,10 +48,19 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({
return null;
}
const buttonClassName = 'app-region-no-drag inline-flex h-12 w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary';
const containerClassName = position === 'left'
? 'app-region-no-drag -ml-3 mr-2 flex h-12 shrink-0 items-center'
: 'app-region-no-drag -mr-3 ml-2 flex h-12 shrink-0 items-center';
// Left-side controls sit in the same cluster as h-8 titlebar icon buttons
// (app menu / sidebar / project actions). Match that size and avoid negative
// margins so TitlebarLeftControls can publish an accurate reserved width —
// otherwise the project-actions chevron overlaps the session title.
// Right-side controls keep a taller Windows-style hit target.
const isLeft = position === 'left';
const buttonClassName = cn(
'app-region-no-drag inline-flex items-center justify-center text-muted-foreground transition-colors hover:bg-interactive-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary',
isLeft ? 'h-8 w-8 rounded-md' : 'h-12 w-11',
);
const containerClassName = isLeft
? '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 (
<div className={containerClassName} aria-label={t('header.windowControls.groupAria')}>
@@ -56,7 +56,9 @@ export const TitlebarLeftControls: React.FC = () => {
}
const publishWidth = () => {
const width = node.getBoundingClientRect().width;
// Prefer scrollWidth so negative child margins / overflow cannot under-report
// the space the overlay actually occupies over the header.
const width = Math.max(node.getBoundingClientRect().width, node.scrollWidth);
document.documentElement.style.setProperty('--oc-titlebar-controls-width', `${Math.round(width)}px`);
};
@@ -14,40 +14,24 @@ import {
setDesktopKeepAwake,
setDesktopLaunchAtLogin,
setDesktopMinimizeToTray,
usesFramelessElectronChrome,
type DesktopWindowControlsPosition,
} from '@/lib/desktop';
import { useI18n } from '@/lib/i18n';
import { updateDesktopSettings } from '@/lib/persistence';
import { runtimeFetch } from '@/lib/runtime-fetch';
import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch';
import { useUIStore } from '@/stores/useUIStore';
import {
SettingsSection,
SettingsCheckboxRow,
SettingsChipGroup,
SettingsFieldRow,
SETTINGS_OPTION_STACK_CLASS,
SettingsStackedField,
SETTINGS_ICON_BUTTON_CLASS,
} from '@/components/sections/shared/SettingsSection';
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' },
];
export const DesktopNetworkSettings: React.FC = () => {
const { t } = useI18n();
const tUnsafe = React.useCallback((key: string) => t(key as Parameters<typeof t>[0]), [t]);
const isLocalDesktop = isDesktopShell() && isDesktopLocalOriginActive();
const isMacDesktop = isLocalDesktop
&& typeof window !== 'undefined'
&& window.__OPENCHAMBER_PLATFORM__ === 'darwin';
const showWindowControlsPosition = usesFramelessElectronChrome();
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
const [savedValue, setSavedValue] = React.useState(false);
const [draftValue, setDraftValue] = React.useState(false);
const [savedPassword, setSavedPassword] = React.useState('');
@@ -242,11 +226,6 @@ export const DesktopNetworkSettings: React.FC = () => {
}
}, []);
const handleWindowControlsPositionChange = React.useCallback((value: DesktopWindowControlsPosition) => {
setDesktopWindowControlsPosition(value);
void updateDesktopSettings({ desktopWindowControlsPosition: value });
}, [setDesktopWindowControlsPosition]);
const handleLaunchAtLoginToggle = React.useCallback(async () => {
if (!launchAtLoginSupported || isSavingLaunchAtLogin) {
return;
@@ -362,181 +341,156 @@ export const DesktopNetworkSettings: React.FC = () => {
}
}, [draftMacMenuBarEnabled, draftPassword, draftValue, isDirty, t]);
if (!isLocalDesktop && !showWindowControlsPosition) {
if (!isLocalDesktop) {
return null;
}
return (
<>
{showWindowControlsPosition ? (
<SettingsSection title={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}>
<SettingsFieldRow
settingsItem="sessions.desktop-window-controls-position"
label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
alignEnd={false}
controlClassName="flex-col items-stretch"
>
<SettingsChipGroup
value={desktopWindowControlsPosition}
options={WINDOW_CONTROLS_POSITION_OPTIONS.map((option) => ({
value: option.id,
label: tUnsafe(option.labelKey),
}))}
onChange={handleWindowControlsPositionChange}
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionAria')}
/>
</SettingsFieldRow>
</SettingsSection>
) : null}
{isLocalDesktop ? (
<SettingsSection title={t('settings.openchamber.desktopNetwork.title')}>
<div className="space-y-3">
{(launchAtLoginSupported || isMacDesktop || minimizeToTraySupported || keepAwakeSupported) ? (
<div className={SETTINGS_OPTION_STACK_CLASS}>
{launchAtLoginSupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-launch-at-login"
checked={launchAtLoginEnabled}
onChange={(checked) => {
if (checked === launchAtLoginEnabled) return;
void handleLaunchAtLoginToggle();
}}
disabled={isSavingLaunchAtLogin}
label={t('settings.openchamber.desktopNetwork.field.launchAtLogin')}
info={t('settings.openchamber.desktopNetwork.field.launchAtLoginDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.launchAtLoginAria')}
/>
) : null}
{isMacDesktop ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-mac-menu-bar"
checked={draftMacMenuBarEnabled}
onChange={setDraftMacMenuBarEnabled}
disabled={isLoading || isSaving}
label={t('settings.openchamber.desktopNetwork.field.macMenuBar')}
info={t('settings.openchamber.desktopNetwork.field.macMenuBarDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.macMenuBarAria')}
/>
) : null}
{minimizeToTraySupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-minimize-to-tray"
checked={minimizeToTrayEnabled}
onChange={(checked) => {
if (checked === minimizeToTrayEnabled) return;
void handleMinimizeToTrayToggle();
}}
disabled={isSavingMinimizeToTray}
label={t('settings.openchamber.desktopNetwork.field.minimizeToTray')}
info={t('settings.openchamber.desktopNetwork.field.minimizeToTrayDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.minimizeToTrayAria')}
/>
) : null}
{keepAwakeSupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-keep-awake"
checked={keepAwakeEnabled}
onChange={(checked) => {
if (checked === keepAwakeEnabled) return;
void handleKeepAwakeToggle();
}}
disabled={isSavingKeepAwake}
label={t('settings.openchamber.desktopNetwork.field.keepAwake')}
info={t('settings.openchamber.desktopNetwork.field.keepAwakeDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.keepAwakeAria')}
/>
) : null}
</div>
) : null}
<SettingsStackedField
settingsItem="sessions.desktop-ui-password"
label={(
<label htmlFor="desktop-ui-password">
{t('settings.openchamber.desktopPassword.field.password')}
</label>
)}
info={t('settings.openchamber.desktopPassword.field.passwordDescription')}
>
<Input
id="desktop-ui-password"
type={showPassword ? 'text' : 'password'}
className="h-8 min-w-0 flex-1"
value={draftPassword}
onChange={(event) => handlePasswordChange(event.target.value)}
placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')}
disabled={isLoading || isSaving}
required={draftValue}
aria-invalid={lanRequiresPassword}
/>
<Button
type="button"
variant="ghost"
size="xs"
onClick={() => setShowPassword((current: boolean) => !current)}
className={SETTINGS_ICON_BUTTON_CLASS}
aria-label={t(showPassword ? 'settings.openchamber.desktopPassword.actions.hidePassword' : 'settings.openchamber.desktopPassword.actions.showPassword')}
aria-pressed={showPassword}
>
<Icon name={showPassword ? 'eye-off' : 'eye'} className="h-4 w-4" />
</Button>
</SettingsStackedField>
<div className={SETTINGS_OPTION_STACK_CLASS}>
<SettingsSection title={t('settings.openchamber.desktopNetwork.title')}>
<div className="space-y-3">
{(launchAtLoginSupported || isMacDesktop || minimizeToTraySupported || keepAwakeSupported) ? (
<div className={SETTINGS_OPTION_STACK_CLASS}>
{launchAtLoginSupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-lan-access"
checked={draftValue}
onChange={setDraftValue}
disabled={isLoading || isSaving}
label={t('settings.openchamber.desktopNetwork.field.allowLanAccess')}
info={t('settings.openchamber.desktopNetwork.field.allowLanAccessDescription')}
description={(
<>
<span className="block text-[var(--status-warning)]/85">
{t('settings.openchamber.desktopNetwork.field.warning')}
</span>
{lanRequiresPassword || lanBlockedByMissingPassword ? (
<span className="block text-[var(--status-warning)]/85">
{t('settings.openchamber.desktopNetwork.field.passwordRequiredWarning')}
</span>
) : null}
</>
)}
ariaLabel={t('settings.openchamber.desktopNetwork.field.allowLanAccessAria')}
settingsItem="sessions.desktop-launch-at-login"
checked={launchAtLoginEnabled}
onChange={(checked) => {
if (checked === launchAtLoginEnabled) return;
void handleLaunchAtLoginToggle();
}}
disabled={isSavingLaunchAtLogin}
label={t('settings.openchamber.desktopNetwork.field.launchAtLogin')}
info={t('settings.openchamber.desktopNetwork.field.launchAtLoginDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.launchAtLoginAria')}
/>
</div>
{error ? (
<div className="typography-micro text-[var(--status-error)]">{error}</div>
) : null}
{lanUrl ? (
<div className="typography-micro text-muted-foreground/80">
{isDirty && !savedValue
? t('settings.openchamber.desktopNetwork.hint.openAfterRestart')
: t('settings.openchamber.desktopNetwork.hint.openNow')}
<span className="font-mono text-foreground">{lanUrl}</span>
</div>
{isMacDesktop ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-mac-menu-bar"
checked={draftMacMenuBarEnabled}
onChange={setDraftMacMenuBarEnabled}
disabled={isLoading || isSaving}
label={t('settings.openchamber.desktopNetwork.field.macMenuBar')}
info={t('settings.openchamber.desktopNetwork.field.macMenuBarDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.macMenuBarAria')}
/>
) : null}
<div className="flex justify-start py-1.5">
<Button
type="button"
size="xs"
onClick={handleSaveAndRestart}
disabled={saveDisabled}
className="shrink-0 !font-normal"
>
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.desktopNetwork.actions.saveAndRestart')}
</Button>
</div>
{minimizeToTraySupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-minimize-to-tray"
checked={minimizeToTrayEnabled}
onChange={(checked) => {
if (checked === minimizeToTrayEnabled) return;
void handleMinimizeToTrayToggle();
}}
disabled={isSavingMinimizeToTray}
label={t('settings.openchamber.desktopNetwork.field.minimizeToTray')}
info={t('settings.openchamber.desktopNetwork.field.minimizeToTrayDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.minimizeToTrayAria')}
/>
) : null}
{keepAwakeSupported ? (
<SettingsCheckboxRow
settingsItem="sessions.desktop-keep-awake"
checked={keepAwakeEnabled}
onChange={(checked) => {
if (checked === keepAwakeEnabled) return;
void handleKeepAwakeToggle();
}}
disabled={isSavingKeepAwake}
label={t('settings.openchamber.desktopNetwork.field.keepAwake')}
info={t('settings.openchamber.desktopNetwork.field.keepAwakeDescription')}
ariaLabel={t('settings.openchamber.desktopNetwork.field.keepAwakeAria')}
/>
) : null}
</div>
</SettingsSection>
) : null}
</>
) : null}
<SettingsStackedField
settingsItem="sessions.desktop-ui-password"
label={(
<label htmlFor="desktop-ui-password">
{t('settings.openchamber.desktopPassword.field.password')}
</label>
)}
info={t('settings.openchamber.desktopPassword.field.passwordDescription')}
>
<Input
id="desktop-ui-password"
type={showPassword ? 'text' : 'password'}
className="h-8 min-w-0 flex-1"
value={draftPassword}
onChange={(event) => handlePasswordChange(event.target.value)}
placeholder={t('settings.openchamber.desktopPassword.field.passwordPlaceholder')}
disabled={isLoading || isSaving}
required={draftValue}
aria-invalid={lanRequiresPassword}
/>
<Button
type="button"
variant="ghost"
size="xs"
onClick={() => setShowPassword((current: boolean) => !current)}
className={SETTINGS_ICON_BUTTON_CLASS}
aria-label={t(showPassword ? 'settings.openchamber.desktopPassword.actions.hidePassword' : 'settings.openchamber.desktopPassword.actions.showPassword')}
aria-pressed={showPassword}
>
<Icon name={showPassword ? 'eye-off' : 'eye'} className="h-4 w-4" />
</Button>
</SettingsStackedField>
<div className={SETTINGS_OPTION_STACK_CLASS}>
<SettingsCheckboxRow
settingsItem="sessions.desktop-lan-access"
checked={draftValue}
onChange={setDraftValue}
disabled={isLoading || isSaving}
label={t('settings.openchamber.desktopNetwork.field.allowLanAccess')}
info={t('settings.openchamber.desktopNetwork.field.allowLanAccessDescription')}
description={(
<>
<span className="block text-[var(--status-warning)]/85">
{t('settings.openchamber.desktopNetwork.field.warning')}
</span>
{lanRequiresPassword || lanBlockedByMissingPassword ? (
<span className="block text-[var(--status-warning)]/85">
{t('settings.openchamber.desktopNetwork.field.passwordRequiredWarning')}
</span>
) : null}
</>
)}
ariaLabel={t('settings.openchamber.desktopNetwork.field.allowLanAccessAria')}
/>
</div>
{error ? (
<div className="typography-micro text-[var(--status-error)]">{error}</div>
) : null}
{lanUrl ? (
<div className="typography-micro text-muted-foreground/80">
{isDirty && !savedValue
? t('settings.openchamber.desktopNetwork.hint.openAfterRestart')
: t('settings.openchamber.desktopNetwork.hint.openNow')}
<span className="font-mono text-foreground">{lanUrl}</span>
</div>
) : null}
<div className="flex justify-start py-1.5">
<Button
type="button"
size="xs"
onClick={handleSaveAndRestart}
disabled={saveDisabled}
className="shrink-0 !font-normal"
>
{isSaving ? t('settings.common.actions.saving') : t('settings.openchamber.desktopNetwork.actions.saveAndRestart')}
</Button>
</div>
</div>
</SettingsSection>
);
};
@@ -14,7 +14,7 @@ import { DesktopNetworkSettings } from './DesktopNetworkSettings';
import { KeyboardShortcutsSettings } from './KeyboardShortcutsSettings';
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
import { useDeviceInfo } from '@/lib/device';
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime, usesFramelessElectronChrome } from '@/lib/desktop';
import { isDesktopLocalOriginActive, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import { isCapacitorApp } from '@/lib/platform';
import { useI18n } from '@/lib/i18n';
import { subscribeRuntimeEndpointChanged } from '@/lib/runtime-switch';
@@ -42,7 +42,7 @@ export const OpenChamberPage: React.FC<OpenChamberPageProps> = ({ section }) =>
const showAbout = isMobile && isWebRuntime();
const isVSCode = isVSCodeRuntime();
void runtimeEndpointEpoch;
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
// If no section specified, show all (mobile/legacy behavior)
if (!section) {
@@ -135,7 +135,7 @@ const GeneralSectionContent: React.FC = () => {
const isVSCode = isVSCodeRuntime();
const runtimeEndpointEpoch = useRuntimeEndpointEpoch();
void runtimeEndpointEpoch;
const showDesktopNetworkSettings = isDesktopShell() && (isDesktopLocalOriginActive() || usesFramelessElectronChrome());
const showDesktopNetworkSettings = isDesktopShell() && isDesktopLocalOriginActive();
// Passkeys only work against the browser's WebAuthn UI on the web surface —
// desktop shell, VS Code, and the Capacitor app never show the login screen.
const showPasskeySettings = isWebRuntime() && !isDesktopShell() && !isVSCode && !isCapacitorApp();
@@ -162,6 +162,7 @@ const VisualSectionContent: React.FC = () => {
const isVSCode = isVSCodeRuntime();
return <OpenChamberVisualSettings visibleSettings={[
'theme',
'windowControlsPosition',
'pwaInstallName',
'pwaOrientation',
'mobileKeyboardMode',
@@ -17,7 +17,14 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Icon } from "@/components/icon/Icon";
import { invokeDesktop, isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
import {
invokeDesktop,
isDesktopShell,
isVSCodeRuntime,
isWebRuntime,
usesFramelessElectronChrome,
type DesktopWindowControlsPosition,
} from '@/lib/desktop';
import { useDeviceInfo } from '@/lib/device';
import { usePwaDetection } from '@/hooks/usePwaDetection';
import { updateDesktopSettings } from '@/lib/persistence';
@@ -271,7 +278,13 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
return mode === 'markdown' ? 'markdown' : 'plain';
};
type VisibleSetting = 'sessionAssist' | 'sessionGoal' | 'theme' | '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';
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' },
];
interface OpenChamberVisualSettingsProps {
/** Which settings to show. If undefined, shows all. */
@@ -408,6 +421,9 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
);
const dockBadgeEnabled = useUIStore(state => state.dockBadgeEnabled);
const setDockBadgeEnabled = useUIStore(state => state.setDockBadgeEnabled);
const showWindowControlsPosition = usesFramelessElectronChrome();
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
const reportUsage = useUIStore(state => state.reportUsage);
const setReportUsage = useUIStore(state => state.setReportUsage);
@@ -418,6 +434,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
void updateDesktopSettings({ reportUsage: enabled });
}, [setReportUsage]);
const handleWindowControlsPositionChange = React.useCallback((value: DesktopWindowControlsPosition) => {
setDesktopWindowControlsPosition(value);
void updateDesktopSettings({ desktopWindowControlsPosition: value });
}, [setDesktopWindowControlsPosition]);
const shouldAnimateChatPreview = (isSettingsDialogOpen || isMobile || isVSCodeRuntime())
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
@@ -596,11 +617,12 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
const isVSCode = isVSCodeRuntime();
const hasThemeSettings = shouldShow('theme') && !isVSCode;
const showWindowControlsPositionSetting = shouldShow('windowControlsPosition') && showWindowControlsPosition;
const hasLocalizationSettings = shouldShow('theme') || shouldShow('timeFormat') || shouldShow('weekStart');
const showMobileLayoutSetting = isMobile && isWebRuntime() && !isDesktopShell() && !isVSCode;
const hasAppearanceSettings = isVSCode
? hasLocalizationSettings
: (shouldShow('theme') || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
: (shouldShow('theme') || showWindowControlsPositionSetting || showMobileLayoutSetting || shouldShow('pwaInstallName') || shouldShow('pwaOrientation') || shouldShow('timeFormat') || shouldShow('weekStart'));
const hasLayoutSettings = shouldShow('fontSize') || shouldShow('terminalFontSize') || shouldShow('editorFontSize') || shouldShow('spacing') || (shouldShow('inputBarOffset') && isMobile);
const hasNavigationSettings = (shouldShow('terminalQuickKeys') && !isMobile) || ((shouldShow('terminalShell') || shouldShow('terminalLoginShell')) && !isVSCode) || shouldShow('fileEditorKeymap') || (shouldShow('expandedEditorToolbar') && !isVSCode);
const hasBehaviorSettings = shouldShow('mermaidRendering')
@@ -980,6 +1002,25 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
</SettingsSection>
)}
{showWindowControlsPositionSetting && (
<SettingsSection
title={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}
description={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
divider={hasThemeSettings}
settingsItem="sessions.desktop-window-controls-position"
>
<SettingsChipGroup
value={desktopWindowControlsPosition}
options={WINDOW_CONTROLS_POSITION_OPTIONS.map((option) => ({
value: option.id,
label: tUnsafe(option.labelKey),
}))}
onChange={handleWindowControlsPositionChange}
aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsPositionAria')}
/>
</SettingsSection>
)}
{hasLocalizationSettings && (
<SettingsSection title={t('settings.openchamber.visual.section.localization')}>
<SettingsTwoColumn>
+2 -2
View File
@@ -246,8 +246,8 @@ export const getElectronPlatform = (): string | null => {
return typeof platform === 'string' ? platform : null;
};
/** Width of the three in-app window control buttons (3 × w-11). */
export const DESKTOP_WINDOW_CONTROLS_WIDTH_PX = 132;
/** 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;
/** Windows and Linux use frameless windows with in-app minimize/maximize/close controls. */
export const usesFramelessElectronChrome = (): boolean => {
+1 -1
View File
@@ -392,7 +392,7 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
},
{
id: 'sessions.desktop-window-controls-position',
page: 'general',
page: 'appearance',
titleKey: 'settings.openchamber.desktopNetwork.field.windowControlsPosition',
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription',
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows'],