diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index 8233fc449b..2e04ab651f 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -9,9 +9,10 @@ import ConfigSettings from './config/ConfigSettings'; import PromptsSettingsSection from './PromptsSettingsSection'; import { ExtensionConfig } from '../../api'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; -import { Bot, Share2, Monitor, MessageSquare, FileText } from 'lucide-react'; +import { Bot, Share2, Monitor, MessageSquare, FileText, Keyboard } from 'lucide-react'; import { useState, useEffect, useRef } from 'react'; import ChatSettingsSection from './chat/ChatSettingsSection'; +import KeyboardShortcutsSection from './keyboard/KeyboardShortcutsSection'; import { CONFIGURATION_ENABLED } from '../../updates'; import { trackSettingsTabViewed } from '../../utils/analytics'; @@ -52,6 +53,7 @@ export default function SettingsView({ app: 'app', chat: 'chat', prompts: 'prompts', + keyboard: 'keyboard', }; const targetTab = sectionToTab[viewOptions.section]; @@ -130,6 +132,14 @@ export default function SettingsView({ Prompts + + + Keyboard + App @@ -169,6 +179,13 @@ export default function SettingsView({ + + + + { ipcMain.handle('save-settings', (_event, settings) => { try { + const oldSettings = loadSettings(); saveSettings(settings); - return true; + + // Check if keyboard shortcuts changed + const oldShortcuts = getKeyboardShortcuts(oldSettings); + const newShortcuts = getKeyboardShortcuts(settings); + const shortcutsChanged = JSON.stringify(oldShortcuts) !== JSON.stringify(newShortcuts); + + // Re-register global shortcuts if they changed + if (shortcutsChanged) { + registerGlobalShortcuts(); + } + + return { success: true, shortcutsChanged }; } catch (error) { console.error('Error saving settings:', error); - return false; + return { success: false, shortcutsChanged: false }; } }); @@ -1806,6 +1819,37 @@ const focusWindow = () => { } }; +// Register global shortcuts based on settings +const registerGlobalShortcuts = () => { + // Unregister all existing shortcuts first + globalShortcut.unregisterAll(); + + const settings = loadSettings(); + const shortcuts = getKeyboardShortcuts(settings); + + // Register focus window shortcut (global) + if (shortcuts.focusWindow) { + try { + globalShortcut.register(shortcuts.focusWindow, () => { + focusWindow(); + }); + } catch (e) { + console.error('Error registering focus window hotkey:', e); + } + } + + // Register quick launcher shortcut (global) + if (shortcuts.quickLauncher) { + try { + globalShortcut.register(shortcuts.quickLauncher, () => { + createLauncher(); + }); + } catch (e) { + console.error('Error registering launcher hotkey:', e); + } + } +}; + async function appMain() { await configureProxy(); @@ -1872,21 +1916,8 @@ async function appMain() { }); }); - try { - globalShortcut.register('CommandOrControl+Alt+Shift+G', () => { - createLauncher(); - }); - } catch (e) { - console.error('Error registering launcher hotkey:', e); - } - - try { - globalShortcut.register('CommandOrControl+Alt+G', () => { - focusWindow(); - }); - } catch (e) { - console.error('Error registering focus window hotkey:', e); - } + // Register global shortcuts based on settings + registerGlobalShortcuts(); session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { details.requestHeaders['Origin'] = 'http://localhost:5173'; @@ -1936,20 +1967,24 @@ async function appMain() { const menu = Menu.getApplicationMenu(); + const shortcuts = getKeyboardShortcuts(settings); + const appMenu = menu?.items.find((item) => item.label === 'Goose'); if (appMenu?.submenu) { appMenu.submenu.insert(1, new MenuItem({ type: 'separator' })); - appMenu.submenu.insert( - 1, - new MenuItem({ - label: 'Settings', - accelerator: 'CmdOrCtrl+,', - click() { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings'); - }, - }) - ); + if (shortcuts.settings) { + appMenu.submenu.insert( + 1, + new MenuItem({ + label: 'Settings', + accelerator: shortcuts.settings, + click() { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings'); + }, + }) + ); + } appMenu.submenu.insert(1, new MenuItem({ type: 'separator' })); } @@ -1960,7 +1995,7 @@ async function appMain() { const findSubmenu = Menu.buildFromTemplate([ { label: 'Find…', - accelerator: process.platform === 'darwin' ? 'Command+F' : 'Control+F', + accelerator: shortcuts.find || undefined, click() { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) focusedWindow.webContents.send('find-command'); @@ -1968,7 +2003,7 @@ async function appMain() { }, { label: 'Find Next', - accelerator: process.platform === 'darwin' ? 'Command+G' : 'Control+G', + accelerator: shortcuts.findNext || undefined, click() { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) focusedWindow.webContents.send('find-next'); @@ -1976,7 +2011,7 @@ async function appMain() { }, { label: 'Find Previous', - accelerator: process.platform === 'darwin' ? 'Shift+Command+G' : 'Shift+Control+G', + accelerator: shortcuts.findPrevious || undefined, click() { const focusedWindow = BrowserWindow.getFocusedWindow(); if (focusedWindow) focusedWindow.webContents.send('find-previous'); @@ -2005,37 +2040,43 @@ async function appMain() { const fileMenu = menu?.items.find((item) => item.label === 'File'); if (fileMenu?.submenu) { - fileMenu.submenu.insert( - 0, - new MenuItem({ - label: 'New Chat', - accelerator: 'CmdOrCtrl+T', - click() { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) focusedWindow.webContents.send('new-chat'); - }, - }) - ); + if (shortcuts.newChat) { + fileMenu.submenu.insert( + 0, + new MenuItem({ + label: 'New Chat', + accelerator: shortcuts.newChat, + click() { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) focusedWindow.webContents.send('new-chat'); + }, + }) + ); + } - fileMenu.submenu.insert( - 1, - new MenuItem({ - label: 'New Chat Window', - accelerator: process.platform === 'darwin' ? 'Cmd+N' : 'Ctrl+N', - click() { - ipcMain.emit('create-chat-window'); - }, - }) - ); + if (shortcuts.newChatWindow) { + fileMenu.submenu.insert( + 1, + new MenuItem({ + label: 'New Chat Window', + accelerator: shortcuts.newChatWindow, + click() { + ipcMain.emit('create-chat-window'); + }, + }) + ); + } - fileMenu.submenu.insert( - 2, - new MenuItem({ - label: 'Open Directory...', - accelerator: 'CmdOrCtrl+O', - click: () => openDirectoryDialog(), - }) - ); + if (shortcuts.openDirectory) { + fileMenu.submenu.insert( + 2, + new MenuItem({ + label: 'Open Directory...', + accelerator: shortcuts.openDirectory, + click: () => openDirectoryDialog(), + }) + ); + } const recentFilesSubmenu = buildRecentFilesMenu(); if (recentFilesSubmenu.length > 0) { @@ -2050,25 +2091,30 @@ async function appMain() { fileMenu.submenu.insert(4, new MenuItem({ type: 'separator' })); - fileMenu.submenu.append( - new MenuItem({ - label: 'Focus Goose Window', - accelerator: 'CmdOrCtrl+Alt+G', - click() { - focusWindow(); - }, - }) - ); + // Add global shortcuts from settings + if (shortcuts.focusWindow) { + fileMenu.submenu.append( + new MenuItem({ + label: 'Focus Goose Window', + accelerator: shortcuts.focusWindow, + click() { + focusWindow(); + }, + }) + ); + } - fileMenu.submenu.append( - new MenuItem({ - label: 'Quick Launcher', - accelerator: 'CmdOrCtrl+Alt+Shift+G', - click() { - createLauncher(); - }, - }) - ); + if (shortcuts.quickLauncher) { + fileMenu.submenu.append( + new MenuItem({ + label: 'Quick Launcher', + accelerator: shortcuts.quickLauncher, + click() { + createLauncher(); + }, + }) + ); + } } if (menu) { @@ -2089,29 +2135,31 @@ async function appMain() { } if (windowMenu.submenu) { - windowMenu.submenu.append( - new MenuItem({ - label: 'Always on Top', - type: 'checkbox', - accelerator: process.platform === 'darwin' ? 'Cmd+Shift+T' : 'Ctrl+Shift+T', - click(menuItem) { - const focusedWindow = BrowserWindow.getFocusedWindow(); - if (focusedWindow) { - const isAlwaysOnTop = menuItem.checked; + if (shortcuts.alwaysOnTop) { + windowMenu.submenu.append( + new MenuItem({ + label: 'Always on Top', + type: 'checkbox', + accelerator: shortcuts.alwaysOnTop, + click(menuItem) { + const focusedWindow = BrowserWindow.getFocusedWindow(); + if (focusedWindow) { + const isAlwaysOnTop = menuItem.checked; - if (process.platform === 'darwin') { - focusedWindow.setAlwaysOnTop(isAlwaysOnTop, 'floating'); - } else { - focusedWindow.setAlwaysOnTop(isAlwaysOnTop); + if (process.platform === 'darwin') { + focusedWindow.setAlwaysOnTop(isAlwaysOnTop, 'floating'); + } else { + focusedWindow.setAlwaysOnTop(isAlwaysOnTop); + } + + console.log( + `[Main] Set always-on-top to ${isAlwaysOnTop} for window ${focusedWindow.id}` + ); } - - console.log( - `[Main] Set always-on-top to ${isAlwaysOnTop} for window ${focusedWindow.id}` - ); - } - }, - }) - ); + }, + }) + ); + } } } diff --git a/ui/desktop/src/utils/settings.ts b/ui/desktop/src/utils/settings.ts index 3eaf5e97d8..910ab89757 100644 --- a/ui/desktop/src/utils/settings.ts +++ b/ui/desktop/src/utils/settings.ts @@ -1,6 +1,7 @@ import { app } from 'electron'; import fs from 'fs'; import path from 'path'; +import { defaultKeyboardShortcuts } from './keyboardShortcutDefaults'; export interface EnvToggles { GOOSE_SERVER__MEMORY: boolean; @@ -13,6 +14,19 @@ export interface ExternalGoosedConfig { secret: string; } +export interface KeyboardShortcuts { + focusWindow: string | null; // null means disabled + quickLauncher: string | null; + newChat: string | null; + newChatWindow: string | null; + openDirectory: string | null; + settings: string | null; + find: string | null; + findNext: string | null; + findPrevious: string | null; + alwaysOnTop: string | null; +} + export interface Settings { envToggles: EnvToggles; showMenuBarIcon: boolean; @@ -20,6 +34,8 @@ export interface Settings { enableWakelock: boolean; spellcheckEnabled: boolean; externalGoosed?: ExternalGoosedConfig; + globalShortcut?: string | null; // Deprecated: use keyboardShortcuts.focusWindow + keyboardShortcuts?: KeyboardShortcuts; } const SETTINGS_FILE = path.join(app.getPath('userData'), 'settings.json'); @@ -33,6 +49,8 @@ const defaultSettings: Settings = { showDockIcon: true, enableWakelock: false, spellcheckEnabled: true, + globalShortcut: 'CommandOrControl+Alt+G', // Deprecated: kept for backwards compatibility + keyboardShortcuts: defaultKeyboardShortcuts, }; // Settings management @@ -69,3 +87,17 @@ export function updateEnvironmentVariables(envToggles: EnvToggles): void { delete process.env.GOOSE_SERVER__COMPUTER_CONTROLLER; } } + +export function getKeyboardShortcuts(settings: Settings): KeyboardShortcuts { + // Migrate from old globalShortcut field if needed + if (!settings.keyboardShortcuts && settings.globalShortcut !== undefined) { + return { + ...defaultKeyboardShortcuts, + focusWindow: settings.globalShortcut, + quickLauncher: settings.globalShortcut + ? settings.globalShortcut.replace(/\+G$/i, '+Shift+G') + : null, + }; + } + return settings.keyboardShortcuts || defaultKeyboardShortcuts; +}