Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -425,7 +424,6 @@ const PersistentFooterActions: React.FC = () => {
</div>

<div className="bitfun-nav-panel__footer-right">
<GithubStarButton />
<NotificationButton className="bitfun-nav-panel__footer-btn" navFooterHoverIconSwap />
</div>
</div>
Expand Down
12 changes: 8 additions & 4 deletions src/web-ui/src/app/components/SceneBar/SceneBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface SceneBarProps {
onMaximize?: () => void;
onClose?: () => void;
isMaximized?: boolean;
reserveNativeWindowControls?: boolean;
}

const SceneBar: React.FC<SceneBarProps> = ({
Expand All @@ -35,12 +36,14 @@ const SceneBar: React.FC<SceneBarProps> = ({
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);
Expand Down Expand Up @@ -108,13 +111,14 @@ const SceneBar: React.FC<SceneBarProps> = ({
})}
</div>

{hasWindowControls && (
{showWindowControls && (
<div className="bitfun-scene-bar__controls" data-bf-component="scene-bar" data-bf-part="controls">
<WindowControls
onMinimize={onMinimize!}
onMaximize={onMaximize!}
onClose={onClose!}
onMinimize={onMinimize}
onMaximize={onMaximize}
onClose={onClose}
isMaximized={isMaximized}
nativePlaceholder={reserveNativeWindowControls && !hasWindowControls}
/>
</div>
)}
Expand Down
10 changes: 6 additions & 4 deletions src/web-ui/src/app/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -83,6 +83,7 @@ const AppLayout: React.FC<AppLayoutProps> = ({ className = '' }) => {
const { t: tCommon } = useI18n('common');
const currentAppearance = useAppearance().current;
const backgroundMedia = currentAppearance?.backgroundMedia;
const isOpenHarmony = isOpenHarmonyRuntime();
usePermissionRequestNotify();
const {
currentWorkspace,
Expand Down Expand Up @@ -757,10 +758,11 @@ const AppLayout: React.FC<AppLayoutProps> = ({ className = '' }) => {
{/* Main content — always render WorkspaceBody; WelcomeScene in viewport handles no-workspace state */}
<main className="bitfun-app-main-workspace" data-testid="app-main-content" data-bf-component="app-layout" data-bf-part="main">
<WorkspaceBody
onMinimize={canUseNativeWindowControls && !isMacOS ? handleMinimize : undefined}
onMaximize={canUseNativeWindowControls ? handleMaximize : undefined}
onClose={canUseNativeWindowControls && !isMacOS ? handleClose : undefined}
onMinimize={canUseNativeWindowControls && !isMacOS && !isOpenHarmony ? handleMinimize : undefined}
onMaximize={canUseNativeWindowControls && !isOpenHarmony ? handleMaximize : undefined}
onClose={canUseNativeWindowControls && !isMacOS && !isOpenHarmony ? handleClose : undefined}
isMaximized={isMaximized}
reserveNativeWindowControls={isOpenHarmony}
isEntering={transitionDir === 'entering'}
isExiting={transitionDir === 'returning'}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/web-ui/src/app/layout/WorkspaceBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ interface WorkspaceBodyProps {
onMaximize?: () => void;
onClose?: () => void;
isMaximized?: boolean;
reserveNativeWindowControls?: boolean;
sceneOverlay?: React.ReactNode;
}

Expand All @@ -43,6 +44,7 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
onMaximize,
onClose,
isMaximized = false,
reserveNativeWindowControls = false,
sceneOverlay,
}) => {
const { workspace: currentWorkspace } = useCurrentWorkspace();
Expand Down Expand Up @@ -177,6 +179,7 @@ const WorkspaceBody: React.FC<WorkspaceBodyProps> = ({
onMaximize={onMaximize}
onClose={onClose}
isMaximized={isMaximized}
reserveNativeWindowControls={reserveNativeWindowControls}
/>
<SceneViewport
workspacePath={currentWorkspace?.rootPath}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface WindowControlsProps extends React.HTMLAttributes<HTMLDivElement
'data-testid-minimize'?: string;
'data-testid-maximize'?: string;
'data-testid-close'?: string;
/** Render only the native-titlebar spacer used by the OHOS host. */
nativePlaceholder?: boolean;
}

/**
Expand All @@ -46,9 +48,15 @@ export const WindowControls: React.FC<WindowControlsProps> = ({
'data-testid-minimize': testIdMinimize,
'data-testid-maximize': testIdMaximize,
'data-testid-close': testIdClose,
nativePlaceholder = false,
...props
}) => {
const { t } = useTranslation('common');

if (nativePlaceholder) {
return <div className={`window-controls ${className}`} style={{ width: 128 }} {...props} />;
}

const defaultMinimizeIcon = (
<svg width="10" height="10" viewBox="0 0 14 14" fill="none">
<line x1="3" y1="7" x2="11" y2="7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
Expand Down
11 changes: 10 additions & 1 deletion src/web-ui/src/infrastructure/runtime/environment.test.ts
Original file line number Diff line number Diff line change
@@ -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', {
Expand Down Expand Up @@ -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);
});
});
11 changes: 11 additions & 0 deletions src/web-ui/src/infrastructure/runtime/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Loading