feat: add Linux system title bar setting option (#12040)

* feat: add Linux system title bar setting option

Add a setting in Display Settings that allows Linux users to switch
between custom title bar (frameless window with WindowControls) and
native system title bar. The setting requires app restart to take effect
since Electron's frame option cannot be changed at runtime.

- Add useSystemTitleBar config key and getter/setter in ConfigManager
- Add IPC channel and handler for setting persistence
- Add Redux state and action for useSystemTitleBar
- Add UI toggle in DisplaySettings (Linux only)
- Hide WindowControls when system title bar is enabled on Linux
- Modify WindowService to check setting when creating window on Linux
- Add translations for all supported locales

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

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

* fix: improve window controls positioning in left navbar layout

Move WindowControls inside NavbarContainer as a flex item instead of
using fixed positioning. This ensures the window controls are properly
positioned adjacent to the navbar content when using the left navbar
layout.

- Remove position: fixed from WindowControlsContainer styling
- Include WindowControls as last child of NavbarContainer
- Remove extra padding-right from NavbarRight since controls are now
  in the flex layout

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

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

* fix: remove padding-right from TabsBar to align window controls to edge

Remove the 8px padding-right for Windows/Linux in TabsBar that was
causing window controls to not be flush against the window edge.

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

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

* fix: add right padding for settings button when Linux uses system title bar

* fix: restore NavbarRight paddingRight to 15px for consistent spacing

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
beyondkmp 2026-01-23 13:52:09 +08:00 committed by GitHub
parent 2cf38fcc41
commit 858a5ceaec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 150 additions and 16 deletions

View File

@ -48,6 +48,7 @@ export enum IpcChannel {
App_QuoteToMain = 'app:quote-to-main',
App_SetDisableHardwareAcceleration = 'app:set-disable-hardware-acceleration',
App_SetUseSystemTitleBar = 'app:set-use-system-title-bar',
Notification_Send = 'notification:send',
Notification_OnClick = 'notification:on-click',

View File

@ -900,6 +900,9 @@ export async function registerIpc(mainWindow: BrowserWindow, app: Electron.App)
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
configManager.setDisableHardwareAcceleration(isDisable)
})
ipcMain.handle(IpcChannel.App_SetUseSystemTitleBar, (_, isActive: boolean) => {
configManager.setUseSystemTitleBar(isActive)
})
ipcMain.handle(IpcChannel.TRACE_SAVE_DATA, (_, topicId: string) => saveSpans(topicId))
ipcMain.handle(IpcChannel.TRACE_GET_DATA, (_, topicId: string, traceId: string, modelName?: string) =>
getSpans(topicId, traceId, modelName)

View File

@ -45,6 +45,7 @@ export enum ConfigKeys {
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList',
DisableHardwareAcceleration = 'disableHardwareAcceleration',
UseSystemTitleBar = 'useSystemTitleBar',
Proxy = 'proxy',
EnableDeveloperMode = 'enableDeveloperMode',
ClientId = 'clientId',
@ -251,6 +252,14 @@ export class ConfigManager {
this.set(ConfigKeys.DisableHardwareAcceleration, value)
}
getUseSystemTitleBar(): boolean {
return this.get<boolean>(ConfigKeys.UseSystemTitleBar, false)
}
setUseSystemTitleBar(value: boolean) {
this.set(ConfigKeys.UseSystemTitleBar, value)
}
setAndNotify(key: string, value: unknown) {
this.set(key, value, true)
}

View File

@ -75,7 +75,8 @@ export class WindowService {
trafficLightPosition: { x: 8, y: 13 }
}
: {
frame: false // Frameless window for Windows and Linux
// On Linux, allow using system title bar if setting is enabled
frame: isLinux && configManager.getUseSystemTitleBar() ? true : false
}),
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
darkTheme: nativeTheme.shouldUseDarkColors,

View File

@ -503,6 +503,7 @@ const api = {
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
setDisableHardwareAcceleration: (isDisable: boolean) =>
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
setUseSystemTitleBar: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetUseSystemTitleBar, isActive),
trace: {
saveData: (topicId: string) => ipcRenderer.invoke(IpcChannel.TRACE_SAVE_DATA, topicId),
getData: (topicId: string, traceId: string, modelName?: string) =>

View File

@ -2,12 +2,13 @@ import { PlusOutlined } from '@ant-design/icons'
import { loggerService } from '@logger'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { isMac } from '@renderer/config/constant'
import { isLinux, isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
import tabsService from '@renderer/services/TabsService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
@ -122,6 +123,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
const { settedTheme, toggleTheme } = useTheme()
const { hideMinappPopup, minAppsCache } = useMinappPopup()
const { minapps } = useMinapps()
const { useSystemTitleBar } = useSettings()
const { t } = useTranslation()
const getTabId = (path: string): string => {
@ -268,7 +270,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
<PlusOutlined />
</AddTabButton>
</HorizontalScrollContainer>
<RightButtonsContainer>
<RightButtonsContainer style={{ paddingRight: isLinux && useSystemTitleBar ? '12px' : undefined }}>
<Tooltip
title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)}
mouseEnterDelay={0.8}

View File

@ -1,9 +1,6 @@
import styled from 'styled-components'
export const WindowControlsContainer = styled.div`
position: fixed;
top: 0;
right: 0;
display: flex;
align-items: center;
height: var(--navbar-height);

View File

@ -1,4 +1,5 @@
import { isLinux, isWin } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
import { Tooltip } from 'antd'
import { Minus, Square, X } from 'lucide-react'
import type { SVGProps } from 'react'
@ -49,6 +50,7 @@ const DEFAULT_DELAY = 1
const WindowControls: React.FC = () => {
const [isMaximized, setIsMaximized] = useState(false)
const { t } = useTranslation()
const { useSystemTitleBar } = useSettings()
useEffect(() => {
// Check initial maximized state
@ -67,6 +69,11 @@ const WindowControls: React.FC = () => {
return null
}
// Hide on Linux if using system title bar
if (isLinux && useSystemTitleBar) {
return null
}
const handleMinimize = () => {
window.api.windowControls.minimize()
}

View File

@ -22,12 +22,10 @@ export const Navbar: FC<Props> = ({ children, ...props }) => {
}
return (
<>
<NavbarContainer {...props} style={{ backgroundColor }} $isFullScreen={isFullscreen}>
{children}
</NavbarContainer>
{!isTopNavbar && !minappShow && <WindowControls />}
</>
<NavbarContainer {...props} style={{ backgroundColor }} $isFullScreen={isFullscreen}>
{children}
{!minappShow && <WindowControls />}
</NavbarContainer>
)
}

View File

@ -34,6 +34,7 @@ import {
setTopicPosition,
setTray as _setTray,
setTrayOnClose,
setUseSystemTitleBar as _setUseSystemTitleBar,
setWindowStyle
} from '@renderer/store/settings'
import type { SidebarIcon, ThemeMode, TranslateLanguageCode } from '@renderer/types'
@ -117,6 +118,10 @@ export function useSettings() {
setDisableHardwareAcceleration(disableHardwareAcceleration: boolean) {
dispatch(setDisableHardwareAcceleration(disableHardwareAcceleration))
window.api.setDisableHardwareAcceleration(disableHardwareAcceleration)
},
setUseSystemTitleBar(useSystemTitleBar: boolean) {
dispatch(_setUseSystemTitleBar(useSystemTitleBar))
window.api.setUseSystemTitleBar(useSystemTitleBar)
}
}
}

View File

@ -4944,6 +4944,13 @@
"show": "Show Tray Icon",
"title": "Tray"
},
"use_system_title_bar": {
"confirm": {
"content": "Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "Restart Required"
},
"title": "Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Reset",
"title": "Page Zoom"

View File

@ -4944,6 +4944,13 @@
"show": "显示托盘图标",
"title": "托盘"
},
"use_system_title_bar": {
"confirm": {
"content": "更改标题栏样式需要重启应用才能生效,是否现在重启?",
"title": "需要重启应用"
},
"title": "使用系统标题栏 (Linux)"
},
"zoom": {
"reset": "重置",
"title": "缩放"

View File

@ -4944,6 +4944,13 @@
"show": "顯示系統匣圖示",
"title": "系統匣"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "重設",
"title": "縮放"

View File

@ -4944,6 +4944,13 @@
"show": "Tray-Symbol anzeigen",
"title": "Tray"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Zurücksetzen",
"title": "Zoom"

View File

@ -4944,6 +4944,13 @@
"show": "Εμφάνιση εικονιδίου συνδρομής",
"title": "Συνδρομή"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Επαναφορά",
"title": "Κλίμακα"

View File

@ -4944,6 +4944,13 @@
"show": "Mostrar bandera del sistema",
"title": "Bandera"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Restablecer",
"title": "Escala"

View File

@ -4944,6 +4944,13 @@
"show": "Afficher l'icône dans la barre d'état système",
"title": "Barre d'état système"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Réinitialiser",
"title": "Zoom"

View File

@ -4944,6 +4944,13 @@
"show": "トレイアイコンを表示",
"title": "トレイ"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "リセット",
"title": "ページズーム"

View File

@ -4944,6 +4944,13 @@
"show": "Mostrar ícone de bandeja",
"title": "Tray"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Redefinir",
"title": "Escala"

View File

@ -4944,6 +4944,13 @@
"show": "Показать значок в трее",
"title": "Трей"
},
"use_system_title_bar": {
"confirm": {
"content": "[to be translated]:Changing the title bar style requires restarting the app to take effect. Do you want to restart now?",
"title": "[to be translated]:Restart Required"
},
"title": "[to be translated]:Use System Title Bar (Linux)"
},
"zoom": {
"reset": "Сбросить",
"title": "Масштаб страницы"

View File

@ -1,7 +1,6 @@
import { Navbar, NavbarCenter, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup'
import { isLinux, isWin } from '@renderer/config/constant'
import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts'
@ -123,7 +122,7 @@ const HeaderNavbar: FC<Props> = ({
justifyContent: 'flex-end',
flex: activeTopicOrSession === 'topic' ? 1 : 'none',
position: 'relative',
paddingRight: isWin || isLinux ? '144px' : '15px',
paddingRight: '15px',
minWidth: activeTopicOrSession === 'topic' ? '' : 'auto'
}}
className="home-navbar-right">

View File

@ -2,10 +2,11 @@ import CodeEditor from '@renderer/components/CodeEditor'
import { ResetIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout'
import TextBadge from '@renderer/components/TextBadge'
import { isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
import { isLinux, isMac, THEME_COLOR_PRESETS } from '@renderer/config/constant'
import { DEFAULT_SIDEBAR_ICONS } from '@renderer/config/sidebar'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer'
import useUserTheme from '@renderer/hooks/useUserTheme'
import { useAppDispatch } from '@renderer/store'
import type { AssistantIconType } from '@renderer/store/settings'
@ -68,12 +69,15 @@ const DisplaySettings: FC = () => {
sidebarIcons,
setTheme,
assistantIconType,
userTheme
userTheme,
useSystemTitleBar,
setUseSystemTitleBar
} = useSettings()
const { navbarPosition, setNavbarPosition } = useNavbarPosition()
const { theme, settedTheme } = useTheme()
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { setTimeoutTimer } = useTimer()
const [currentZoom, setCurrentZoom] = useState(1.0)
const { setUserTheme } = useUserTheme()
@ -88,6 +92,26 @@ const DisplaySettings: FC = () => {
[setWindowStyle]
)
const handleUseSystemTitleBarChange = (checked: boolean) => {
window.modal.confirm({
title: t('settings.use_system_title_bar.confirm.title'),
content: t('settings.use_system_title_bar.confirm.content'),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
centered: true,
onOk() {
setUseSystemTitleBar(checked)
setTimeoutTimer(
'handleUseSystemTitleBarChange',
() => {
window.api.relaunchApp()
},
500
)
}
})
}
const handleColorPrimaryChange = useCallback(
(colorHex: string) => {
setUserTheme({
@ -260,6 +284,15 @@ const DisplaySettings: FC = () => {
</SettingRow>
</>
)}
{isLinux && (
<>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.use_system_title_bar.title')}</SettingRowTitle>
<Switch checked={useSystemTitleBar} onChange={handleUseSystemTitleBarChange} />
</SettingRow>
</>
)}
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle style={{ justifyContent: 'flex-start', gap: 5 }}>

View File

@ -198,6 +198,8 @@ export interface SettingsState {
enableQuickPanelTriggers: boolean
// 硬件加速设置
disableHardwareAcceleration: boolean
// 使用系统标题栏 (仅Linux)
useSystemTitleBar: boolean
exportMenuOptions: {
image: boolean
markdown: boolean
@ -383,6 +385,8 @@ export const initialState: SettingsState = {
confirmRegenerateMessage: true,
// 硬件加速设置
disableHardwareAcceleration: false,
// 使用系统标题栏 (仅Linux)
useSystemTitleBar: false,
exportMenuOptions: {
image: true,
markdown: true,
@ -818,6 +822,9 @@ const settingsSlice = createSlice({
setDisableHardwareAcceleration: (state, action: PayloadAction<boolean>) => {
state.disableHardwareAcceleration = action.payload
},
setUseSystemTitleBar: (state, action: PayloadAction<boolean>) => {
state.useSystemTitleBar = action.payload
},
setOpenAISummaryText: (state, action: PayloadAction<OpenAIReasoningSummary>) => {
state.openAI.summaryText = action.payload
},
@ -998,6 +1005,7 @@ export const {
setConfirmDeleteMessage,
setConfirmRegenerateMessage,
setDisableHardwareAcceleration,
setUseSystemTitleBar,
setOpenAISummaryText,
setOpenAIVerbosity,
setOpenAIStreamOptionsIncludeUsage,