diff --git a/src/main/index.ts b/src/main/index.ts index cd7511d..2f6e8b6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -23,6 +23,26 @@ let systemTray: SystemTray | undefined; let autoUpdateService: AutoUpdateService | undefined; let globalHotkeyService: GlobalHotkeyService | undefined; +let quickPromptInitialized = false; + +/** Lazily creates the quick prompt window on first use. */ +function ensureQuickPromptWindow(): BrowserWindowType | undefined { + if (quickPromptWindow && !quickPromptWindow.isDestroyed()) return quickPromptWindow; + if (quickPromptInitialized) return undefined; + if (!mainWindow || !appService) return undefined; + + try { + quickPromptWindow = createQuickPromptWindow(); + registerQuickPromptIpcHandlers(mainWindow, appService, quickPromptWindow); + quickPromptInitialized = true; + } catch (err) { + console.error('[aryx] Failed to create quick prompt window:', err); + quickPromptInitialized = true; + } + + return quickPromptWindow; +} + async function bootstrap(): Promise { appService = new AryxAppService(); autoUpdateService?.dispose(); @@ -30,8 +50,6 @@ async function bootstrap(): Promise { mainWindow = createMainWindow(); - // Defer quick prompt window creation — create it lazily when workspace is ready - // so a failure here cannot block the main window from loading. registerIpcHandlers(mainWindow, appService, autoUpdateService); // Start workspace loading in parallel — don't block window from showing. @@ -59,18 +77,13 @@ async function bootstrap(): Promise { systemTray?.updateRunningCount(updatedWorkspace); }); - // Create quick prompt window and register global hotkey - try { - quickPromptWindow = createQuickPromptWindow(); - registerQuickPromptIpcHandlers(mainWindow!, appService!, quickPromptWindow); - } catch (err) { - console.error('[aryx] Failed to create quick prompt window:', err); - } - + // Register global hotkey — the quick prompt window is created lazily on + // first press so it cannot interfere with main window startup. globalHotkeyService = new GlobalHotkeyService(); const hotkeySettings = workspace.settings.quickPrompt ?? createDefaultQuickPromptSettings(); globalHotkeyService.register(hotkeySettings, () => { - if (quickPromptWindow) toggleQuickPromptWindow(quickPromptWindow); + const win = ensureQuickPromptWindow(); + if (win) toggleQuickPromptWindow(win); }); // Re-register hotkey when settings change diff --git a/src/main/services/globalHotkey.ts b/src/main/services/globalHotkey.ts index f00379e..e49b2e7 100644 --- a/src/main/services/globalHotkey.ts +++ b/src/main/services/globalHotkey.ts @@ -2,10 +2,9 @@ import electron from 'electron'; import type { QuickPromptSettings } from '@shared/domain/tooling'; -const { globalShortcut } = electron; - export class GlobalHotkeyService { private currentAccelerator: string | undefined; + private lastFailedAccelerator: string | undefined; private callback: (() => void) | undefined; register(settings: QuickPromptSettings, callback: () => void): void { @@ -17,16 +16,41 @@ export class GlobalHotkeyService { } const accelerator = toElectronAccelerator(settings.hotkey); + + // Already registered this accelerator — nothing to do if (accelerator === this.currentAccelerator) return; - this.unregister(); - const registered = globalShortcut.register(accelerator, callback); + // Don't retry an accelerator that already failed (avoids log spam on + // repeated workspace-updated events during startup) + if (accelerator === this.lastFailedAccelerator) return; - if (registered) { - this.currentAccelerator = accelerator; - } else { - console.warn(`[globalHotkey] Failed to register accelerator: ${accelerator}`); + this.unregister(); + + // Access globalShortcut lazily to ensure electron is fully initialised + const { globalShortcut } = electron; + if (!globalShortcut) { + console.error('[globalHotkey] electron.globalShortcut is not available'); + return; + } + + try { + const registered = globalShortcut.register(accelerator, callback); + if (registered) { + console.info(`[globalHotkey] Registered: ${accelerator}`); + this.currentAccelerator = accelerator; + this.lastFailedAccelerator = undefined; + } else { + console.warn( + `[globalHotkey] Failed to register accelerator: ${accelerator}` + + ' — it may be reserved by the OS or another application', + ); + this.currentAccelerator = undefined; + this.lastFailedAccelerator = accelerator; + } + } catch (err) { + console.error(`[globalHotkey] Error registering ${accelerator}:`, err); this.currentAccelerator = undefined; + this.lastFailedAccelerator = accelerator; } } @@ -38,9 +62,15 @@ export class GlobalHotkeyService { unregister(): void { if (this.currentAccelerator) { - globalShortcut.unregister(this.currentAccelerator); + try { + const { globalShortcut } = electron; + globalShortcut?.unregister(this.currentAccelerator); + } catch { + // best-effort + } this.currentAccelerator = undefined; } + this.lastFailedAccelerator = undefined; } dispose(): void { @@ -50,10 +80,11 @@ export class GlobalHotkeyService { } /** - * Convert our portable hotkey string (e.g. "Super+Shift+A") to an Electron - * accelerator string. Electron uses "Meta" which maps to Cmd on macOS and - * Win key on Windows/Linux. + * Convert a portable hotkey string to an Electron accelerator. + * + * - "Super" → "Meta" (Win key on Windows, Cmd on macOS) + * - Strings already using Electron-native names pass through unchanged. */ function toElectronAccelerator(hotkey: string): string { - return hotkey.replace(/Super/gi, 'Meta'); + return hotkey.replace(/\bSuper\b/gi, 'Meta'); } diff --git a/src/preload/quickprompt.ts b/src/preload/quickprompt.ts index 1ab2972..6a596c5 100644 --- a/src/preload/quickprompt.ts +++ b/src/preload/quickprompt.ts @@ -1,34 +1,49 @@ import electron from 'electron'; -import { ipcChannels } from '@shared/contracts/channels'; import type { QuickPromptElectronApi } from '@shared/contracts/ipc'; const { contextBridge, ipcRenderer } = electron; +// Channel constants are inlined here (not imported from @shared/contracts/channels) +// to avoid Rollup code-splitting a shared chunk that Electron's sandboxed preload +// environment cannot resolve at runtime. +const ch = { + send: 'quick-prompt:send', + discard: 'quick-prompt:discard', + close: 'quick-prompt:close', + continueInAryx: 'quick-prompt:continue-in-aryx', + cancelTurn: 'quick-prompt:cancel-turn', + getCapabilities: 'quick-prompt:get-capabilities', + setSettings: 'quick-prompt:set-settings', + sessionEvent: 'quick-prompt:session-event', + show: 'quick-prompt:show', + hide: 'quick-prompt:hide', +} as const; + const api: QuickPromptElectronApi = { - send: (input) => ipcRenderer.invoke(ipcChannels.quickPromptSend, input), - discard: () => ipcRenderer.invoke(ipcChannels.quickPromptDiscard), - close: () => ipcRenderer.invoke(ipcChannels.quickPromptClose), - continueInAryx: () => ipcRenderer.invoke(ipcChannels.quickPromptContinueInAryx), - cancelTurn: () => ipcRenderer.invoke(ipcChannels.quickPromptCancelTurn), - getCapabilities: () => ipcRenderer.invoke(ipcChannels.quickPromptGetCapabilities), - setSettings: (settings) => ipcRenderer.invoke(ipcChannels.quickPromptSetSettings, settings), + send: (input) => ipcRenderer.invoke(ch.send, input), + discard: () => ipcRenderer.invoke(ch.discard), + close: () => ipcRenderer.invoke(ch.close), + continueInAryx: () => ipcRenderer.invoke(ch.continueInAryx), + cancelTurn: () => ipcRenderer.invoke(ch.cancelTurn), + getCapabilities: () => ipcRenderer.invoke(ch.getCapabilities), + setSettings: (settings) => ipcRenderer.invoke(ch.setSettings, settings), onSessionEvent: (listener) => { const handler = (_event: Electron.IpcRendererEvent, sessionEvent: Parameters[0]) => listener(sessionEvent); - ipcRenderer.on(ipcChannels.quickPromptSessionEvent, handler); - return () => ipcRenderer.off(ipcChannels.quickPromptSessionEvent, handler); + ipcRenderer.on(ch.sessionEvent, handler); + return () => ipcRenderer.off(ch.sessionEvent, handler); }, onShow: (listener) => { const handler = () => listener(); - ipcRenderer.on(ipcChannels.quickPromptShow, handler); - return () => ipcRenderer.off(ipcChannels.quickPromptShow, handler); + ipcRenderer.on(ch.show, handler); + return () => ipcRenderer.off(ch.show, handler); }, onHide: (listener) => { const handler = () => listener(); - ipcRenderer.on(ipcChannels.quickPromptHide, handler); - return () => ipcRenderer.off(ipcChannels.quickPromptHide, handler); + ipcRenderer.on(ch.hide, handler); + return () => ipcRenderer.off(ch.hide, handler); }, }; diff --git a/src/renderer/components/SettingsPanel.tsx b/src/renderer/components/SettingsPanel.tsx index 03efde7..52b6d12 100644 --- a/src/renderer/components/SettingsPanel.tsx +++ b/src/renderer/components/SettingsPanel.tsx @@ -8,6 +8,7 @@ import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor import { McpServerEditor } from '@renderer/components/settings/McpServerEditor'; import { WorkspaceAgentEditor } from '@renderer/components/settings/WorkspaceAgentEditor'; import { getElectronApi } from '@renderer/lib/electronApi'; +import { isMac } from '@renderer/lib/platform'; import type { SidecarCapabilities, QuotaSnapshot } from '@shared/contracts/sidecar'; import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling'; import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling'; @@ -1328,14 +1329,15 @@ function QuickPromptSettingsSection({ onUpdate?: (patch: Partial) => void; }) { const enabled = settings?.enabled ?? true; - const hotkey = settings?.hotkey ?? 'Super+Shift+A'; + const hotkey = settings?.hotkey ?? 'Alt+Shift+C'; const defaultModel = settings?.defaultModel; const defaultReasoning = settings?.defaultReasoningEffort; const hotkeyDisplay = hotkey - .replace('Super', process.platform === 'darwin' ? '⌘' : 'Win') + .replace('Super', isMac ? '⌘' : 'Win') + .replace('Alt', isMac ? '⌥' : 'Alt') .replace('Shift', '⇧') - .replace('+', ' + '); + .replace(/\+/g, ' + '); return (
diff --git a/src/shared/domain/tooling.ts b/src/shared/domain/tooling.ts index 948d583..d1c4e25 100644 --- a/src/shared/domain/tooling.ts +++ b/src/shared/domain/tooling.ts @@ -71,7 +71,7 @@ export interface QuickPromptSettings { export function createDefaultQuickPromptSettings(): QuickPromptSettings { return { enabled: true, - hotkey: 'Super+Shift+A', + hotkey: 'Alt+Shift+C', }; }