mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-27 13:23:57 +02:00
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:
+24
-11
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user