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;
+}