[eric] shortcuts: Cmd+1..9 focus the Nth dock tile, routed through main so a focused webview cannot eat it

This commit is contained in:
ciregenz
2026-08-06 14:16:44 -07:00
parent a926a08190
commit d1e666ddda
4 changed files with 32 additions and 2 deletions
+13
View File
@@ -2254,6 +2254,18 @@ function routeNewTabShortcut(event, input) {
} catch (_) {}
}
// Cmd/Ctrl+1..9: focus the Nth dock tile, Arc-style. Routed through main so it works from a focused webview too.
function routeDockShortcut(event, input) {
if (input.type !== 'keyDown') return;
if (!(input.meta || input.control) || input.shift || input.alt) return;
const key = input.key || '';
if (key < '1' || key > '9' || key.length !== 1) return;
event.preventDefault();
try {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('openswarm:dock-shortcut', Number(key) - 1);
} catch (_) {}
}
// Cmd/Ctrl+R: the default menu's Reload accelerator reloads the WHOLE app even when a browser webview is focused (the "Ctrl+R reloads OpenSwarm, not the browser" complaint). preventDefault kills that accelerator (same electron#19279 path as Cmd+W, dispatched against whichever webContents is focused, hence both main window AND guests); the renderer then reloads the last-interacted browser, or the app if none. Shift+R (force reload) is left alone.
function routeReloadShortcut(event, input) {
if (input.type !== 'keyDown') return;
@@ -2458,6 +2470,7 @@ app.on('web-contents-created', (_event, contents) => {
contents.on('before-input-event', swallowCloseWindowShortcut);
contents.on('before-input-event', routeReloadShortcut);
contents.on('before-input-event', routeNewTabShortcut);
contents.on('before-input-event', routeDockShortcut);
}
// The main app window (created while this flag is set) gets a text-focused native menu; OAuth
// popups are 'window' contents created with the flag OFF, so they keep the OS default.
+6
View File
@@ -92,6 +92,12 @@ contextBridge.exposeInMainWorld('openswarm', {
revealBundle: (folderPath) => ipcRenderer.invoke('help:reveal-bundle', folderPath),
// Reveal a user-attached file in Finder/Explorer (reveal-only; main checks existence).
revealPath: (filePath) => ipcRenderer.invoke('files:reveal', filePath),
// Cmd/Ctrl+1..9: focus the Nth dock tile (0-based index arrives here).
onDockShortcut: (cb) => {
const listener = (_event, index) => cb(index);
ipcRenderer.on('openswarm:dock-shortcut', listener);
return () => ipcRenderer.removeListener('openswarm:dock-shortcut', listener);
},
// Native OS notification for a finished workflow run, posted by the MAIN process
// so it survives a minimized/hidden/backgrounded renderer (the renderer's own
@@ -2,6 +2,8 @@ import React, { useEffect, type RefObject } from 'react';
import Box from '@mui/material/Box';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { addViewCard, addBrowserTab, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
import { store } from '@/shared/state/store';
import { buildDockEntries } from '../desktop/dockEntries';
import DashboardHeader from './DashboardHeader';
import TetherLayerHost from './TetherLayerHost';
import { useLiveMultiDrag } from '../hooks/interaction/useLiveMultiDrag';
@@ -296,7 +298,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
// (including from focused guests), so these bridges are the only firing path, no double-handling.
const browserHomepage = useAppSelector((st) => st.settings.data.browser_homepage ?? 'https://www.google.com');
React.useEffect(() => {
const w = window as unknown as { openswarm?: { onCloseShortcut?: (cb: () => void) => () => void; onNewTabShortcut?: (cb: () => void) => () => void } };
const w = window as unknown as { openswarm?: { onCloseShortcut?: (cb: () => void) => () => void; onNewTabShortcut?: (cb: () => void) => () => void; onDockShortcut?: (cb: (index: number) => void) => () => void } };
const offs: Array<() => void> = [];
if (w.openswarm?.onCloseShortcut) offs.push(w.openswarm.onCloseShortcut(() => handleDeleteSelected()));
if (w.openswarm?.onNewTabShortcut) {
@@ -306,8 +308,16 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
else onAddBrowser();
}));
}
if (w.openswarm?.onDockShortcut) {
offs.push(w.openswarm.onDockShortcut((index: number) => {
// Same order the dock draws, so Cmd+N matches what the user sees top-to-bottom.
const entries = buildDockEntries({ sessions: store.getState().agents.sessions, cards, viewCards, browserCards, workflowCards, outputs });
const entry = entries[index];
if (entry) handleFocusCard(entry.id, entry.rect);
}));
}
return () => { offs.forEach((off) => off()); };
}, [handleDeleteSelected, browserCards, browserHomepage, dispatch, onAddBrowser]);
}, [handleDeleteSelected, browserCards, browserHomepage, dispatch, onAddBrowser, cards, viewCards, workflowCards, outputs, handleFocusCard]);
// Gestures write the transform imperatively (no React commit per frame), so a foreign render mid-gesture would paint the stale committed transform for a frame. Re-applying live after EVERY render seals that; do not remove.
React.useLayoutEffect(() => {
+1
View File
@@ -102,6 +102,7 @@ declare global {
notify?: (payload: OpenSwarmNotifyRequest) => Promise<boolean>;
onNotificationAction?: (cb: (payload: OpenSwarmNotifyAction) => void) => () => void;
revealPath?: (filePath: string) => Promise<{ ok: boolean }>;
onDockShortcut?: (cb: (index: number) => void) => () => void;
}
interface Window {