fix: integrate mini chat window controls on Windows

This commit is contained in:
Bohdan Triapitsyn
2026-06-04 18:43:07 +03:00
parent 697dc7a041
commit 3214b79854
4 changed files with 93 additions and 79 deletions
+3 -1
View File
@@ -2101,7 +2101,9 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
icon: getWindowIconPath(), icon: getWindowIconPath(),
show: false, show: false,
backgroundColor: '#151313', backgroundColor: '#151313',
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default', frame: process.platform === 'win32' ? false : undefined,
autoHideMenuBar: process.platform !== 'darwin',
titleBarStyle: process.platform === 'darwin' || process.platform === 'win32' ? 'hidden' : 'default',
trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 17 } : undefined, trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 17 } : undefined,
webPreferences: { webPreferences: {
additionalArguments: [ additionalArguments: [
@@ -0,0 +1,83 @@
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';
type WindowsWindowControlsProps = {
visible: boolean;
};
export const WindowsWindowControls = React.memo(function WindowsWindowControls({ visible }: WindowsWindowControlsProps) {
const { t } = useI18n();
const [isMaximized, setIsMaximized] = React.useState(false);
useEffect(() => {
if (!visible) {
return;
}
let disposed = false;
void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state')
.then((state) => {
if (!disposed) {
setIsMaximized(Boolean(state?.maximized));
}
})
.catch(() => {});
const handleMaximizedChange = (event: Event) => {
const detail = (event as CustomEvent<{ maximized?: boolean }>).detail;
setIsMaximized(Boolean(detail?.maximized));
};
window.addEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
return () => {
disposed = true;
window.removeEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
};
}, [visible]);
if (!visible) {
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';
return (
<div className="app-region-no-drag -mr-3 ml-2 flex h-12 shrink-0 items-center" aria-label={t('header.windowControls.groupAria')}>
<button
type="button"
className={buttonClassName}
onClick={() => { void invokeDesktop('desktop_minimize_current_window'); }}
title={t('header.windowControls.minimize')}
aria-label={t('header.windowControls.minimize')}
>
<Icon name="subtract" className="h-4 w-4" />
</button>
<button
type="button"
className={buttonClassName}
onClick={() => {
void invokeDesktop<{ maximized?: boolean }>('desktop_toggle_current_window_maximized')
.then((state) => setIsMaximized(Boolean(state?.maximized)))
.catch(() => {});
}}
title={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
aria-label={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
>
<Icon name={isMaximized ? 'fullscreen-exit' : 'checkbox-blank'} className="h-3.5 w-3.5" />
</button>
<button
type="button"
className={cn(buttonClassName, 'hover:bg-status-error hover:text-status-error-foreground')}
onClick={() => { void invokeDesktop('desktop_close_current_window'); }}
title={t('header.windowControls.close')}
aria-label={t('header.windowControls.close')}
>
<Icon name="close" className="h-4 w-4" />
</button>
</div>
);
});
+1 -77
View File
@@ -32,6 +32,7 @@ import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { UpdateDialog } from '@/components/ui/UpdateDialog';
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device'; import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
import { cn, hasModifier } from '@/lib/utils'; import { cn, hasModifier } from '@/lib/utils';
@@ -127,83 +128,6 @@ const HeaderIconActionButton = React.memo(function HeaderIconActionButton({
); );
}); });
type WindowsWindowControlsProps = {
visible: boolean;
};
const WindowsWindowControls = React.memo(function WindowsWindowControls({ visible }: WindowsWindowControlsProps) {
const { t } = useI18n();
const [isMaximized, setIsMaximized] = React.useState(false);
useEffect(() => {
if (!visible) {
return;
}
let disposed = false;
void invokeDesktop<{ maximized?: boolean }>('desktop_get_current_window_state')
.then((state) => {
if (!disposed) {
setIsMaximized(Boolean(state?.maximized));
}
})
.catch(() => {});
const handleMaximizedChange = (event: Event) => {
const detail = (event as CustomEvent<{ maximized?: boolean }>).detail;
setIsMaximized(Boolean(detail?.maximized));
};
window.addEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
return () => {
disposed = true;
window.removeEventListener('openchamber:window-maximized-changed', handleMaximizedChange);
};
}, [visible]);
if (!visible) {
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';
return (
<div className="app-region-no-drag -mr-3 ml-2 flex h-12 shrink-0 items-center" aria-label={t('header.windowControls.groupAria')}>
<button
type="button"
className={buttonClassName}
onClick={() => { void invokeDesktop('desktop_minimize_current_window'); }}
title={t('header.windowControls.minimize')}
aria-label={t('header.windowControls.minimize')}
>
<Icon name="subtract" className="h-4 w-4" />
</button>
<button
type="button"
className={buttonClassName}
onClick={() => {
void invokeDesktop<{ maximized?: boolean }>('desktop_toggle_current_window_maximized')
.then((state) => setIsMaximized(Boolean(state?.maximized)))
.catch(() => {});
}}
title={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
aria-label={isMaximized ? t('header.windowControls.restore') : t('header.windowControls.maximize')}
>
<Icon name={isMaximized ? 'fullscreen-exit' : 'checkbox-blank'} className="h-3.5 w-3.5" />
</button>
<button
type="button"
className={cn(buttonClassName, 'hover:bg-status-error hover:text-status-error-foreground')}
onClick={() => { void invokeDesktop('desktop_close_current_window'); }}
title={t('header.windowControls.close')}
aria-label={t('header.windowControls.close')}
>
<Icon name="close" className="h-4 w-4" />
</button>
</div>
);
});
type DesktopGitHubControlProps = { type DesktopGitHubControlProps = {
isMobile: boolean; isMobile: boolean;
githubAuthStatus: GitHubAuthStatus | null; githubAuthStatus: GitHubAuthStatus | null;
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
import { ChatContainer } from '@/components/chat/ChatContainer'; import { ChatContainer } from '@/components/chat/ChatContainer';
import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext'; import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext';
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
import { SessionSwitcherDropdown } from '@/components/session/SessionSwitcherDropdown'; import { SessionSwitcherDropdown } from '@/components/session/SessionSwitcherDropdown';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n'; import { useI18n } from '@/lib/i18n';
@@ -69,6 +70,9 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
const [pinned, setPinned] = React.useState(false); const [pinned, setPinned] = React.useState(false);
const macosMajor = typeof window !== 'undefined' ? window.__OPENCHAMBER_MACOS_MAJOR__ ?? 0 : 0; const macosMajor = typeof window !== 'undefined' ? window.__OPENCHAMBER_MACOS_MAJOR__ ?? 0 : 0;
const hasMacTrafficLights = Number.isFinite(macosMajor) && macosMajor > 0; const hasMacTrafficLights = Number.isFinite(macosMajor) && macosMajor > 0;
const isWindowsElectronDesktop = typeof window !== 'undefined'
&& Boolean(window.__OPENCHAMBER_ELECTRON__)
&& window.__OPENCHAMBER_PLATFORM__ === 'win32';
const macosHeaderSizeClass = hasMacTrafficLights const macosHeaderSizeClass = hasMacTrafficLights
? macosMajor >= 26 ? macosMajor >= 26
? 'h-12' ? 'h-12'
@@ -258,7 +262,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
className={cn( className={cn(
'flex items-center gap-3 border-b border-[var(--interactive-border)] bg-sidebar pr-3', 'flex items-center gap-3 border-b border-[var(--interactive-border)] bg-sidebar pr-3',
hasMacTrafficLights ? 'pl-[5.5rem]' : 'pl-3', hasMacTrafficLights ? 'pl-[5.5rem]' : 'pl-3',
macosHeaderSizeClass || 'min-h-14', isWindowsElectronDesktop ? 'h-12' : macosHeaderSizeClass || 'min-h-14',
)} )}
style={dragRegionStyle} style={dragRegionStyle}
> >
@@ -327,6 +331,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
> >
<Icon name="external-link" className="h-4 w-4" /> <Icon name="external-link" className="h-4 w-4" />
</Button> </Button>
<WindowsWindowControls visible={isWindowsElectronDesktop} />
</header> </header>
); );
}; };