[eric] shortcuts: Cmd+T new tab or browser, Cmd+W closes the focused card undoably, routed through main like Cmd+R

This commit is contained in:
ciregenz
2026-08-06 12:34:29 -07:00
parent 9d0562db22
commit 19926b5dea
4 changed files with 53 additions and 1 deletions
+18
View File
@@ -2234,9 +2234,26 @@ function swallowCloseWindowShortcut(event, input) {
(input.key || '').toLowerCase() === 'w'
) {
event.preventDefault();
// Arc semantics: the swallowed close becomes "close the focused card" in the renderer (undoable via Cmd+Z).
if (!input.shift) {
try {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('openswarm:close-shortcut');
} catch (_) {}
}
}
}
// Cmd/Ctrl+T: new tab in the last-interacted browser, or a new browser card (Arc muscle memory).
function routeNewTabShortcut(event, input) {
if (input.type !== 'keyDown') return;
if (!(input.meta || input.control) || input.shift || input.alt) return;
if ((input.key || '').toLowerCase() !== 't') return;
event.preventDefault();
try {
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('openswarm:newtab-shortcut');
} 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;
@@ -2398,6 +2415,7 @@ app.on('web-contents-created', (_event, contents) => {
if (isCreatingMainWindow || contents.getType() === 'webview') {
contents.on('before-input-event', swallowCloseWindowShortcut);
contents.on('before-input-event', routeReloadShortcut);
contents.on('before-input-event', routeNewTabShortcut);
}
// 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.
+14
View File
@@ -188,6 +188,20 @@ contextBridge.exposeInMainWorld('openswarm', {
return () => ipcRenderer.removeListener('openswarm:reload-shortcut', listener);
},
// Cmd/Ctrl+W with the window-close swallowed in main: the renderer closes the focused card instead.
onCloseShortcut: (cb) => {
const listener = () => cb();
ipcRenderer.on('openswarm:close-shortcut', listener);
return () => ipcRenderer.removeListener('openswarm:close-shortcut', listener);
},
// Cmd/Ctrl+T: new tab in the last-interacted browser, else a new browser card.
onNewTabShortcut: (cb) => {
const listener = () => cb();
ipcRenderer.on('openswarm:newtab-shortcut', listener);
return () => ipcRenderer.removeListener('openswarm:newtab-shortcut', listener);
},
// In-page browser shortcuts (zoom/find/tab-cycle) from a focused guest webview, carrying the guest's webContents id so the renderer targets that exact browser.
onBrowserShortcut: (cb) => {
const listener = (_event, payload) => cb(payload);
@@ -1,7 +1,7 @@
import React, { useEffect, type RefObject } from 'react';
import Box from '@mui/material/Box';
import { useAppDispatch, useAppSelector } from '@/shared/hooks';
import { addViewCard, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
import { addViewCard, addBrowserTab, clearTiledCard, toggleMinimizeCard, selectFullscreenCardId } from '@/shared/state/dashboardLayoutSlice';
import DashboardHeader from './DashboardHeader';
import TetherLayerHost from './TetherLayerHost';
import { useLiveMultiDrag } from '../hooks/interaction/useLiveMultiDrag';
@@ -33,6 +33,7 @@ import type { CardType, useDashboardSelection } from '../hooks/state/useDashboar
import type { useCanvasControls } from '../hooks/interaction/useCanvasControls';
import { useWebviewSuspend } from '../hooks/interaction/useWebviewSuspend';
import { deleteSelectedCards } from '../hooks/interaction/deleteSelectedCards';
import { getLastInteractedBrowser } from '@/shared/browserFocus';
import type { TetherInputs } from '../geometry/dashboardTethers';
type Selection = ReturnType<typeof useDashboardSelection>;
@@ -291,6 +292,23 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
if (newestDeletable) deleteSelectedCards(new Map([[newestDeletable.id, newestDeletable.type]]), dispatch);
}, [selection, dispatch, newestDeletable]);
// Cmd/Ctrl+W and Cmd/Ctrl+T arrive as IPC echoes: main preventDefaults both before any DOM keydown
// (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 offs: Array<() => void> = [];
if (w.openswarm?.onCloseShortcut) offs.push(w.openswarm.onCloseShortcut(() => handleDeleteSelected()));
if (w.openswarm?.onNewTabShortcut) {
offs.push(w.openswarm.onNewTabShortcut(() => {
const browserId = getLastInteractedBrowser();
if (browserId && browserCards[browserId]) dispatch(addBrowserTab({ browserId, url: browserHomepage, makeActive: true }));
else onAddBrowser();
}));
}
return () => { offs.forEach((off) => off()); };
}, [handleDeleteSelected, browserCards, browserHomepage, dispatch, onAddBrowser]);
// 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(() => {
canvas.actions.syncTransform();
+2
View File
@@ -76,6 +76,8 @@ declare global {
onUpdateError: (cb: (message: string) => void) => () => void;
onWebviewNewWindow: (cb: (url: string, webContentsId: number, disposition?: string) => void) => () => void;
onReloadShortcut?: (cb: () => void) => () => void;
onCloseShortcut?: (cb: () => void) => () => void;
onNewTabShortcut?: (cb: () => void) => () => void;
onBrowserShortcut?: (cb: (payload: { action: string; webContentsId: number }) => void) => () => void;
openExternal: (url: string) => Promise<void>;
harvestUsage?: (provider: string) => Promise<{ ok: boolean; total: number; titles: string[]; memories: string[] } | null>;