diff --git a/electron/main.js b/electron/main.js index ef9e71a6..19c91f4e 100644 --- a/electron/main.js +++ b/electron/main.js @@ -2252,6 +2252,14 @@ function routeReloadShortcut(event, input) { // guest never reach the host renderer, so we catch them here and forward the intent + the guest's // webContents id so the renderer can target that exact browser. Attached to guests ONLY: on the host // the renderer's own keydown handles canvas-vs-browser, and intercepting there would eat canvas zoom. +// The renderer registers the user's new-agent combo so it still fires while a guest webview holds focus (host keydown never sees those). +let newAgentCombo = { primary: true, shift: false, key: 'l' }; +ipcMain.on('set-new-agent-shortcut', (_e, combo) => { + if (combo && typeof combo.key === 'string' && combo.key) { + newAgentCombo = { primary: !!combo.primary, shift: !!combo.shift, key: combo.key.toLowerCase() }; + } +}); + function routeBrowserShortcut(event, input, webContentsId) { if (input.type !== 'keyDown' || input.alt) return; const mod = input.meta || input.control; @@ -2263,6 +2271,7 @@ function routeBrowserShortcut(event, input, webContentsId) { else if (mod && !input.shift && key === 'f') action = 'find'; else if (mod && input.shift && key === 't') action = 'reopen-closed'; else if (input.control && !input.meta && key === 'tab') action = input.shift ? 'tab-prev' : 'tab-next'; + else if (mod === newAgentCombo.primary && input.shift === newAgentCombo.shift && key === newAgentCombo.key) action = 'new-agent'; if (!action) return; event.preventDefault(); try { diff --git a/electron/preload.js b/electron/preload.js index 3492415d..b7b751cd 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -43,6 +43,7 @@ contextBridge.exposeInMainWorld('openswarm', { setWindowButtonsVisible: (visible) => ipcRenderer.invoke('set-window-buttons-visible', visible), // Native window bg tracks the theme so a live resize never paints the boot-dark color behind a light UI. setWindowBackground: (color) => ipcRenderer.invoke('set-window-background', color), + setNewAgentShortcut: (combo) => ipcRenderer.send('set-new-agent-shortcut', combo), // Phase 2 provenance: { sha, shortSha, builtAt, channel } for the About panel. getBuildInfo: () => ipcRenderer.invoke('get-build-info'), diff --git a/frontend/src/app/components/Layout/AppShell.tsx b/frontend/src/app/components/Layout/AppShell.tsx index 847a60b8..cba5ee1d 100644 --- a/frontend/src/app/components/Layout/AppShell.tsx +++ b/frontend/src/app/components/Layout/AppShell.tsx @@ -281,6 +281,7 @@ const AppShell: React.FC = () => { return w.openswarm.onBrowserShortcut((payload: { action: string; webContentsId: number }) => { // Reopen-last-closed is global (no target browser), so handle it before the per-browser id guard. if (payload.action === 'reopen-closed') { dispatch(reopenLastClosed()); return; } + if (payload.action === 'new-agent') { window.dispatchEvent(new CustomEvent('openswarm:new-agent')); return; } const id = findBrowserByWebContentsId(payload.webContentsId) ?? getLastInteractedBrowser(); if (!id) return; switch (payload.action) { diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts index 83b40876..9e469652 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useDashboardShortcuts.ts @@ -37,6 +37,9 @@ export function useDashboardShortcuts({ const needsShift = parts.includes('shift'); const needsAlt = parts.includes('alt'); + // Main matches this combo inside focused webviews (host keydown never fires there) and echoes it back as openswarm:new-agent. + (window as unknown as { openswarm?: { setNewAgentShortcut?: (c: { primary: boolean; shift: boolean; key: string }) => void } }).openswarm?.setNewAgentShortcut?.({ primary: needsPrimary, shift: needsShift, key }); + const handleShortcut = (e: KeyboardEvent) => { if (!isActive) return; // Don't fire shortcuts when dashboard is hidden if (e.key.toLowerCase() !== key) return; @@ -46,9 +49,14 @@ export function useDashboardShortcuts({ e.preventDefault(); setToolbarOpen(true); }; + const handleForwarded = (): void => { if (isActive) setToolbarOpen(true); }; window.addEventListener('keydown', handleShortcut); - return () => window.removeEventListener('keydown', handleShortcut); - }, [newAgentShortcut]); + window.addEventListener('openswarm:new-agent', handleForwarded); + return () => { + window.removeEventListener('keydown', handleShortcut); + window.removeEventListener('openswarm:new-agent', handleForwarded); + }; + }, [newAgentShortcut, isActive, setToolbarOpen]); useEffect(() => { const handleEnter = (e: KeyboardEvent) => { @@ -64,7 +72,7 @@ export function useDashboardShortcuts({ }; window.addEventListener('keydown', handleEnter); return () => window.removeEventListener('keydown', handleEnter); - }, [selection.selectedIds, dispatch]); + }, [selection.selectedIds, dispatch, isActive]); useEffect(() => { const handleDelete = (e: KeyboardEvent) => { @@ -79,7 +87,7 @@ export function useDashboardShortcuts({ }; window.addEventListener('keydown', handleDelete); return () => window.removeEventListener('keydown', handleDelete); - }, [selection, dispatch]); + }, [selection, dispatch, isActive]); // Cmd/Ctrl+Shift+T reopens the most recently closed card (browser, agent, note, app, workflow, or browser tab), like a browser's reopen-closed-tab. The guest-focused case routes through main -> AppShell. useEffect(() => { @@ -123,5 +131,5 @@ export function useDashboardShortcuts({ }; window.addEventListener('keydown', handleSearch); return () => window.removeEventListener('keydown', handleSearch); - }, []); + }, [isActive, setSearchPaletteOpen]); }