feat(desktop): gate traffic-lights behind window-controls style setting
This commit is contained in:
@@ -5,6 +5,80 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getDesktopWindowControlsOrder, invokeDesktop } from '@/lib/desktop';
|
||||
import type { DesktopWindowControlAction, DesktopWindowControlsSide } from '@/lib/desktop';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
// macOS chrome colors; intentionally theme-independent — these replicate a
|
||||
// foreign platform's chrome, not OpenChamber's own status tokens, so the
|
||||
// theme-system hex rule does not apply.
|
||||
const TRAFFIC_LIGHT_COLORS: Record<DesktopWindowControlAction, { fill: string; glyph: string }> = {
|
||||
close: { fill: '#FF5F57', glyph: '#7F0808' },
|
||||
minimize: { fill: '#FEBC2E', glyph: '#B3710C' },
|
||||
maximize: { fill: '#28C940', glyph: '#006200' },
|
||||
};
|
||||
|
||||
const TrafficLightGlyph: React.FC<{ action: DesktopWindowControlAction }> = ({ action }) => {
|
||||
if (action === 'close') {
|
||||
return (
|
||||
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.2} strokeLinecap="round" className="size-[9px]" aria-hidden>
|
||||
<path d="M3.5 3.5 L8.5 8.5 M8.5 3.5 L3.5 8.5" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
if (action === 'minimize') {
|
||||
return (
|
||||
<svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth={1.4} strokeLinecap="round" className="size-[9px]" aria-hidden>
|
||||
<path d="M3 6 L9 6" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
// Green expand: two paired corner triangles pointing diagonally outward.
|
||||
return (
|
||||
<svg viewBox="0 0 12 12" fill="currentColor" className="size-[9px]" aria-hidden>
|
||||
<path d="M5.25 2.25 L9.75 2.25 L9.75 6.75 Z" />
|
||||
<path d="M6.75 9.75 L2.25 9.75 L2.25 5.25 Z" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
type TrafficLightButtonProps = {
|
||||
action: DesktopWindowControlAction;
|
||||
isMaximized: boolean;
|
||||
onActivate: (action: DesktopWindowControlAction) => void;
|
||||
};
|
||||
|
||||
const TrafficLightButton: React.FC<TrafficLightButtonProps> = ({ action, isMaximized, onActivate }) => {
|
||||
const { t } = useI18n();
|
||||
const { fill, glyph } = TRAFFIC_LIGHT_COLORS[action];
|
||||
const label =
|
||||
action === 'close'
|
||||
? t('header.windowControls.close')
|
||||
: action === 'minimize'
|
||||
? t('header.windowControls.minimize')
|
||||
: isMaximized
|
||||
? t('header.windowControls.restore')
|
||||
: t('header.windowControls.maximize');
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onActivate(action)}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
// 24px-wide button wrapping a 14px circle centers it at a 24px interval
|
||||
// between neighbors, giving a 10px edge-to-edge gap. The 32px height
|
||||
// keeps the titlebar's vertical hit band.
|
||||
className="app-region-no-drag flex h-8 w-[24px] items-center justify-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
>
|
||||
<span
|
||||
className="flex size-3.5 items-center justify-center rounded-full shadow-[inset_0_0_0_0.5px_rgba(0,0,0,0.28)] transition-[filter] duration-75 active:brightness-90"
|
||||
style={{ backgroundColor: fill, color: glyph }}
|
||||
>
|
||||
<span className="flex opacity-0 transition-opacity duration-75 group-hover/wctl:opacity-100">
|
||||
<TrafficLightGlyph action={action} />
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
type WindowsWindowControlsProps = {
|
||||
visible: boolean;
|
||||
@@ -17,6 +91,7 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({
|
||||
}: WindowsWindowControlsProps) {
|
||||
const { t } = useI18n();
|
||||
const [isMaximized, setIsMaximized] = React.useState(false);
|
||||
const desktopWindowControlsStyle = useUIStore((state) => state.desktopWindowControlsStyle);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
@@ -48,12 +123,50 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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';
|
||||
// Order is side-driven for both styles: close, minimize, maximize on the
|
||||
// left; minimize, maximize, close on the right.
|
||||
const order = getDesktopWindowControlsOrder(position);
|
||||
|
||||
const activate = (action: DesktopWindowControlAction) => {
|
||||
if (action === 'close') {
|
||||
void invokeDesktop('desktop_close_current_window');
|
||||
return;
|
||||
}
|
||||
if (action === 'minimize') {
|
||||
void invokeDesktop('desktop_minimize_current_window');
|
||||
return;
|
||||
}
|
||||
void invokeDesktop<{ maximized?: boolean }>('desktop_toggle_current_window_maximized')
|
||||
.then((state) => setIsMaximized(Boolean(state?.maximized)))
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// Traffic-light chrome: macOS-style 14px circles in an h-8 band. Glyphs
|
||||
// reveal on any-cluster hover (group-hover/wctl). TitlebarLeftControls
|
||||
// measures and republishes the cluster width via ResizeObserver, so the
|
||||
// narrower footprint takes effect without touching reserved-width constants.
|
||||
if (desktopWindowControlsStyle === 'traffic-lights') {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'app-region-no-drag group/wctl flex h-8 shrink-0 items-center',
|
||||
isLeft ? 'mr-1' : 'ml-1',
|
||||
)}
|
||||
aria-label={t('header.windowControls.groupAria')}
|
||||
>
|
||||
{order.map((action) => (
|
||||
<TrafficLightButton key={action} action={action} isMaximized={isMaximized} onActivate={activate} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Classic Windows-style square buttons. Left side matches the h-8 titlebar
|
||||
// icon cluster (app menu / sidebar / project actions) and avoids negative
|
||||
// margins so TitlebarLeftControls publishes an accurate reserved width —
|
||||
// otherwise the project-actions chevron overlaps the session title. Right
|
||||
// side keeps a taller h-12 Windows-style hit target.
|
||||
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',
|
||||
@@ -113,7 +226,7 @@ export const WindowsWindowControls = React.memo(function WindowsWindowControls({
|
||||
|
||||
return (
|
||||
<div className={containerClassName} aria-label={t('header.windowControls.groupAria')}>
|
||||
{getDesktopWindowControlsOrder(position).map(renderControl)}
|
||||
{order.map(renderControl)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
isWebRuntime,
|
||||
usesFramelessElectronChrome,
|
||||
type DesktopWindowControlsPosition,
|
||||
type DesktopWindowControlsStyle,
|
||||
} from '@/lib/desktop';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { usePwaDetection } from '@/hooks/usePwaDetection';
|
||||
@@ -285,6 +286,11 @@ const WINDOW_CONTROLS_POSITION_OPTIONS: Array<{ id: DesktopWindowControlsPositio
|
||||
{ id: 'right', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsRight' },
|
||||
];
|
||||
|
||||
const WINDOW_CONTROLS_STYLE_OPTIONS: Array<{ id: DesktopWindowControlsStyle; labelKey: string }> = [
|
||||
{ id: 'classic', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsClassic' },
|
||||
{ id: 'traffic-lights', labelKey: 'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights' },
|
||||
];
|
||||
|
||||
interface OpenChamberVisualSettingsProps {
|
||||
/** Which settings to show. If undefined, shows all. */
|
||||
visibleSettings?: VisibleSetting[];
|
||||
@@ -425,6 +431,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const showWindowControlsPosition = usesFramelessElectronChrome();
|
||||
const desktopWindowControlsPosition = useUIStore((state) => state.desktopWindowControlsPosition);
|
||||
const setDesktopWindowControlsPosition = useUIStore((state) => state.setDesktopWindowControlsPosition);
|
||||
const desktopWindowControlsStyle = useUIStore((state) => state.desktopWindowControlsStyle);
|
||||
const setDesktopWindowControlsStyle = useUIStore((state) => state.setDesktopWindowControlsStyle);
|
||||
const [chatRenderPreviewTick, setChatRenderPreviewTick] = React.useState(0);
|
||||
const reportUsage = useUIStore(state => state.reportUsage);
|
||||
const setReportUsage = useUIStore(state => state.setReportUsage);
|
||||
@@ -440,6 +448,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
void updateDesktopSettings({ desktopWindowControlsPosition: value });
|
||||
}, [setDesktopWindowControlsPosition]);
|
||||
|
||||
const handleWindowControlsStyleChange = React.useCallback((value: DesktopWindowControlsStyle) => {
|
||||
setDesktopWindowControlsStyle(value);
|
||||
void updateDesktopSettings({ desktopWindowControlsStyle: value });
|
||||
}, [setDesktopWindowControlsStyle]);
|
||||
|
||||
const shouldAnimateChatPreview = (isSettingsDialogOpen || isMobile || isVSCodeRuntime())
|
||||
&& (visibleSettings ? visibleSettings.includes('chatRenderMode') : true);
|
||||
|
||||
@@ -738,6 +751,10 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
const option = MOBILE_KEYBOARD_MODE_OPTIONS.find((item) => item.id === mobileKeyboardMode);
|
||||
return option ? tUnsafe(option.labelKey) : undefined;
|
||||
}, [mobileKeyboardMode, tUnsafe]);
|
||||
const selectedWindowControlsStyleLabel = React.useMemo(() => {
|
||||
const option = WINDOW_CONTROLS_STYLE_OPTIONS.find((item) => item.id === desktopWindowControlsStyle);
|
||||
return option ? tUnsafe(option.labelKey) : undefined;
|
||||
}, [desktopWindowControlsStyle, tUnsafe]);
|
||||
|
||||
const handleMobileLayoutPreferenceChange = React.useCallback((value: MobileLayoutPreference) => {
|
||||
if (value === mobileLayoutPreference) {
|
||||
@@ -1010,20 +1027,41 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
||||
|
||||
{showWindowControlsPositionSetting && (
|
||||
<SettingsSection
|
||||
title={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}
|
||||
description={t('settings.openchamber.desktopNetwork.field.windowControlsPositionDescription')}
|
||||
title={t('settings.openchamber.desktopNetwork.field.windowControls')}
|
||||
info={t('settings.openchamber.desktopNetwork.field.windowControlsDescription')}
|
||||
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')}
|
||||
/>
|
||||
<SettingsTwoColumn>
|
||||
<SettingsStackedField
|
||||
label={t('settings.openchamber.desktopNetwork.field.windowControlsPosition')}
|
||||
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')}
|
||||
/>
|
||||
</SettingsStackedField>
|
||||
<SettingsStackedField
|
||||
label={t('settings.openchamber.desktopNetwork.field.windowControlsStyle')}
|
||||
settingsItem="sessions.desktop-window-controls-style"
|
||||
>
|
||||
<Select value={desktopWindowControlsStyle} onValueChange={(value: DesktopWindowControlsStyle) => handleWindowControlsStyleChange(value)}>
|
||||
<SelectTrigger aria-label={t('settings.openchamber.desktopNetwork.field.windowControlsStyleAria')} size={SETTINGS_SELECT_SIZE} className={SETTINGS_SELECT_TRIGGER_CLASS}>
|
||||
<SelectValue>{selectedWindowControlsStyleLabel}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{WINDOW_CONTROLS_STYLE_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.id} value={option.id}>{tUnsafe(option.labelKey)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingsStackedField>
|
||||
</SettingsTwoColumn>
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
|
||||
@@ -71,6 +71,17 @@ html[data-oc-vibrancy] .main-content-safe-area {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
/* ── Linux frameless window border ────────────────────────────────────────
|
||||
`data-oc-window-border` is set on <html> by initLinuxWindowBorder()
|
||||
(ui/src/lib/desktop.ts), and `data-oc-window-maximized` drops the border
|
||||
so a maximized window stays flush with screen edges. Rounded corners
|
||||
were removed: Electron 41's transparent-window path on Linux is unreliable
|
||||
without --disable-gpu, and native rounded corners landed upstream only in
|
||||
Electron 43 (PR #51459). */
|
||||
html[data-oc-window-border]:not([data-oc-window-maximized]) #root {
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* Left sidebar: solid until the native layer is ready, then a translucent
|
||||
overlay so the vibrancy frosts through. */
|
||||
html[data-oc-vibrancy] .oc-vibrancy-surface {
|
||||
|
||||
@@ -41,6 +41,7 @@ export type SkillCatalogConfig = {
|
||||
export type DesktopWindowControlsPosition = 'left' | 'right';
|
||||
export type DesktopWindowControlsSide = 'left' | 'right';
|
||||
export type DesktopWindowControlAction = 'close' | 'minimize' | 'maximize';
|
||||
export type DesktopWindowControlsStyle = 'classic' | 'traffic-lights';
|
||||
|
||||
export type DesktopSettings = {
|
||||
themeId?: string;
|
||||
@@ -142,6 +143,7 @@ export type DesktopSettings = {
|
||||
pwaOrientation?: 'system' | 'portrait' | 'landscape';
|
||||
mobileKeyboardMode?: MobileKeyboardMode;
|
||||
desktopWindowControlsPosition?: DesktopWindowControlsPosition;
|
||||
desktopWindowControlsStyle?: DesktopWindowControlsStyle;
|
||||
inputSpellcheckEnabled?: boolean;
|
||||
showOpenCodeUpdateNotifications?: boolean;
|
||||
agentControlToolEnabled?: boolean;
|
||||
@@ -252,9 +254,6 @@ export const getElectronPlatform = (): string | null => {
|
||||
return typeof platform === 'string' ? platform : 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';
|
||||
|
||||
@@ -301,6 +300,37 @@ export const hasDesktopInvoke = (): boolean => {
|
||||
return typeof getDesktopBridge()?.invoke === 'function';
|
||||
};
|
||||
|
||||
/**
|
||||
* Linux frameless Electron windows show a 1px CSS border on #root (see
|
||||
* index.css). This function flips the attribute on <html> so the rule
|
||||
* matches, and tracks `data-oc-window-maximized` so the border drops when
|
||||
* maximized (a 1px line inset from every screen edge would look off).
|
||||
* No-op outside Linux frameless Electron.
|
||||
*/
|
||||
export const initLinuxWindowBorder = (): void => {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return;
|
||||
if (!usesFramelessElectronChrome()) return;
|
||||
const platform = getElectronPlatform();
|
||||
if (platform !== 'linux') return;
|
||||
|
||||
const root = document.documentElement;
|
||||
root.setAttribute('data-oc-window-border', '');
|
||||
|
||||
const applyMaximized = (maximized: boolean) => {
|
||||
root.toggleAttribute('data-oc-window-maximized', maximized);
|
||||
};
|
||||
|
||||
void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state')
|
||||
.then((state) => applyMaximized(Boolean(state?.maximized)))
|
||||
.catch(() => {});
|
||||
|
||||
const onMaximizedChange = (event: Event) => {
|
||||
const detail = (event as CustomEvent<{ maximized?: boolean }>).detail;
|
||||
applyMaximized(Boolean(detail?.maximized));
|
||||
};
|
||||
window.addEventListener('openchamber:window-maximized-changed', onMaximizedChange);
|
||||
};
|
||||
|
||||
export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke();
|
||||
|
||||
export const invokeDesktop = async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T | null> => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
normalizeDesktopWindowControlsPosition,
|
||||
resolveDesktopWindowControlsSide,
|
||||
} from './desktop';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
|
||||
describe('desktop window controls position', () => {
|
||||
test('defaults to right', () => {
|
||||
@@ -30,3 +31,24 @@ describe('desktop window controls position', () => {
|
||||
expect(getDesktopWindowControlsOrder('right')).toEqual(['minimize', 'maximize', 'close']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUIStore v12→v13 migration', () => {
|
||||
test('introduces classic style while preserving position', () => {
|
||||
const migrate = useUIStore.persist.getOptions().migrate!;
|
||||
const result = migrate({ desktopWindowControlsPosition: 'left' }, 12) as Record<string, unknown>;
|
||||
expect(result.desktopWindowControlsPosition).toBe('left');
|
||||
expect(result.desktopWindowControlsStyle).toBe('classic');
|
||||
});
|
||||
|
||||
test('does not overwrite a persisted style', () => {
|
||||
const migrate = useUIStore.persist.getOptions().migrate!;
|
||||
const result = migrate({ desktopWindowControlsStyle: 'traffic-lights' }, 12) as Record<string, unknown>;
|
||||
expect(result.desktopWindowControlsStyle).toBe('traffic-lights');
|
||||
});
|
||||
|
||||
test('coerces an invalid persisted style to classic', () => {
|
||||
const migrate = useUIStore.persist.getOptions().migrate!;
|
||||
const result = migrate({ desktopWindowControlsStyle: 'macos' }, 12) as Record<string, unknown>;
|
||||
expect(result.desktopWindowControlsStyle).toBe('classic');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -945,11 +945,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': 'Failed to archive {count} session(s)',
|
||||
'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. Defaults to the right.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': 'Window controls',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Choose where window controls appear and how they look.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Window controls position',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Window controls style',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Left',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Right',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Classic',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Traffic lights',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Start OpenChamber at login',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Start OpenChamber when you log in',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Starts the app in the background without opening a window. Use the desktop status icon to open it.',
|
||||
|
||||
@@ -912,11 +912,16 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "No se pudo archivar {count} sesión(es)",
|
||||
"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. Por defecto a la derecha.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControls": "Controles de ventana",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Elige dónde aparecen los controles de ventana y su apariencia.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posición",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posición de los controles de ventana",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo de los controles de ventana",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Izquierda",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Derecha",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Clásico",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Semáforo",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar OpenChamber al iniciar sesión",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar OpenChamber al iniciar sesión",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia la app en segundo plano sin abrir una ventana. Usa el icono de estado del escritorio para abrirla.",
|
||||
|
||||
@@ -833,11 +833,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': 'Échec de l\'archivage des sessions {count}',
|
||||
'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. Par défaut à droite.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': 'Contrôles de fenêtre',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Choisissez où apparaissent les contrôles de fenêtre et leur apparence.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Position',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Position des contrôles de fenêtre',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Style',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Style des contrôles de fenêtre',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Gauche',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Droite',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Classique',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Feux de circulation',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'Démarrez OpenChamber lors de la connexion',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': 'Démarrez OpenChamber lorsque vous vous connectez',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'Démarre l\'application en arrière-plan sans ouvrir de fenêtre. Utilisez l\'icône d\'état du bureau pour l\'ouvrir.',
|
||||
|
||||
@@ -945,11 +945,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '{count} 個の Session のアーカイブに失敗しました',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '{count} 個の Session の削除に失敗しました',
|
||||
'settings.openchamber.desktopNetwork.title': 'Desktop ネットワークアクセス',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'ウィンドウコントロールの位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '最小化・最大化・閉じるボタンの表示位置を選びます。デフォルトは右側です。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': 'ウィンドウコントロール',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'ウィンドウコントロールの表示位置とスタイルを選びます。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'ウィンドウコントロールの位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'スタイル',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'ウィンドウコントロールのスタイル',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'クラシック',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'トラフィックライト',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': 'ログイン時に OpenChamber を起動',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': 'ログイン時に OpenChamber を起動',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': 'ウィンドウを開かずにバックグラウンドでアプリを起動します。デスクトップのステータスアイコンから開けます。',
|
||||
|
||||
@@ -912,11 +912,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '세션 {count}개를 보관하지 못했습니다',
|
||||
'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.windowControls': '창 컨트롤',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '창 컨트롤의 위치와 모양을 선택합니다.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '위치',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '창 컨트롤 위치',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '스타일',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '창 컨트롤 스타일',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '왼쪽',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': '오른쪽',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '클래식',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '트래픽 라이트',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '로그인 시 OpenChamber 시작',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': '로그인 시 OpenChamber 시작',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '창을 열지 않고 백그라운드에서 앱을 시작합니다. 데스크톱 상태 아이콘으로 열 수 있습니다.',
|
||||
|
||||
@@ -780,11 +780,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.desktopNetwork.hint.openAfterRestart': 'Po restarcie otwórz z innego urządzenia: ',
|
||||
'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. Domyślnie po prawej.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': 'Elementy sterujące oknem',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': 'Wybierz, gdzie mają się pojawiać elementy sterujące oknem i jak mają wyglądać.',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': 'Pozycja',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': 'Pozycja elementów sterujących oknem',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': 'Styl',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': 'Styl elementów sterujących oknem',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': 'Lewo',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': 'Prawo',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': 'Klasyczny',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': 'Sygnalizacja świetlna',
|
||||
'settings.openchamber.git.changesViewAria': 'Tryb widoku zmian Git',
|
||||
'settings.openchamber.git.changesViewTitle': 'Widok zmian',
|
||||
'settings.openchamber.git.enableGitmoji': 'Włącz wybieranie Gitmoji',
|
||||
|
||||
@@ -912,11 +912,16 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Não foi possível arquivar {count} sessão(es)",
|
||||
"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. O padrão é à direita.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControls": "Controles da janela",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Escolha onde os controles da janela aparecem e como eles são exibidos.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Posição",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Posição dos controles da janela",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Estilo",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Estilo dos controles da janela",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Esquerda",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Direita",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Clássico",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Semáforo",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Iniciar o OpenChamber ao fazer login",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Iniciar o OpenChamber ao fazer login",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Inicia o app em segundo plano sem abrir uma janela. Use o ícone de status da área de trabalho para abrir.",
|
||||
|
||||
@@ -912,11 +912,16 @@ export const settingsDict = {
|
||||
"settings.openchamber.sessionRetention.toast.failedArchiveCount": "Не вдалося заархівувати сесій: {count}",
|
||||
"settings.openchamber.sessionRetention.toast.failedDeleteCount": "Не вдалося видалити сесій: {count}",
|
||||
"settings.openchamber.desktopNetwork.title": "Мережевий доступ десктопного застосунку",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Позиція елементів керування вікном",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPositionDescription": "Виберіть, де з’являються кнопки згортання, розгортання та закриття. За замовчуванням справа.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControls": "Елементи керування вікном",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsDescription": "Виберіть, де з’являються елементи керування вікном і як вони виглядають.",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPosition": "Позиція",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsPositionAria": "Позиція елементів керування вікном",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyle": "Стиль",
|
||||
"settings.openchamber.desktopNetwork.field.windowControlsStyleAria": "Стиль елементів керування вікном",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsLeft": "Зліва",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsRight": "Справа",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsClassic": "Класичний",
|
||||
"settings.openchamber.desktopNetwork.option.windowControlsTrafficLights": "Світлофор",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginAria": "Запускати OpenChamber під час входу в систему",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLogin": "Запускати OpenChamber під час входу в систему",
|
||||
"settings.openchamber.desktopNetwork.field.launchAtLoginDescription": "Запускає застосунок у фоні без відкриття вікна. Відкрийте його через піктограму стану на робочому столі.",
|
||||
|
||||
@@ -912,11 +912,16 @@ export const settingsDict = {
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '归档 {count} 个会话失败',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '删除 {count} 个会话失败',
|
||||
'settings.openchamber.desktopNetwork.title': '桌面端网络访问',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '窗口控件位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '选择最小化、最大化和关闭按钮的显示位置。默认在右侧。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': '窗口控件',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '选择窗口控件的位置和外观。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '窗口控件位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '样式',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '窗口控件样式',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左侧',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右侧',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '经典',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '红绿灯',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginAria': '登录时启动 OpenChamber',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLogin': '登录时启动 OpenChamber',
|
||||
'settings.openchamber.desktopNetwork.field.launchAtLoginDescription': '在后台启动应用且不打开窗口。可通过桌面状态图标打开。',
|
||||
|
||||
@@ -909,11 +909,16 @@
|
||||
'settings.openchamber.sessionRetention.toast.failedArchiveCount': '封存 {count} 個工作階段失敗',
|
||||
'settings.openchamber.sessionRetention.toast.failedDeleteCount': '刪除 {count} 個工作階段失敗',
|
||||
'settings.openchamber.desktopNetwork.title': '桌面端網路存取',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '視窗控制項位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription': '選擇最小化、最大化和關閉按鈕的顯示位置。預設在右側。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControls': '視窗控制項',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsDescription': '選擇視窗控制項的位置和外觀。',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPosition': '位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsPositionAria': '視窗控制項位置',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyle': '樣式',
|
||||
'settings.openchamber.desktopNetwork.field.windowControlsStyleAria': '視窗控制項樣式',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsLeft': '左側',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsRight': '右側',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsClassic': '經典',
|
||||
'settings.openchamber.desktopNetwork.option.windowControlsTrafficLights': '紅綠燈',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessAria': '允許桌面 sidecar 區域網路存取',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccess': '允許你本機網路中的其他裝置開啟此應用程式',
|
||||
'settings.openchamber.desktopNetwork.field.allowLanAccessDescription': '會重新啟動應用程式,以便手機、平板和同一 Wi‑Fi 下的其他電腦存取。',
|
||||
|
||||
@@ -557,6 +557,7 @@ const materializeAuthoritativeUiSettings = (settings: DesktopSettings): DesktopS
|
||||
timeFormatPreference: defaults.timeFormatPreference,
|
||||
weekStartPreference: defaults.weekStartPreference,
|
||||
desktopWindowControlsPosition: defaults.desktopWindowControlsPosition,
|
||||
desktopWindowControlsStyle: defaults.desktopWindowControlsStyle,
|
||||
chatRenderMode: defaults.chatRenderMode,
|
||||
activityRenderMode: defaults.activityRenderMode,
|
||||
mermaidRenderingMode: defaults.mermaidRenderingMode,
|
||||
@@ -748,6 +749,16 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
||||
store.setDesktopWindowControlsPosition(nextPosition);
|
||||
}
|
||||
}
|
||||
if (typeof settings.desktopWindowControlsStyle === 'string') {
|
||||
const nextStyle = settings.desktopWindowControlsStyle === 'traffic-lights'
|
||||
? 'traffic-lights'
|
||||
: settings.desktopWindowControlsStyle === 'classic'
|
||||
? 'classic'
|
||||
: null;
|
||||
if (nextStyle && nextStyle !== store.desktopWindowControlsStyle) {
|
||||
store.setDesktopWindowControlsStyle(nextStyle);
|
||||
}
|
||||
}
|
||||
if (typeof settings.chatRenderMode === 'string'
|
||||
&& (settings.chatRenderMode === 'sorted' || settings.chatRenderMode === 'live')) {
|
||||
if (settings.chatRenderMode !== store.chatRenderMode) {
|
||||
@@ -1379,6 +1390,11 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
||||
result.desktopWindowControlsPosition = 'right';
|
||||
}
|
||||
}
|
||||
if (typeof candidate.desktopWindowControlsStyle === 'string') {
|
||||
if (candidate.desktopWindowControlsStyle === 'classic' || candidate.desktopWindowControlsStyle === 'traffic-lights') {
|
||||
result.desktopWindowControlsStyle = candidate.desktopWindowControlsStyle;
|
||||
}
|
||||
}
|
||||
if (typeof candidate.chatRenderMode === 'string'
|
||||
&& (candidate.chatRenderMode === 'sorted' || candidate.chatRenderMode === 'live')) {
|
||||
result.chatRenderMode = candidate.chatRenderMode;
|
||||
|
||||
@@ -399,9 +399,16 @@ const SETTINGS_SEARCH_ITEMS: readonly SettingsSearchItem[] = [
|
||||
{
|
||||
id: 'sessions.desktop-window-controls-position',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.windowControlsPosition',
|
||||
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsPositionDescription',
|
||||
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows'],
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.windowControls',
|
||||
descriptionKey: 'settings.openchamber.desktopNetwork.field.windowControlsDescription',
|
||||
keywords: ['desktop', 'window', 'controls', 'minimize', 'maximize', 'close', 'titlebar', 'linux', 'windows', 'position'],
|
||||
isAvailable: (ctx) => ctx.isDesktop && (ctx.isWindows || !ctx.isMac),
|
||||
},
|
||||
{
|
||||
id: 'sessions.desktop-window-controls-style',
|
||||
page: 'appearance',
|
||||
titleKey: 'settings.openchamber.desktopNetwork.field.windowControlsStyle',
|
||||
keywords: ['desktop', 'window', 'controls', 'style', 'traffic', 'lights', 'classic', 'macos', 'titlebar'],
|
||||
isAvailable: (ctx) => ctx.isDesktop && (ctx.isWindows || !ctx.isMac),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ import { applyPersistedDirectoryPreferences } from './lib/directoryPersistence'
|
||||
import { startTypographyWatcher } from './lib/typographyWatcher'
|
||||
import { startModelPrefsAutoSave } from './lib/modelPrefsAutoSave'
|
||||
import { initializeLocale, I18nProvider } from './lib/i18n'
|
||||
import { initLinuxWindowBorder } from './lib/desktop'
|
||||
import type { RuntimeAPIs } from './lib/api/types'
|
||||
|
||||
declare global {
|
||||
@@ -26,6 +27,7 @@ const runtimeAPIs = (typeof window !== 'undefined' && window.__OPENCHAMBER_RUNTI
|
||||
})();
|
||||
|
||||
initializeLocale();
|
||||
initLinuxWindowBorder();
|
||||
|
||||
// Initialize settings asynchronously — the app renders with defaults first
|
||||
// and hydrates once persisted preferences are applied. Users with non-default
|
||||
|
||||
@@ -22,6 +22,7 @@ export type SessionRetentionAction = 'archive' | 'delete';
|
||||
export type TimeFormatPreference = 'auto' | '12h' | '24h';
|
||||
export type WeekStartPreference = 'auto' | 'sunday' | 'monday';
|
||||
export type DesktopWindowControlsPosition = 'left' | 'right';
|
||||
export type DesktopWindowControlsStyle = 'classic' | 'traffic-lights';
|
||||
export type FileEditorKeymap = 'default' | 'vim';
|
||||
|
||||
function normalizeFileEditorKeymap(value: unknown): FileEditorKeymap {
|
||||
@@ -693,6 +694,7 @@ interface UIStore {
|
||||
timeFormatPreference: TimeFormatPreference;
|
||||
weekStartPreference: WeekStartPreference;
|
||||
desktopWindowControlsPosition: DesktopWindowControlsPosition;
|
||||
desktopWindowControlsStyle: DesktopWindowControlsStyle;
|
||||
mermaidRenderingMode: MermaidRenderingMode;
|
||||
userMessageRenderingMode: UserMessageRenderingMode;
|
||||
collapsibleUserMessages: boolean;
|
||||
@@ -854,6 +856,7 @@ interface UIStore {
|
||||
setTimeFormatPreference: (value: TimeFormatPreference) => void;
|
||||
setWeekStartPreference: (value: WeekStartPreference) => void;
|
||||
setDesktopWindowControlsPosition: (value: DesktopWindowControlsPosition) => void;
|
||||
setDesktopWindowControlsStyle: (value: DesktopWindowControlsStyle) => void;
|
||||
setMermaidRenderingMode: (value: MermaidRenderingMode) => void;
|
||||
setUserMessageRenderingMode: (value: UserMessageRenderingMode) => void;
|
||||
setCollapsibleUserMessages: (value: boolean) => void;
|
||||
@@ -1004,6 +1007,7 @@ export const useUIStore = create<UIStore>()(
|
||||
timeFormatPreference: 'auto',
|
||||
weekStartPreference: 'auto',
|
||||
desktopWindowControlsPosition: 'right',
|
||||
desktopWindowControlsStyle: 'classic',
|
||||
mermaidRenderingMode: 'svg',
|
||||
userMessageRenderingMode: 'markdown',
|
||||
collapsibleUserMessages: true,
|
||||
@@ -2169,6 +2173,9 @@ export const useUIStore = create<UIStore>()(
|
||||
setDesktopWindowControlsPosition: (value) => {
|
||||
set({ desktopWindowControlsPosition: value === 'left' ? 'left' : 'right' });
|
||||
},
|
||||
setDesktopWindowControlsStyle: (value) => {
|
||||
set({ desktopWindowControlsStyle: value === 'traffic-lights' ? 'traffic-lights' : 'classic' });
|
||||
},
|
||||
setMermaidRenderingMode: (value) => {
|
||||
set({ mermaidRenderingMode: value });
|
||||
},
|
||||
@@ -2247,7 +2254,7 @@ export const useUIStore = create<UIStore>()(
|
||||
{
|
||||
name: 'ui-store',
|
||||
storage: createDeferredSafeJSONStorage(),
|
||||
version: 12,
|
||||
version: 13,
|
||||
migrate: (persistedState, version) => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return persistedState;
|
||||
@@ -2261,6 +2268,15 @@ export const useUIStore = create<UIStore>()(
|
||||
}
|
||||
}
|
||||
|
||||
// v12 -> v13: introduce window-controls style. Existing users keep
|
||||
// their persisted position; style defaults to "classic" so no one
|
||||
// suddenly sees traffic lights on upgrade.
|
||||
if (version < 13) {
|
||||
if (state.desktopWindowControlsStyle !== 'classic' && state.desktopWindowControlsStyle !== 'traffic-lights') {
|
||||
state.desktopWindowControlsStyle = 'classic';
|
||||
}
|
||||
}
|
||||
|
||||
// v10 -> v11: move the previous terminal font default forward.
|
||||
if (version < 11 && state.terminalFontSize === 13) {
|
||||
state.terminalFontSize = 14;
|
||||
@@ -2439,6 +2455,7 @@ export const useUIStore = create<UIStore>()(
|
||||
timeFormatPreference: state.timeFormatPreference,
|
||||
weekStartPreference: state.weekStartPreference,
|
||||
desktopWindowControlsPosition: state.desktopWindowControlsPosition,
|
||||
desktopWindowControlsStyle: state.desktopWindowControlsStyle,
|
||||
mermaidRenderingMode: state.mermaidRenderingMode,
|
||||
userMessageRenderingMode: state.userMessageRenderingMode,
|
||||
collapsibleUserMessages: state.collapsibleUserMessages,
|
||||
|
||||
@@ -197,6 +197,12 @@ export const createSettingsHelpers = (dependencies) => {
|
||||
result.desktopWindowControlsPosition = 'left';
|
||||
}
|
||||
}
|
||||
if (typeof candidate.desktopWindowControlsStyle === 'string') {
|
||||
const style = candidate.desktopWindowControlsStyle.trim();
|
||||
if (style === 'classic' || style === 'traffic-lights') {
|
||||
result.desktopWindowControlsStyle = style;
|
||||
}
|
||||
}
|
||||
if (candidate.permissionAutoAccept && typeof candidate.permissionAutoAccept === 'object' && !Array.isArray(candidate.permissionAutoAccept)) {
|
||||
const sessions = {};
|
||||
const sourceSessions = candidate.permissionAutoAccept.sessions;
|
||||
|
||||
@@ -169,6 +169,19 @@ describe('settings helpers', () => {
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsPosition: 'center' })).toEqual({});
|
||||
});
|
||||
|
||||
it('sanitizes desktopWindowControlsStyle and rejects unknown values', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsStyle: 'classic' })).toEqual({
|
||||
desktopWindowControlsStyle: 'classic',
|
||||
});
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsStyle: 'traffic-lights' })).toEqual({
|
||||
desktopWindowControlsStyle: 'traffic-lights',
|
||||
});
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsStyle: 'macos' })).toEqual({});
|
||||
expect(helpers.sanitizeSettingsUpdate({ desktopWindowControlsStyle: 'auto' })).toEqual({});
|
||||
});
|
||||
|
||||
it('sanitizes the persisted permission auto-accept policy', () => {
|
||||
const helpers = createTestHelpers();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user