From 0009bd0706c895838a03dd54c30400d47630a6a0 Mon Sep 17 00:00:00 2001 From: SWangHash <88996709+SWangHash@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:44:27 +0800 Subject: [PATCH] fix(ohos): preserve native window controls --- .../NavPanel/components/PersistentFooterActions.tsx | 2 -- src/web-ui/src/app/components/SceneBar/SceneBar.tsx | 12 ++++++++---- src/web-ui/src/app/layout/AppLayout.tsx | 10 ++++++---- src/web-ui/src/app/layout/WorkspaceBody.tsx | 3 +++ .../components/WindowControls/WindowControls.tsx | 8 ++++++++ .../src/infrastructure/runtime/environment.test.ts | 11 ++++++++++- src/web-ui/src/infrastructure/runtime/environment.ts | 11 +++++++++++ 7 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx index ca23356f3a..11f99bb607 100644 --- a/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/PersistentFooterActions.tsx @@ -25,7 +25,6 @@ import { useToolbarModeContext } from '@/flow_chat/components/toolbar-mode/Toolb import { useNotification } from '@/shared/notification-system'; import { useAccountLoginState } from '@/infrastructure/account/useAccountLoginState'; import NotificationButton from '../../TitleBar/NotificationButton'; -import GithubStarButton from './GithubStarButton'; import { usePrivacy } from '../../Privacy/PrivacyContext'; import { hasActionableUnreadReply, @@ -425,7 +424,6 @@ const PersistentFooterActions: React.FC = () => {
-
diff --git a/src/web-ui/src/app/components/SceneBar/SceneBar.tsx b/src/web-ui/src/app/components/SceneBar/SceneBar.tsx index af1ff374fb..fb43165e76 100644 --- a/src/web-ui/src/app/components/SceneBar/SceneBar.tsx +++ b/src/web-ui/src/app/components/SceneBar/SceneBar.tsx @@ -27,6 +27,7 @@ interface SceneBarProps { onMaximize?: () => void; onClose?: () => void; isMaximized?: boolean; + reserveNativeWindowControls?: boolean; } const SceneBar: React.FC = ({ @@ -35,12 +36,14 @@ const SceneBar: React.FC = ({ onMaximize, onClose, isMaximized = false, + reserveNativeWindowControls = false, }) => { const { openTabs, activeTabId, tabDefs, activateScene, closeScene } = useSceneManager(); const sessionTitle = useCurrentSessionTitle(); const settingsTabTitle = useCurrentSettingsTabTitle(); const { t } = useI18n('common'); const hasWindowControls = !!(onMinimize && onMaximize && onClose); + const showWindowControls = hasWindowControls || reserveNativeWindowControls; const sceneBarClassName = `bitfun-scene-bar ${!hasWindowControls ? 'bitfun-scene-bar--no-controls' : ''} ${className}`.trim(); const isSingleTab = openTabs.length <= 1; const tabCount = Math.max(openTabs.length, 1); @@ -108,13 +111,14 @@ const SceneBar: React.FC = ({ })} - {hasWindowControls && ( + {showWindowControls && (
)} diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index ded1494b3d..e0698c1bc1 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -34,7 +34,7 @@ import { WorkspaceKind } from '@/shared/types'; import { SSHContext } from '@/features/ssh-remote/SSHRemoteContext'; import { shortcutManager, parseStoredKeybindings } from '@/infrastructure/services/ShortcutManager'; import { useSessionModeStore } from '../stores/sessionModeStore'; -import { isMacOSDesktopRuntime } from '@/infrastructure/runtime'; +import { isMacOSDesktopRuntime, isOpenHarmonyRuntime } from '@/infrastructure/runtime'; import { flowChatSessionConfigForWorkspace } from '../utils/projectSessionWorkspace'; import { notificationService } from '@/shared/notification-system'; import { AppearanceBackgroundMediaLayer, appearanceRuntime, useAppearance } from '@/infrastructure/appearance'; @@ -83,6 +83,7 @@ const AppLayout: React.FC = ({ className = '' }) => { const { t: tCommon } = useI18n('common'); const currentAppearance = useAppearance().current; const backgroundMedia = currentAppearance?.backgroundMedia; + const isOpenHarmony = isOpenHarmonyRuntime(); usePermissionRequestNotify(); const { currentWorkspace, @@ -757,10 +758,11 @@ const AppLayout: React.FC = ({ className = '' }) => { {/* Main content — always render WorkspaceBody; WelcomeScene in viewport handles no-workspace state */}
diff --git a/src/web-ui/src/app/layout/WorkspaceBody.tsx b/src/web-ui/src/app/layout/WorkspaceBody.tsx index 2dc9fa092b..1482abb023 100644 --- a/src/web-ui/src/app/layout/WorkspaceBody.tsx +++ b/src/web-ui/src/app/layout/WorkspaceBody.tsx @@ -32,6 +32,7 @@ interface WorkspaceBodyProps { onMaximize?: () => void; onClose?: () => void; isMaximized?: boolean; + reserveNativeWindowControls?: boolean; sceneOverlay?: React.ReactNode; } @@ -43,6 +44,7 @@ const WorkspaceBody: React.FC = ({ onMaximize, onClose, isMaximized = false, + reserveNativeWindowControls = false, sceneOverlay, }) => { const { workspace: currentWorkspace } = useCurrentWorkspace(); @@ -177,6 +179,7 @@ const WorkspaceBody: React.FC = ({ onMaximize={onMaximize} onClose={onClose} isMaximized={isMaximized} + reserveNativeWindowControls={reserveNativeWindowControls} /> = ({ 'data-testid-minimize': testIdMinimize, 'data-testid-maximize': testIdMaximize, 'data-testid-close': testIdClose, + nativePlaceholder = false, ...props }) => { const { t } = useTranslation('common'); + + if (nativePlaceholder) { + return
; + } + const defaultMinimizeIcon = ( diff --git a/src/web-ui/src/infrastructure/runtime/environment.test.ts b/src/web-ui/src/infrastructure/runtime/environment.test.ts index 97eb4b0747..38826b77b1 100644 --- a/src/web-ui/src/infrastructure/runtime/environment.test.ts +++ b/src/web-ui/src/infrastructure/runtime/environment.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { isTauriRuntime, supportsNativeWindowControls } from './environment'; +import { isOpenHarmonyRuntime, isTauriRuntime, supportsNativeWindowControls } from './environment'; const setTauriInternals = (value: unknown) => { vi.stubGlobal('window', { @@ -40,4 +40,13 @@ describe('runtime environment', () => { expect(isTauriRuntime()).toBe(true); expect(supportsNativeWindowControls()).toBe(true); }); + + it('recognizes the OpenHarmony webview runtime', () => { + vi.stubGlobal('navigator', { + userAgent: 'Mozilla/5.0 OpenHarmony 5.0', + platform: 'OpenHarmony', + }); + + expect(isOpenHarmonyRuntime()).toBe(true); + }); }); diff --git a/src/web-ui/src/infrastructure/runtime/environment.ts b/src/web-ui/src/infrastructure/runtime/environment.ts index 886c26ce27..10022c025a 100644 --- a/src/web-ui/src/infrastructure/runtime/environment.ts +++ b/src/web-ui/src/infrastructure/runtime/environment.ts @@ -23,6 +23,17 @@ export const supportsNativeWindowControls = (): boolean => { return isTauriRuntime() && typeof currentWindow?.label === 'string'; }; +/** + * OpenHarmony renders the window chrome in its native ArkUI host. The web + * view must reserve that space but must not render a second set of buttons or + * call the desktop/Tauri window APIs for the same action. + */ +export const isOpenHarmonyRuntime = (): boolean => { + if (typeof navigator === 'undefined') return false; + const fingerprint = `${navigator.userAgent ?? ''} ${navigator.platform ?? ''}`; + return /(?:openharmony|harmonyos|ohos)/i.test(fingerprint); +}; + export const supportsNativeWindowDragging = supportsNativeWindowControls; export const isMacOSDesktopRuntime = (): boolean =>