diff --git a/src/main/index.ts b/src/main/index.ts index 6252481a..81a33cfd 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -424,8 +424,8 @@ function syncTrafficLightPosition(win: BrowserWindow): void { */ function createWindow(): void { const isMac = process.platform === 'darwin'; - const isLinux = process.platform === 'linux'; const iconPath = isMac ? undefined : getWindowIconPath(); + const useNativeTitleBar = !isMac && configManager.getConfig().general.useNativeTitleBar; mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT, @@ -436,7 +436,7 @@ function createWindow(): void { contextIsolation: true, }, backgroundColor: '#1a1a1a', - ...(isLinux ? {} : { titleBarStyle: 'hidden' as const }), + ...(useNativeTitleBar ? {} : { titleBarStyle: 'hidden' as const }), ...(isMac && { trafficLightPosition: getTrafficLightPositionForZoom(1) }), title: 'claude-devtools', }); @@ -485,6 +485,7 @@ function createWindow(): void { // Prevent Electron's default Ctrl+R / Cmd+R page reload so the renderer // keyboard handler can use it as "Refresh Session" (fixes #58). + // Also prevent Ctrl+Shift+R / Cmd+Shift+R (hard reload). if ((input.control || input.meta) && input.key.toLowerCase() === 'r') { event.preventDefault(); return; diff --git a/src/main/ipc/configValidation.ts b/src/main/ipc/configValidation.ts index b860bd15..aa71e865 100644 --- a/src/main/ipc/configValidation.ts +++ b/src/main/ipc/configValidation.ts @@ -204,6 +204,7 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V 'defaultTab', 'claudeRootPath', 'autoExpandAIGroups', + 'useNativeTitleBar', ]; const result: Partial = {}; @@ -274,6 +275,12 @@ function validateGeneralSection(data: unknown): ValidationSuccess<'general'> | V } result.autoExpandAIGroups = value; break; + case 'useNativeTitleBar': + if (typeof value !== 'boolean') { + return { valid: false, error: `general.${key} must be a boolean` }; + } + result.useNativeTitleBar = value; + break; default: return { valid: false, error: `Unsupported general key: ${key}` }; } diff --git a/src/main/ipc/window.ts b/src/main/ipc/window.ts index d2cb1edc..06547659 100644 --- a/src/main/ipc/window.ts +++ b/src/main/ipc/window.ts @@ -5,7 +5,7 @@ */ import { createLogger } from '@shared/utils/logger'; -import { BrowserWindow, type IpcMain } from 'electron'; +import { app, BrowserWindow, type IpcMain } from 'electron'; const logger = createLogger('IPC:window'); @@ -40,6 +40,11 @@ export function registerWindowHandlers(ipcMain: IpcMain): void { return win != null && !win.isDestroyed() && win.isMaximized(); }); + ipcMain.handle('app:relaunch', () => { + app.relaunch(); + app.exit(0); + }); + logger.info('Window handlers registered'); } @@ -48,5 +53,6 @@ export function removeWindowHandlers(ipcMain: IpcMain): void { ipcMain.removeHandler('window:maximize'); ipcMain.removeHandler('window:close'); ipcMain.removeHandler('window:isMaximized'); + ipcMain.removeHandler('app:relaunch'); logger.info('Window handlers removed'); } diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts index 7f3ea4a3..6d4ff8dc 100644 --- a/src/main/services/infrastructure/ConfigManager.ts +++ b/src/main/services/infrastructure/ConfigManager.ts @@ -182,6 +182,7 @@ export interface GeneralConfig { defaultTab: 'dashboard' | 'last-session'; claudeRootPath: string | null; autoExpandAIGroups: boolean; + useNativeTitleBar: boolean; } export interface DisplayConfig { @@ -250,6 +251,7 @@ const DEFAULT_CONFIG: AppConfig = { defaultTab: 'dashboard', claudeRootPath: null, autoExpandAIGroups: false, + useNativeTitleBar: false, }, display: { showTimestamps: true, diff --git a/src/preload/constants/ipcChannels.ts b/src/preload/constants/ipcChannels.ts index eec0bc05..408e9041 100644 --- a/src/preload/constants/ipcChannels.ts +++ b/src/preload/constants/ipcChannels.ts @@ -171,3 +171,6 @@ export const WINDOW_CLOSE = 'window:close'; /** Whether the window is currently maximized */ export const WINDOW_IS_MAXIMIZED = 'window:isMaximized'; + +/** Relaunch the application */ +export const APP_RELAUNCH = 'app:relaunch'; diff --git a/src/preload/index.ts b/src/preload/index.ts index 09f16c5c..e9c32ae6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -2,6 +2,7 @@ import { WINDOW_ZOOM_FACTOR_CHANGED_CHANNEL } from '@shared/constants'; import { contextBridge, ipcRenderer } from 'electron'; import { + APP_RELAUNCH, CONTEXT_CHANGED, CONTEXT_GET_ACTIVE, CONTEXT_LIST, @@ -357,6 +358,7 @@ const electronAPI: ElectronAPI = { maximize: () => ipcRenderer.invoke(WINDOW_MAXIMIZE), close: () => ipcRenderer.invoke(WINDOW_CLOSE), isMaximized: () => ipcRenderer.invoke(WINDOW_IS_MAXIMIZED) as Promise, + relaunch: () => ipcRenderer.invoke(APP_RELAUNCH), }, onTodoChange: (callback: (event: IpcFileChangePayload) => void): (() => void) => { diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 0b2d851f..96868677 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -512,6 +512,7 @@ export class HttpAPIClient implements ElectronAPI { maximize: async (): Promise => {}, close: async (): Promise => {}, isMaximized: async (): Promise => false, + relaunch: async (): Promise => {}, }; // --------------------------------------------------------------------------- diff --git a/src/renderer/components/layout/WindowsTitleBar.tsx b/src/renderer/components/layout/CustomTitleBar.tsx similarity index 74% rename from src/renderer/components/layout/WindowsTitleBar.tsx rename to src/renderer/components/layout/CustomTitleBar.tsx index bde601c0..04895818 100644 --- a/src/renderer/components/layout/WindowsTitleBar.tsx +++ b/src/renderer/components/layout/CustomTitleBar.tsx @@ -1,32 +1,36 @@ /** - * WindowsTitleBar - Conventional title bar for Windows when the native frame is hidden. + * CustomTitleBar - Conventional title bar for Windows and Linux when the native frame is hidden. * * Renders a draggable top strip with window controls (minimize, maximize/restore, close) - * on the right, matching Windows conventions. Only shown in Electron on Windows (win32). + * on the right. Only shown in Electron on Windows or Linux (macOS uses native traffic lights). */ import { useEffect, useState } from 'react'; import { isElectronMode } from '@renderer/api'; +import faviconUrl from '@renderer/favicon.png'; +import { useStore } from '@renderer/store'; import { Minus, Square, X } from 'lucide-react'; const TITLE_BAR_HEIGHT = 32; -function isWindowsDesktop(): boolean { +function needsCustomTitleBar(): boolean { if (!isElectronMode()) return false; - return window.navigator.userAgent.includes('Windows'); + const ua = window.navigator.userAgent; + return ua.includes('Windows') || ua.includes('Linux'); } -export const WindowsTitleBar = (): React.JSX.Element | null => { +export const CustomTitleBar = (): React.JSX.Element | null => { const [isMaximized, setIsMaximized] = useState(false); - const isWin = isWindowsDesktop(); + const useNativeTitleBar = useStore((s) => s.appConfig?.general?.useNativeTitleBar ?? false); + const showTitleBar = needsCustomTitleBar() && !useNativeTitleBar; const api = typeof window !== 'undefined' ? window.electronAPI?.windowControls : null; useEffect(() => { if (api) void api.isMaximized().then(setIsMaximized); }, [api]); - if (!isWin || !api) return null; + if (!showTitleBar || !api) return null; const { minimize, maximize, close, isMaximized: getIsMaximized } = api; @@ -49,14 +53,9 @@ export const WindowsTitleBar = (): React.JSX.Element | null => { return (
- {/* Draggable area — app title optional */} -
- - claude-devtools - + {/* Draggable area — app icon */} +
+
{/* Window controls — no-drag so they receive clicks */} diff --git a/src/renderer/components/layout/TabbedLayout.tsx b/src/renderer/components/layout/TabbedLayout.tsx index 7abcca82..e466d6b5 100644 --- a/src/renderer/components/layout/TabbedLayout.tsx +++ b/src/renderer/components/layout/TabbedLayout.tsx @@ -16,9 +16,9 @@ import { UpdateDialog } from '../common/UpdateDialog'; import { WorkspaceIndicator } from '../common/WorkspaceIndicator'; import { CommandPalette } from '../search/CommandPalette'; +import { CustomTitleBar } from './CustomTitleBar'; import { PaneContainer } from './PaneContainer'; import { Sidebar } from './Sidebar'; -import { WindowsTitleBar } from './WindowsTitleBar'; export const TabbedLayout = (): React.JSX.Element => { // Enable keyboard shortcuts @@ -33,7 +33,7 @@ export const TabbedLayout = (): React.JSX.Element => { { '--macos-traffic-light-padding-left': `${trafficLightPadding}px` } as React.CSSProperties } > - +
{/* Command Palette (Cmd+K) */} diff --git a/src/renderer/components/settings/hooks/useSettingsConfig.ts b/src/renderer/components/settings/hooks/useSettingsConfig.ts index 8f4fbdc4..192346ae 100644 --- a/src/renderer/components/settings/hooks/useSettingsConfig.ts +++ b/src/renderer/components/settings/hooks/useSettingsConfig.ts @@ -31,6 +31,7 @@ export interface SafeConfig { defaultTab: 'dashboard' | 'last-session'; claudeRootPath: string | null; autoExpandAIGroups: boolean; + useNativeTitleBar: boolean; }; notifications: { enabled: boolean; @@ -156,6 +157,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn { defaultTab: displayConfig?.general?.defaultTab ?? 'dashboard', claudeRootPath: displayConfig?.general?.claudeRootPath ?? null, autoExpandAIGroups: displayConfig?.general?.autoExpandAIGroups ?? false, + useNativeTitleBar: displayConfig?.general?.useNativeTitleBar ?? false, }, notifications: { enabled: displayConfig?.notifications?.enabled ?? true, diff --git a/src/renderer/components/settings/hooks/useSettingsHandlers.ts b/src/renderer/components/settings/hooks/useSettingsHandlers.ts index 40a9997f..617e2c45 100644 --- a/src/renderer/components/settings/hooks/useSettingsHandlers.ts +++ b/src/renderer/components/settings/hooks/useSettingsHandlers.ts @@ -288,6 +288,7 @@ export function useSettingsHandlers({ defaultTab: 'dashboard', claudeRootPath: null, autoExpandAIGroups: false, + useNativeTitleBar: false, }, display: { showTimestamps: true, diff --git a/src/renderer/components/settings/sections/GeneralSection.tsx b/src/renderer/components/settings/sections/GeneralSection.tsx index deca9d3f..1ed1ae20 100644 --- a/src/renderer/components/settings/sections/GeneralSection.tsx +++ b/src/renderer/components/settings/sections/GeneralSection.tsx @@ -297,6 +297,31 @@ export const GeneralSection = ({ disabled={saving} /> + {isElectron && !window.navigator.userAgent.includes('Macintosh') && ( + + { + const shouldRelaunch = await confirm({ + title: 'Restart required', + message: 'The app needs to restart to apply the title bar change. Restart now?', + confirmLabel: 'Restart', + }); + if (shouldRelaunch) { + onGeneralToggle('useNativeTitleBar', v); + // Small delay to let config persist before relaunch + setTimeout(() => { + void window.electronAPI?.windowControls?.relaunch(); + }, 200); + } + }} + disabled={saving} + /> + + )} {isElectron && ( <> diff --git a/src/shared/types/api.ts b/src/shared/types/api.ts index 351cf3d1..81960cea 100644 --- a/src/shared/types/api.ts +++ b/src/shared/types/api.ts @@ -432,6 +432,7 @@ export interface ElectronAPI { maximize: () => Promise; close: () => Promise; isMaximized: () => Promise; + relaunch: () => Promise; }; // Updater API diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts index 10dadc50..766d7044 100644 --- a/src/shared/types/notifications.ts +++ b/src/shared/types/notifications.ts @@ -264,6 +264,8 @@ export interface AppConfig { claudeRootPath: string | null; /** Whether to auto-expand AI response groups when opening a transcript or receiving new messages */ autoExpandAIGroups: boolean; + /** Whether to use the native OS title bar instead of the custom one (Linux/Windows) */ + useNativeTitleBar: boolean; }; /** Display and UI settings */ display: {