feat: support custom minimize, maximize and close (#9847)

* feat: add window control functionality for Windows and Linux

- Introduced new IPC channels for window management: minimize, maximize, unmaximize, close, and check maximized state.
- Implemented window control buttons in the UI, allowing users to minimize, maximize, and close the application.
- Enhanced Navbar and TabContainer components to include window controls, improving user experience on non-Mac platforms.
- Styled window control buttons for better visual integration.

This update enhances the application's usability by providing essential window management features.

* add tooltip

* fix macos

* lint error

* update i18n

* lint

* fix: add WindowControls to MinApp popup and improve hover styles

- Add WindowControls component to MinappPopupContainer title bar for Windows/Linux
- Fix ButtonsGroup overlap with WindowControls by adding proper margin
- Improve WindowControls hover background visibility by using rgba(128,128,128,0.3)
- Ensure WindowControls is positioned at the right edge of title bar

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* lint

* add types

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
beyondkmp 2025-09-05 12:30:01 +08:00 committed by GitHub
parent 19846c7e01
commit b1a39e9b38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 326 additions and 30 deletions

View File

@ -123,6 +123,12 @@ export enum IpcChannel {
Windows_SetMinimumSize = 'window:set-minimum-size', Windows_SetMinimumSize = 'window:set-minimum-size',
Windows_Resize = 'window:resize', Windows_Resize = 'window:resize',
Windows_GetSize = 'window:get-size', Windows_GetSize = 'window:get-size',
Windows_Minimize = 'window:minimize',
Windows_Maximize = 'window:maximize',
Windows_Unmaximize = 'window:unmaximize',
Windows_Close = 'window:close',
Windows_IsMaximized = 'window:is-maximized',
Windows_MaximizedChanged = 'window:maximized-changed',
KnowledgeBase_Create = 'knowledge-base:create', KnowledgeBase_Create = 'knowledge-base:create',
KnowledgeBase_Reset = 'knowledge-base:reset', KnowledgeBase_Reset = 'knowledge-base:reset',

View File

@ -587,6 +587,41 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
return [width, height] return [width, height]
}) })
// Window Controls
ipcMain.handle(IpcChannel.Windows_Minimize, () => {
checkMainWindow()
mainWindow.minimize()
})
ipcMain.handle(IpcChannel.Windows_Maximize, () => {
checkMainWindow()
mainWindow.maximize()
})
ipcMain.handle(IpcChannel.Windows_Unmaximize, () => {
checkMainWindow()
mainWindow.unmaximize()
})
ipcMain.handle(IpcChannel.Windows_Close, () => {
checkMainWindow()
mainWindow.close()
})
ipcMain.handle(IpcChannel.Windows_IsMaximized, () => {
checkMainWindow()
return mainWindow.isMaximized()
})
// Send maximized state changes to renderer
mainWindow.on('maximize', () => {
mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, true)
})
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send(IpcChannel.Windows_MaximizedChanged, false)
})
// VertexAI // VertexAI
ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => { ipcMain.handle(IpcChannel.VertexAI_GetAuthHeaders, async (_, params) => {
return vertexAIService.getAuthHeaders(params) return vertexAIService.getAuthHeaders(params)

View File

@ -66,11 +66,19 @@ export class WindowService {
transparent: false, transparent: false,
vibrancy: 'sidebar', vibrancy: 'sidebar',
visualEffectState: 'active', visualEffectState: 'active',
titleBarStyle: 'hidden', // For Windows and Linux, we use frameless window with custom controls
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight, // For Mac, we keep the native title bar style
...(isMac
? {
titleBarStyle: 'hidden',
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
trafficLightPosition: { x: 8, y: 13 }
}
: {
frame: false // Frameless window for Windows and Linux
}),
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF', backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors, darkTheme: nativeTheme.shouldUseDarkColors,
trafficLightPosition: { x: 8, y: 13 },
...(isLinux ? { icon } : {}), ...(isLinux ? { icon } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '../preload/index.js'), preload: join(__dirname, '../preload/index.js'),

View File

@ -438,6 +438,20 @@ const api = {
cherryin: { cherryin: {
generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) => generateSignature: (params: { method: string; path: string; query: string; body: Record<string, any> }) =>
ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params) ipcRenderer.invoke(IpcChannel.Cherryin_GetSignature, params)
},
windowControls: {
minimize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Minimize),
maximize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Maximize),
unmaximize: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Unmaximize),
close: (): Promise<void> => ipcRenderer.invoke(IpcChannel.Windows_Close),
isMaximized: (): Promise<boolean> => ipcRenderer.invoke(IpcChannel.Windows_IsMaximized),
onMaximizedChange: (callback: (isMaximized: boolean) => void): (() => void) => {
const channel = IpcChannel.Windows_MaximizedChanged
ipcRenderer.on(channel, (_, isMaximized: boolean) => callback(isMaximized))
return () => {
ipcRenderer.removeAllListeners(channel)
}
}
} }
} }

View File

@ -11,6 +11,7 @@ import {
ReloadOutlined ReloadOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import WindowControls from '@renderer/components/WindowControls'
import { isLinux, isMac, isWin } from '@renderer/config/constant' import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useBridge } from '@renderer/hooks/useBridge' import { useBridge } from '@renderer/hooks/useBridge'
@ -434,7 +435,10 @@ const MinappPopupContainer: React.FC = () => {
</Tooltip> </Tooltip>
)} )}
<Spacer /> <Spacer />
<ButtonsGroup className={isWin || isLinux ? 'windows' : ''} isTopNavbar={isTopNavbar}> <ButtonsGroup
className={isWin || isLinux ? 'windows' : ''}
style={{ marginRight: isWin || isLinux ? '140px' : 0 }}
isTopNavbar={isTopNavbar}>
<Tooltip title={t('minapp.popup.goBack')} mouseEnterDelay={0.8} placement="bottom"> <Tooltip title={t('minapp.popup.goBack')} mouseEnterDelay={0.8} placement="bottom">
<TitleButton onClick={() => handleGoBack(appInfo.id)}> <TitleButton onClick={() => handleGoBack(appInfo.id)}>
<ArrowLeftOutlined /> <ArrowLeftOutlined />
@ -500,6 +504,11 @@ const MinappPopupContainer: React.FC = () => {
</TitleButton> </TitleButton>
</Tooltip> </Tooltip>
</ButtonsGroup> </ButtonsGroup>
{(isWin || isLinux) && (
<div style={{ position: 'absolute', right: 0, top: 0, height: '100%' }}>
<WindowControls />
</div>
)}
</TitleContainer> </TitleContainer>
) )
} }
@ -602,7 +611,6 @@ const ButtonsGroup = styled.div<{ isTopNavbar: boolean }>`
gap: 5px; gap: 5px;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
&.windows { &.windows {
margin-right: ${isWin ? '130px' : isLinux ? '100px' : 0};
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
border-radius: 50px; border-radius: 50px;
padding: 0 3px; padding: 0 3px;

View File

@ -1,7 +1,7 @@
import { PlusOutlined } from '@ant-design/icons' import { PlusOutlined } from '@ant-design/icons'
import { Sortable, useDndReorder } from '@renderer/components/dnd' import { Sortable, useDndReorder } from '@renderer/components/dnd'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { isLinux, isMac, isWin } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
@ -39,6 +39,7 @@ import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
import MinAppIcon from '../Icons/MinAppIcon' import MinAppIcon from '../Icons/MinAppIcon'
import WindowControls from '../WindowControls'
interface TabsContainerProps { interface TabsContainerProps {
children: React.ReactNode children: React.ReactNode
@ -268,6 +269,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
<SettingsButton onClick={handleSettingsClick} $active={activeTabId === 'settings'}> <SettingsButton onClick={handleSettingsClick} $active={activeTabId === 'settings'}>
<Settings size={16} /> <Settings size={16} />
</SettingsButton> </SettingsButton>
<WindowControls />
</RightButtonsContainer> </RightButtonsContainer>
</TabsBar> </TabsBar>
<TabContent>{children}</TabContent> <TabContent>{children}</TabContent>
@ -288,7 +290,7 @@ const TabsBar = styled.div<{ $isFullscreen: boolean }>`
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '75px' : '15px')}; padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '75px' : '15px')};
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')}; padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : '0')};
height: var(--navbar-height); height: var(--navbar-height);
position: relative; position: relative;
-webkit-app-region: drag; -webkit-app-region: drag;
@ -427,6 +429,7 @@ const RightButtonsContainer = styled.div`
align-items: center; align-items: center;
gap: 6px; gap: 6px;
margin-left: auto; margin-left: auto;
padding-right: ${isMac ? '12px' : '0'};
flex-shrink: 0; flex-shrink: 0;
` `

View File

@ -0,0 +1,42 @@
import styled from 'styled-components'
export const WindowControlsContainer = styled.div`
display: flex;
align-items: center;
height: 100%;
-webkit-app-region: no-drag;
user-select: none;
`
export const ControlButton = styled.button<{ $isClose?: boolean }>`
display: flex;
align-items: center;
justify-content: center;
width: 46px;
height: var(--navbar-height);
border: none;
background: transparent;
color: var(--color-text);
cursor: pointer;
outline: none;
transition:
background 0.15s,
color 0.15s;
padding: 0;
position: relative;
border-radius: 0;
&:hover {
background: ${(props) => (props.$isClose ? '#e81123' : 'rgba(128, 128, 128, 0.3)')};
color: ${(props) => (props.$isClose ? '#ffffff' : 'var(--color-text)')};
}
&:active {
background: ${(props) => (props.$isClose ? '#c50e1f' : 'rgba(128, 128, 128, 0.4)')};
color: ${(props) => (props.$isClose ? '#ffffff' : 'var(--color-text)')};
}
svg {
pointer-events: none;
}
`

View File

@ -0,0 +1,80 @@
import { isLinux, isWin } from '@renderer/config/constant'
import { Tooltip } from 'antd'
import { Minus, Square, X } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { ControlButton, WindowControlsContainer } from './WindowControls.styled'
// Custom restore icon - two overlapping squares like Windows
const RestoreIcon: React.FC<{ size?: number }> = ({ size = 14 }) => (
<svg width={size} height={size} viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1">
{/* Back square (top-right) */}
<path d="M 4 2 H 11 V 9 H 9 V 4 H 4 V 2" />
{/* Front square (bottom-left) */}
<rect x="2" y="4" width="7" height="7" />
</svg>
)
const WindowControls: React.FC = () => {
const [isMaximized, setIsMaximized] = useState(false)
const { t } = useTranslation()
useEffect(() => {
// Check initial maximized state
window.api.windowControls.isMaximized().then(setIsMaximized)
// Listen for maximized state changes
const unsubscribe = window.api.windowControls.onMaximizedChange(setIsMaximized)
return () => {
unsubscribe()
}
}, [])
// Only show on Windows and Linux
if (!isWin && !isLinux) {
return null
}
const handleMinimize = () => {
window.api.windowControls.minimize()
}
const handleMaximize = () => {
if (isMaximized) {
window.api.windowControls.unmaximize()
} else {
window.api.windowControls.maximize()
}
}
const handleClose = () => {
window.api.windowControls.close()
}
return (
<WindowControlsContainer>
<Tooltip title={t('navbar.window.minimize')} placement="bottom" mouseEnterDelay={0.2}>
<ControlButton onClick={handleMinimize} aria-label="Minimize">
<Minus size={14} />
</ControlButton>
</Tooltip>
<Tooltip
title={isMaximized ? t('navbar.window.restore') : t('navbar.window.maximize')}
placement="bottom"
mouseEnterDelay={0.2}>
<ControlButton onClick={handleMaximize} aria-label={isMaximized ? 'Restore' : 'Maximize'}>
{isMaximized ? <RestoreIcon size={14} /> : <Square size={14} />}
</ControlButton>
</Tooltip>
<Tooltip title={t('navbar.window.close')} placement="bottom" mouseEnterDelay={0.2}>
<ControlButton $isClose onClick={handleClose} aria-label="Close">
<X size={17} />
</ControlButton>
</Tooltip>
</WindowControlsContainer>
)
}
export default WindowControls

View File

@ -6,6 +6,8 @@ import type { FC, PropsWithChildren } from 'react'
import type { HTMLAttributes } from 'react' import type { HTMLAttributes } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import WindowControls from '../WindowControls'
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement> type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
@ -28,7 +30,17 @@ export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
} }
export const NavbarCenter: FC<Props> = ({ children, ...props }) => { export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
return <NavbarCenterContainer {...props}>{children}</NavbarCenterContainer> return (
<NavbarCenterContainer {...props}>
{children}
{/* Add WindowControls for Windows and Linux in NavbarCenter */}
{(isWin || isLinux) && (
<div style={{ position: 'absolute', right: 0, top: 0, height: '100%', display: 'flex', alignItems: 'center' }}>
<WindowControls />
</div>
)}
</NavbarCenterContainer>
)
} }
export const NavbarRight: FC<Props> = ({ children, ...props }) => { export const NavbarRight: FC<Props> = ({ children, ...props }) => {
@ -81,6 +93,7 @@ const NavbarCenterContainer = styled.div`
padding: 0 ${isMac ? '20px' : 0}; padding: 0 ${isMac ? '20px' : 0};
font-weight: bold; font-weight: bold;
color: var(--color-text-1); color: var(--color-text-1);
position: relative;
` `
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>` const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Expand Dialog", "expand": "Expand Dialog",
"hide_sidebar": "Hide Sidebar", "hide_sidebar": "Hide Sidebar",
"show_sidebar": "Show Sidebar" "show_sidebar": "Show Sidebar",
"window": {
"close": "Close",
"maximize": "Maximize",
"minimize": "Minimize",
"restore": "Restore"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Go to provider settings" "provider_settings": "Go to provider settings"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "伸缩对话框", "expand": "伸缩对话框",
"hide_sidebar": "隐藏侧边栏", "hide_sidebar": "隐藏侧边栏",
"show_sidebar": "显示侧边栏" "show_sidebar": "显示侧边栏",
"window": {
"close": "关闭",
"maximize": "最大化",
"minimize": "最小化",
"restore": "还原"
}
}, },
"navigate": { "navigate": {
"provider_settings": "跳转到服务商设置界面" "provider_settings": "跳转到服务商设置界面"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "伸縮對話框", "expand": "伸縮對話框",
"hide_sidebar": "隱藏側邊欄", "hide_sidebar": "隱藏側邊欄",
"show_sidebar": "顯示側邊欄" "show_sidebar": "顯示側邊欄",
"window": {
"close": "關閉",
"maximize": "最大化",
"minimize": "最小化",
"restore": "還原"
}
}, },
"navigate": { "navigate": {
"provider_settings": "跳轉到服務商設置界面" "provider_settings": "跳轉到服務商設置界面"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Επισκευή διαλόγου", "expand": "Επισκευή διαλόγου",
"hide_sidebar": "Απόκρυψη πλάγιας μπάρας", "hide_sidebar": "Απόκρυψη πλάγιας μπάρας",
"show_sidebar": "Εμφάνιση πλάγιας μπάρας" "show_sidebar": "Εμφάνιση πλάγιας μπάρας",
"window": {
"close": "Κλείσιμο",
"maximize": "Μεγιστοποίηση",
"minimize": "Ελαχιστοποίηση",
"restore": "Επαναφορά"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου" "provider_settings": "Μετάβαση στις ρυθμίσεις παρόχου"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Expandir cuadro de diálogo", "expand": "Expandir cuadro de diálogo",
"hide_sidebar": "Ocultar barra lateral", "hide_sidebar": "Ocultar barra lateral",
"show_sidebar": "Mostrar barra lateral" "show_sidebar": "Mostrar barra lateral",
"window": {
"close": "Cerrar",
"maximize": "Maximizar",
"minimize": "Minimizar",
"restore": "Restaurar"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Ir a la configuración del proveedor" "provider_settings": "Ir a la configuración del proveedor"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Agrandir la boîte de dialogue", "expand": "Agrandir la boîte de dialogue",
"hide_sidebar": "Cacher la barre latérale", "hide_sidebar": "Cacher la barre latérale",
"show_sidebar": "Afficher la barre latérale" "show_sidebar": "Afficher la barre latérale",
"window": {
"close": "Fermer",
"maximize": "Agrandir",
"minimize": "Réduire",
"restore": "Restaurer"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Aller aux paramètres du fournisseur" "provider_settings": "Aller aux paramètres du fournisseur"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "ダイアログを展開", "expand": "ダイアログを展開",
"hide_sidebar": "サイドバーを非表示", "hide_sidebar": "サイドバーを非表示",
"show_sidebar": "サイドバーを表示" "show_sidebar": "サイドバーを表示",
"window": {
"close": "閉じる",
"maximize": "最大化",
"minimize": "最小化",
"restore": "元に戻す"
}
}, },
"navigate": { "navigate": {
"provider_settings": "プロバイダー設定に移動" "provider_settings": "プロバイダー設定に移動"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Expandir caixa de diálogo", "expand": "Expandir caixa de diálogo",
"hide_sidebar": "Ocultar barra lateral", "hide_sidebar": "Ocultar barra lateral",
"show_sidebar": "Mostrar barra lateral" "show_sidebar": "Mostrar barra lateral",
"window": {
"close": "Fechar",
"maximize": "Maximizar",
"minimize": "Minimizar",
"restore": "Restaurar"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Ir para as configurações do provedor" "provider_settings": "Ir para as configurações do provedor"

View File

@ -1653,7 +1653,13 @@
"navbar": { "navbar": {
"expand": "Развернуть диалоговое окно", "expand": "Развернуть диалоговое окно",
"hide_sidebar": "Скрыть боковую панель", "hide_sidebar": "Скрыть боковую панель",
"show_sidebar": "Показать боковую панель" "show_sidebar": "Показать боковую панель",
"window": {
"close": "Закрыть",
"maximize": "Развернуть",
"minimize": "Свернуть",
"restore": "Восстановить"
}
}, },
"navigate": { "navigate": {
"provider_settings": "Перейти к настройкам поставщика" "provider_settings": "Перейти к настройкам поставщика"

View File

@ -1,7 +1,8 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isMac } from '@renderer/config/constant' import WindowControls from '@renderer/components/WindowControls'
import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime' import { modelGenerating } from '@renderer/hooks/useRuntime'
@ -87,7 +88,9 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>
<NavbarRight style={{ justifyContent: 'space-between', flex: 1 }} className="home-navbar-right"> <NavbarRight
style={{ justifyContent: 'space-between', flex: 1, position: 'relative' }}
className="home-navbar-right">
<HStack alignItems="center"> <HStack alignItems="center">
{!showAssistants && ( {!showAssistants && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}> <Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={0.8}>
@ -114,18 +117,8 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</AnimatePresence> </AnimatePresence>
<SelectModelButton assistant={assistant} /> <SelectModelButton assistant={assistant} />
</HStack> </HStack>
<HStack alignItems="center" gap={8}> <HStack alignItems="center" gap={6}>
<UpdateAppButton /> <UpdateAppButton />
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
{topicPosition === 'right' && !showTopics && ( {topicPosition === 'right' && !showTopics && (
<Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}> <Tooltip title={t('navbar.show_sidebar')} mouseEnterDelay={2}>
<NavbarIcon onClick={toggleShowTopics}> <NavbarIcon onClick={toggleShowTopics}>
@ -140,7 +133,47 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
</NavbarIcon> </NavbarIcon>
</Tooltip> </Tooltip>
)} )}
{/* For Mac, show search and expand without WindowControls */}
{!isWin && !isLinux && (
<>
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NarrowIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NarrowIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NarrowIcon>
</Tooltip>
</>
)}
</HStack> </HStack>
{/* Search, Expand and WindowControls positioned at the right edge */}
{(isWin || isLinux) && (
<div
style={{
position: 'absolute',
right: 0,
top: 0,
height: '100%',
display: 'flex',
alignItems: 'center',
gap: 6
}}>
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={() => SearchPopup.show()}>
<Search size={18} />
</NavbarIcon>
</Tooltip>
<Tooltip title={t('navbar.expand')} mouseEnterDelay={0.8}>
<NavbarIcon onClick={handleNarrowModeToggle}>
<i className="iconfont icon-icon-adaptive-width"></i>
</NavbarIcon>
</Tooltip>
<WindowControls />
</div>
)}
</NavbarRight> </NavbarRight>
</Navbar> </Navbar>
) )