fix: integrate mini chat window controls on Windows
This commit is contained in:
@@ -2101,7 +2101,9 @@ const createMiniChatWindow = async ({ mode, sessionId = '', directory = '', proj
|
||||
icon: getWindowIconPath(),
|
||||
show: false,
|
||||
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,
|
||||
webPreferences: {
|
||||
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>
|
||||
);
|
||||
});
|
||||
@@ -32,6 +32,7 @@ import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||
import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
|
||||
import { UpdateDialog } from '@/components/ui/UpdateDialog';
|
||||
import { useDeviceInfo, useTabletStandalonePwaRuntime } from '@/lib/device';
|
||||
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 = {
|
||||
isMobile: boolean;
|
||||
githubAuthStatus: GitHubAuthStatus | null;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { ChatContainer } from '@/components/chat/ChatContainer';
|
||||
import { ChatSurfaceProvider } from '@/components/chat/ChatSurfaceContext';
|
||||
import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay';
|
||||
import { WindowsWindowControls } from '@/components/desktop/WindowsWindowControls';
|
||||
import { SessionSwitcherDropdown } from '@/components/session/SessionSwitcherDropdown';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
@@ -69,6 +70,9 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
const [pinned, setPinned] = React.useState(false);
|
||||
const macosMajor = typeof window !== 'undefined' ? window.__OPENCHAMBER_MACOS_MAJOR__ ?? 0 : 0;
|
||||
const hasMacTrafficLights = Number.isFinite(macosMajor) && macosMajor > 0;
|
||||
const isWindowsElectronDesktop = typeof window !== 'undefined'
|
||||
&& Boolean(window.__OPENCHAMBER_ELECTRON__)
|
||||
&& window.__OPENCHAMBER_PLATFORM__ === 'win32';
|
||||
const macosHeaderSizeClass = hasMacTrafficLights
|
||||
? macosMajor >= 26
|
||||
? 'h-12'
|
||||
@@ -258,7 +262,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
className={cn(
|
||||
'flex items-center gap-3 border-b border-[var(--interactive-border)] bg-sidebar pr-3',
|
||||
hasMacTrafficLights ? 'pl-[5.5rem]' : 'pl-3',
|
||||
macosHeaderSizeClass || 'min-h-14',
|
||||
isWindowsElectronDesktop ? 'h-12' : macosHeaderSizeClass || 'min-h-14',
|
||||
)}
|
||||
style={dragRegionStyle}
|
||||
>
|
||||
@@ -327,6 +331,7 @@ const MiniChatHeader: React.FC<{ mode: MiniChatMode }> = ({ mode }) => {
|
||||
>
|
||||
<Icon name="external-link" className="h-4 w-4" />
|
||||
</Button>
|
||||
<WindowsWindowControls visible={isWindowsElectronDesktop} />
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user