From 4ff5ed462aee0d5e7690713621578f9cd3c75400 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 29 May 2025 14:29:21 -0500 Subject: [PATCH] fix: menu bar and dock icon settings (#2490) Co-authored-by: Zane Staggs --- .husky/pre-commit | 16 ++- .../components/settings_v2/SettingsView.tsx | 3 + .../settings_v2/app/AppSettingsSection.tsx | 112 ++++++++++++++++++ .../response_styles/ResponseStylesSection.tsx | 2 +- ui/desktop/src/main.ts | 89 +++++++++++++- ui/desktop/src/preload.ts | 8 ++ ui/desktop/src/utils/settings.ts | 4 + 7 files changed, 228 insertions(+), 6 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx diff --git a/.husky/pre-commit b/.husky/pre-commit index f2796dcae8..4437aea846 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,12 +2,20 @@ # Only auto-format desktop TS code if relevant files are modified if git diff --cached --name-only | grep -q "^ui/desktop/"; then - . "$(dirname -- "$0")/_/husky.sh" - cd ui/desktop && npx lint-staged + if [ -d "ui/desktop" ]; then + . "$(dirname -- "$0")/_/husky.sh" + cd ui/desktop && npx lint-staged + else + echo "Warning: ui/desktop directory does not exist, skipping lint-staged" + fi fi # Only auto-format ui-v2 TS code if relevant files are modified if git diff --cached --name-only | grep -q "^ui-v2/"; then - . "$(dirname -- "$0")/_/husky.sh" - cd ui-v2 && npx lint-staged + if [ -d "ui-v2" ]; then + . "$(dirname -- "$0")/_/husky.sh" + cd ui-v2 && npx lint-staged + else + echo "Warning: ui-v2 directory does not exist, skipping lint-staged" + fi fi diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index b6a5bd7d3d..54d95b9227 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -7,6 +7,7 @@ import { ModeSection } from './mode/ModeSection'; import { ToolSelectionStrategySection } from './tool_selection_strategy/ToolSelectionStrategySection'; import SessionSharingSection from './sessions/SessionSharingSection'; import { ResponseStylesSection } from './response_styles/ResponseStylesSection'; +import AppSettingsSection from './app/AppSettingsSection'; import { ExtensionConfig } from '../../api'; import MoreMenuLayout from '../more_menu/MoreMenuLayout'; @@ -53,6 +54,8 @@ export default function SettingsView({ {/* Tool Selection Strategy */} + {/* App Settings */} + diff --git a/ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx new file mode 100644 index 0000000000..317b9e2b29 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/app/AppSettingsSection.tsx @@ -0,0 +1,112 @@ +import { useState, useEffect } from 'react'; +import { Switch } from '../../ui/switch'; + +export default function AppSettingsSection() { + const [menuBarIconEnabled, setMenuBarIconEnabled] = useState(true); + const [dockIconEnabled, setDockIconEnabled] = useState(true); + const [isMacOS, setIsMacOS] = useState(false); + const [isDockSwitchDisabled, setIsDockSwitchDisabled] = useState(false); + + // Check if running on macOS + useEffect(() => { + setIsMacOS(window.electron.platform === 'darwin'); + }, []); + + // Load menu bar and dock icon states + useEffect(() => { + window.electron.getMenuBarIconState().then((enabled) => { + setMenuBarIconEnabled(enabled); + }); + + if (isMacOS) { + window.electron.getDockIconState().then((enabled) => { + setDockIconEnabled(enabled); + }); + } + }, [isMacOS]); + + const handleMenuBarIconToggle = async () => { + const newState = !menuBarIconEnabled; + // If we're turning off the menu bar icon and the dock icon is hidden, + // we need to show the dock icon to maintain accessibility + if (!newState && !dockIconEnabled && isMacOS) { + const success = await window.electron.setDockIcon(true); + if (success) { + setDockIconEnabled(true); + } + } + const success = await window.electron.setMenuBarIcon(newState); + if (success) { + setMenuBarIconEnabled(newState); + } + }; + + const handleDockIconToggle = async () => { + const newState = !dockIconEnabled; + // If we're turning off the dock icon and the menu bar icon is hidden, + // we need to show the menu bar icon to maintain accessibility + if (!newState && !menuBarIconEnabled) { + const success = await window.electron.setMenuBarIcon(true); + if (success) { + setMenuBarIconEnabled(true); + } + } + + // Disable the switch to prevent rapid toggling + setIsDockSwitchDisabled(true); + setTimeout(() => { + setIsDockSwitchDisabled(false); + }, 1000); + + // Set the dock icon state + const success = await window.electron.setDockIcon(newState); + if (success) { + setDockIconEnabled(newState); + } + }; + + return ( +
+
+

App Settings

+
+
+

Configure Goose app

+
+
+
+

Menu Bar Icon

+

+ Show Goose in the menu bar +

+
+
+ +
+
+ + {isMacOS && ( +
+
+

Dock Icon

+

Show Goose in the dock

+
+
+ +
+
+ )} +
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx b/ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx index 2c62965936..5cf18bb7cd 100644 --- a/ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx +++ b/ui/desktop/src/components/settings_v2/response_styles/ResponseStylesSection.tsx @@ -29,7 +29,7 @@ export const ResponseStylesSection = () => {

Response Styles

-
+

Choose how Goose should format and style its responses

diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts index b93ee16f50..18387424a3 100644 --- a/ui/desktop/src/main.ts +++ b/ui/desktop/src/main.ts @@ -574,7 +574,17 @@ const createChat = async ( // Track tray instance let tray: Tray | null = null; +const destroyTray = () => { + if (tray) { + tray.destroy(); + tray = null; + } +}; + const createTray = () => { + // If tray already exists, destroy it first + destroyTray(); + const isDev = process.env.NODE_ENV === 'development'; let iconPath: string; @@ -701,6 +711,73 @@ ipcMain.handle('directory-chooser', (_event, replace: boolean = false) => { return openDirectoryDialog(replace); }); +// Handle menu bar icon visibility +ipcMain.handle('set-menu-bar-icon', async (_event, show: boolean) => { + try { + const settings = loadSettings(); + settings.showMenuBarIcon = show; + saveSettings(settings); + + if (show) { + createTray(); + } else { + destroyTray(); + } + return true; + } catch (error) { + console.error('Error setting menu bar icon:', error); + return false; + } +}); + +ipcMain.handle('get-menu-bar-icon-state', () => { + try { + const settings = loadSettings(); + return settings.showMenuBarIcon ?? true; + } catch (error) { + console.error('Error getting menu bar icon state:', error); + return true; + } +}); + +// Handle dock icon visibility (macOS only) +ipcMain.handle('set-dock-icon', async (_event, show: boolean) => { + try { + if (process.platform !== 'darwin') return false; + + const settings = loadSettings(); + settings.showDockIcon = show; + saveSettings(settings); + + if (show) { + await app.dock.show(); + } else { + // Only hide the dock if we have a menu bar icon to maintain accessibility + if (settings.showMenuBarIcon) { + app.dock.hide(); + setTimeout(() => { + focusWindow(); + }, 50); + } + } + return true; + } catch (error) { + console.error('Error setting dock icon:', error); + return false; + } +}); + +ipcMain.handle('get-dock-icon-state', () => { + try { + if (process.platform !== 'darwin') return true; + const settings = loadSettings(); + return settings.showDockIcon ?? true; + } catch (error) { + console.error('Error getting dock icon state:', error); + return true; + } +}); + // Add file/directory selection handler ipcMain.handle('select-file-or-directory', async () => { const result = await dialog.showOpenDialog({ @@ -1122,10 +1199,20 @@ app.whenReady().then(async () => { }, 5000); } + // Create tray if enabled in settings + const settings = loadSettings(); + if (settings.showMenuBarIcon) { + createTray(); + } + + // Handle dock icon visibility (macOS only) + if (process.platform === 'darwin' && !settings.showDockIcon && settings.showMenuBarIcon) { + app.dock.hide(); + } + // Parse command line arguments const { dirPath } = parseArgs(); - createTray(); createNewWindow(app, dirPath); // Get the existing menu diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts index 9f667aad08..13674f8827 100644 --- a/ui/desktop/src/preload.ts +++ b/ui/desktop/src/preload.ts @@ -58,6 +58,10 @@ type ElectronAPI = { writeFile: (directory: string, content: string) => Promise; getAllowedExtensions: () => Promise; getPathForFile: (file: File) => string; + setMenuBarIcon: (show: boolean) => Promise; + getMenuBarIconState: () => Promise; + setDockIcon: (show: boolean) => Promise; + getDockIconState: () => Promise; on: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void @@ -117,6 +121,10 @@ const electronAPI: ElectronAPI = { ipcRenderer.invoke('write-file', filePath, content), getPathForFile: (file: File) => webUtils.getPathForFile(file), getAllowedExtensions: () => ipcRenderer.invoke('get-allowed-extensions'), + setMenuBarIcon: (show: boolean) => ipcRenderer.invoke('set-menu-bar-icon', show), + getMenuBarIconState: () => ipcRenderer.invoke('get-menu-bar-icon-state'), + setDockIcon: (show: boolean) => ipcRenderer.invoke('set-dock-icon', show), + getDockIconState: () => ipcRenderer.invoke('get-dock-icon-state'), on: ( channel: string, callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void diff --git a/ui/desktop/src/utils/settings.ts b/ui/desktop/src/utils/settings.ts index b6e2d53131..e553bebd53 100644 --- a/ui/desktop/src/utils/settings.ts +++ b/ui/desktop/src/utils/settings.ts @@ -10,6 +10,8 @@ export interface EnvToggles { export interface Settings { envToggles: EnvToggles; + showMenuBarIcon: boolean; + showDockIcon: boolean; } // Constants @@ -20,6 +22,8 @@ const defaultSettings: Settings = { GOOSE_SERVER__MEMORY: false, GOOSE_SERVER__COMPUTER_CONTROLLER: false, }, + showMenuBarIcon: true, + showDockIcon: true, }; // Settings management