fix: resolve hotkey registration failures and renderer crash

- Change default hotkey from Super+Shift+A to Alt+Shift+A — Win key
  combinations are frequently reserved by the OS and fail to register
- Make quick prompt window creation lazy (first hotkey press) so it
  cannot interfere with main window startup
- Track failed accelerators to prevent error log spam from repeated
  workspace-updated events
- Fix process.platform crash in renderer SettingsPanel — use isMac
  from @renderer/lib/platform instead (renderer has no node globals)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-04-13 11:54:10 +02:00
co-authored by Copilot
parent 6714b93bed
commit fb5970cfa7
5 changed files with 103 additions and 42 deletions
+24 -11
View File
@@ -23,6 +23,26 @@ let systemTray: SystemTray | undefined;
let autoUpdateService: AutoUpdateService | undefined; let autoUpdateService: AutoUpdateService | undefined;
let globalHotkeyService: GlobalHotkeyService | 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<void> { async function bootstrap(): Promise<void> {
appService = new AryxAppService(); appService = new AryxAppService();
autoUpdateService?.dispose(); autoUpdateService?.dispose();
@@ -30,8 +50,6 @@ async function bootstrap(): Promise<void> {
mainWindow = createMainWindow(); 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); registerIpcHandlers(mainWindow, appService, autoUpdateService);
// Start workspace loading in parallel — don't block window from showing. // Start workspace loading in parallel — don't block window from showing.
@@ -59,18 +77,13 @@ async function bootstrap(): Promise<void> {
systemTray?.updateRunningCount(updatedWorkspace); systemTray?.updateRunningCount(updatedWorkspace);
}); });
// Create quick prompt window and register global hotkey // Register global hotkey — the quick prompt window is created lazily on
try { // first press so it cannot interfere with main window startup.
quickPromptWindow = createQuickPromptWindow();
registerQuickPromptIpcHandlers(mainWindow!, appService!, quickPromptWindow);
} catch (err) {
console.error('[aryx] Failed to create quick prompt window:', err);
}
globalHotkeyService = new GlobalHotkeyService(); globalHotkeyService = new GlobalHotkeyService();
const hotkeySettings = workspace.settings.quickPrompt ?? createDefaultQuickPromptSettings(); const hotkeySettings = workspace.settings.quickPrompt ?? createDefaultQuickPromptSettings();
globalHotkeyService.register(hotkeySettings, () => { globalHotkeyService.register(hotkeySettings, () => {
if (quickPromptWindow) toggleQuickPromptWindow(quickPromptWindow); const win = ensureQuickPromptWindow();
if (win) toggleQuickPromptWindow(win);
}); });
// Re-register hotkey when settings change // Re-register hotkey when settings change
+44 -13
View File
@@ -2,10 +2,9 @@ import electron from 'electron';
import type { QuickPromptSettings } from '@shared/domain/tooling'; import type { QuickPromptSettings } from '@shared/domain/tooling';
const { globalShortcut } = electron;
export class GlobalHotkeyService { export class GlobalHotkeyService {
private currentAccelerator: string | undefined; private currentAccelerator: string | undefined;
private lastFailedAccelerator: string | undefined;
private callback: (() => void) | undefined; private callback: (() => void) | undefined;
register(settings: QuickPromptSettings, callback: () => void): void { register(settings: QuickPromptSettings, callback: () => void): void {
@@ -17,16 +16,41 @@ export class GlobalHotkeyService {
} }
const accelerator = toElectronAccelerator(settings.hotkey); const accelerator = toElectronAccelerator(settings.hotkey);
// Already registered this accelerator — nothing to do
if (accelerator === this.currentAccelerator) return; if (accelerator === this.currentAccelerator) return;
this.unregister(); // Don't retry an accelerator that already failed (avoids log spam on
const registered = globalShortcut.register(accelerator, callback); // repeated workspace-updated events during startup)
if (accelerator === this.lastFailedAccelerator) return;
if (registered) { this.unregister();
this.currentAccelerator = accelerator;
} else { // Access globalShortcut lazily to ensure electron is fully initialised
console.warn(`[globalHotkey] Failed to register accelerator: ${accelerator}`); 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.currentAccelerator = undefined;
this.lastFailedAccelerator = accelerator;
} }
} }
@@ -38,9 +62,15 @@ export class GlobalHotkeyService {
unregister(): void { unregister(): void {
if (this.currentAccelerator) { if (this.currentAccelerator) {
globalShortcut.unregister(this.currentAccelerator); try {
const { globalShortcut } = electron;
globalShortcut?.unregister(this.currentAccelerator);
} catch {
// best-effort
}
this.currentAccelerator = undefined; this.currentAccelerator = undefined;
} }
this.lastFailedAccelerator = undefined;
} }
dispose(): void { dispose(): void {
@@ -50,10 +80,11 @@ export class GlobalHotkeyService {
} }
/** /**
* Convert our portable hotkey string (e.g. "Super+Shift+A") to an Electron * Convert a portable hotkey string to an Electron accelerator.
* accelerator string. Electron uses "Meta" which maps to Cmd on macOS and *
* Win key on Windows/Linux. * - "Super" → "Meta" (Win key on Windows, Cmd on macOS)
* - Strings already using Electron-native names pass through unchanged.
*/ */
function toElectronAccelerator(hotkey: string): string { function toElectronAccelerator(hotkey: string): string {
return hotkey.replace(/Super/gi, 'Meta'); return hotkey.replace(/\bSuper\b/gi, 'Meta');
} }
+29 -14
View File
@@ -1,34 +1,49 @@
import electron from 'electron'; import electron from 'electron';
import { ipcChannels } from '@shared/contracts/channels';
import type { QuickPromptElectronApi } from '@shared/contracts/ipc'; import type { QuickPromptElectronApi } from '@shared/contracts/ipc';
const { contextBridge, ipcRenderer } = electron; 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 = { const api: QuickPromptElectronApi = {
send: (input) => ipcRenderer.invoke(ipcChannels.quickPromptSend, input), send: (input) => ipcRenderer.invoke(ch.send, input),
discard: () => ipcRenderer.invoke(ipcChannels.quickPromptDiscard), discard: () => ipcRenderer.invoke(ch.discard),
close: () => ipcRenderer.invoke(ipcChannels.quickPromptClose), close: () => ipcRenderer.invoke(ch.close),
continueInAryx: () => ipcRenderer.invoke(ipcChannels.quickPromptContinueInAryx), continueInAryx: () => ipcRenderer.invoke(ch.continueInAryx),
cancelTurn: () => ipcRenderer.invoke(ipcChannels.quickPromptCancelTurn), cancelTurn: () => ipcRenderer.invoke(ch.cancelTurn),
getCapabilities: () => ipcRenderer.invoke(ipcChannels.quickPromptGetCapabilities), getCapabilities: () => ipcRenderer.invoke(ch.getCapabilities),
setSettings: (settings) => ipcRenderer.invoke(ipcChannels.quickPromptSetSettings, settings), setSettings: (settings) => ipcRenderer.invoke(ch.setSettings, settings),
onSessionEvent: (listener) => { onSessionEvent: (listener) => {
const handler = (_event: Electron.IpcRendererEvent, sessionEvent: Parameters<typeof listener>[0]) => const handler = (_event: Electron.IpcRendererEvent, sessionEvent: Parameters<typeof listener>[0]) =>
listener(sessionEvent); listener(sessionEvent);
ipcRenderer.on(ipcChannels.quickPromptSessionEvent, handler); ipcRenderer.on(ch.sessionEvent, handler);
return () => ipcRenderer.off(ipcChannels.quickPromptSessionEvent, handler); return () => ipcRenderer.off(ch.sessionEvent, handler);
}, },
onShow: (listener) => { onShow: (listener) => {
const handler = () => listener(); const handler = () => listener();
ipcRenderer.on(ipcChannels.quickPromptShow, handler); ipcRenderer.on(ch.show, handler);
return () => ipcRenderer.off(ipcChannels.quickPromptShow, handler); return () => ipcRenderer.off(ch.show, handler);
}, },
onHide: (listener) => { onHide: (listener) => {
const handler = () => listener(); const handler = () => listener();
ipcRenderer.on(ipcChannels.quickPromptHide, handler); ipcRenderer.on(ch.hide, handler);
return () => ipcRenderer.off(ipcChannels.quickPromptHide, handler); return () => ipcRenderer.off(ch.hide, handler);
}, },
}; };
+5 -3
View File
@@ -8,6 +8,7 @@ import { LspProfileEditor } from '@renderer/components/settings/LspProfileEditor
import { McpServerEditor } from '@renderer/components/settings/McpServerEditor'; import { McpServerEditor } from '@renderer/components/settings/McpServerEditor';
import { WorkspaceAgentEditor } from '@renderer/components/settings/WorkspaceAgentEditor'; import { WorkspaceAgentEditor } from '@renderer/components/settings/WorkspaceAgentEditor';
import { getElectronApi } from '@renderer/lib/electronApi'; import { getElectronApi } from '@renderer/lib/electronApi';
import { isMac } from '@renderer/lib/platform';
import type { SidecarCapabilities, QuotaSnapshot } from '@shared/contracts/sidecar'; import type { SidecarCapabilities, QuotaSnapshot } from '@shared/contracts/sidecar';
import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling'; import type { DiscoveredMcpServer, DiscoveredToolingState } from '@shared/domain/discoveredTooling';
import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling'; import { listAcceptedDiscoveredMcpServers, listPendingDiscoveredMcpServers } from '@shared/domain/discoveredTooling';
@@ -1328,14 +1329,15 @@ function QuickPromptSettingsSection({
onUpdate?: (patch: Partial<QuickPromptSettings>) => void; onUpdate?: (patch: Partial<QuickPromptSettings>) => void;
}) { }) {
const enabled = settings?.enabled ?? true; const enabled = settings?.enabled ?? true;
const hotkey = settings?.hotkey ?? 'Super+Shift+A'; const hotkey = settings?.hotkey ?? 'Alt+Shift+C';
const defaultModel = settings?.defaultModel; const defaultModel = settings?.defaultModel;
const defaultReasoning = settings?.defaultReasoningEffort; const defaultReasoning = settings?.defaultReasoningEffort;
const hotkeyDisplay = hotkey const hotkeyDisplay = hotkey
.replace('Super', process.platform === 'darwin' ? '⌘' : 'Win') .replace('Super', isMac ? '⌘' : 'Win')
.replace('Alt', isMac ? '⌥' : 'Alt')
.replace('Shift', '⇧') .replace('Shift', '⇧')
.replace('+', ' + '); .replace(/\+/g, ' + ');
return ( return (
<div> <div>
+1 -1
View File
@@ -71,7 +71,7 @@ export interface QuickPromptSettings {
export function createDefaultQuickPromptSettings(): QuickPromptSettings { export function createDefaultQuickPromptSettings(): QuickPromptSettings {
return { return {
enabled: true, enabled: true,
hotkey: 'Super+Shift+A', hotkey: 'Alt+Shift+C',
}; };
} }