{children}
diff --git a/packages/ui/src/components/icon/sprite.ts b/packages/ui/src/components/icon/sprite.ts
index 23387a55..fa282964 100644
--- a/packages/ui/src/components/icon/sprite.ts
+++ b/packages/ui/src/components/icon/sprite.ts
@@ -153,6 +153,7 @@ export const iconSpriteData = {
"lock-unlock": `
`,
"loop-right-ai": `
`,
"macbook": `
`,
+ "menu-2": `
`,
"menu-fold-2": `
`,
"menu-search": `
`,
"message-2": `
`,
diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx
index 46d81c95..c4116cf6 100644
--- a/packages/ui/src/components/layout/Header.tsx
+++ b/packages/ui/src/components/layout/Header.tsx
@@ -77,7 +77,7 @@ type HeaderIconActionButtonProps = {
visible?: boolean;
title: string;
ariaLabel: string;
- onClick: () => void;
+ onClick: React.MouseEventHandler
;
className?: string;
Icon: IconName;
iconClassName?: string;
@@ -115,6 +115,83 @@ 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 (
+
+
+
+
+
+ );
+});
+
type DesktopGitHubControlProps = {
isMobile: boolean;
githubAuthStatus: GitHubAuthStatus | null;
@@ -731,6 +808,13 @@ export const Header: React.FC = ({
return /Macintosh|Mac OS X/.test(navigator.userAgent || '');
}, []);
+ const isWindowsElectronDesktop = React.useMemo(() => {
+ if (typeof window === 'undefined') {
+ return false;
+ }
+ return Boolean(window.__OPENCHAMBER_ELECTRON__) && window.__OPENCHAMBER_PLATFORM__ === 'win32';
+ }, []);
+
const macosMajorVersion = React.useMemo(() => {
if (typeof window === 'undefined') {
return null;
@@ -1262,6 +1346,16 @@ export const Header: React.FC = ({
toggleSidebar();
}, [blurActiveElement, isMobile, isSessionSwitcherOpen, setSessionSwitcherOpen, toggleSidebar]);
+ const handleOpenWindowsAppMenu = React.useCallback((event: React.MouseEvent) => {
+ const rect = event.currentTarget.getBoundingClientRect();
+ void invokeDesktop('desktop_show_app_menu', {
+ x: rect.left,
+ y: rect.bottom,
+ }).catch((error) => {
+ console.warn('[header] failed to open app menu', error);
+ });
+ }, []);
+
const handleOpenDraftMiniChat = React.useCallback(() => {
void invokeDesktop('desktop_open_draft_mini_chat_window', {
directory: normalize(openDirectory || activeProject?.path || ''),
@@ -1454,7 +1548,7 @@ export const Header: React.FC = ({
}, [isDesktopApp, isMacPlatform, macosMajorVersion]);
const webWindowControlsOverlayStyle = React.useMemo(() => {
- if (isDesktopApp || isVSCode) {
+ if ((isDesktopApp && !isWindowsElectronDesktop) || isVSCode) {
return undefined;
}
@@ -1466,7 +1560,7 @@ export const Header: React.FC = ({
minHeight: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
height: 'max(3rem, var(--oc-wco-titlebar-height, 0px))',
};
- }, [isDesktopApp, isTabletStandalonePwa, isVSCode]);
+ }, [isDesktopApp, isTabletStandalonePwa, isVSCode, isWindowsElectronDesktop]);
const updateHeaderHeight = React.useCallback(() => {
if (typeof document === 'undefined') {
@@ -1884,6 +1978,15 @@ export const Header: React.FC = ({
role="tablist"
aria-label={t('header.navigation.mainAria')}
>
+ {isWindowsElectronDesktop ? (
+
+ ) : null}
= ({
Icon={'picture-in-picture-2'}
/>
{desktopSidebarActions}
+
diff --git a/packages/ui/src/components/layout/SidebarFilesTree.tsx b/packages/ui/src/components/layout/SidebarFilesTree.tsx
index 1295fb03..687bb560 100644
--- a/packages/ui/src/components/layout/SidebarFilesTree.tsx
+++ b/packages/ui/src/components/layout/SidebarFilesTree.tsx
@@ -319,7 +319,7 @@ const FileRow: React.FC