Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions electron/src/lib/__tests__/devtools-shortcuts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { isDevToolsShortcut } from "../devtools-shortcuts";

describe("isDevToolsShortcut", () => {
it("matches F12 on keyDown", () => {
expect(isDevToolsShortcut({ type: "keyDown", key: "F12" }, "darwin")).toBe(true);
expect(isDevToolsShortcut({ type: "keyUp", key: "F12" }, "darwin")).toBe(false);
});

it("matches Cmd+Alt+I on macOS only when meta is pressed", () => {
expect(isDevToolsShortcut({ type: "keyDown", key: "i", meta: true, alt: true }, "darwin")).toBe(true);
expect(isDevToolsShortcut({ type: "keyDown", key: "i", control: true, alt: true }, "darwin")).toBe(false);
});

it("matches Ctrl+Alt+I on non-macOS", () => {
expect(isDevToolsShortcut({ type: "keyDown", key: "I", control: true, alt: true }, "linux")).toBe(true);
expect(isDevToolsShortcut({ type: "keyDown", key: "I", meta: true, alt: true }, "linux")).toBe(false);
});

it("matches Cmd/Ctrl+Shift+J", () => {
expect(isDevToolsShortcut({ type: "keyDown", key: "j", meta: true, shift: true }, "darwin")).toBe(true);
expect(isDevToolsShortcut({ type: "keyDown", key: "j", control: true, shift: true }, "win32")).toBe(true);
});

it("does not match unrelated combos", () => {
expect(isDevToolsShortcut({ type: "keyDown", key: "i", meta: true }, "darwin")).toBe(false);
expect(isDevToolsShortcut({ type: "keyDown", key: "j", meta: true }, "darwin")).toBe(false);
expect(isDevToolsShortcut({ type: "keyDown", key: "k", meta: true, shift: true }, "darwin")).toBe(false);
});
});
27 changes: 27 additions & 0 deletions electron/src/lib/devtools-shortcuts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
type ShortcutInput = {
type?: string;
key?: string;
control?: boolean;
alt?: boolean;
shift?: boolean;
meta?: boolean;
};

/**
* Matches the app's DevTools shortcuts from keyboard input events.
* Scoped to window input handling so it never captures OS-global shortcuts.
*/
export function isDevToolsShortcut(input: ShortcutInput, platform: NodeJS.Platform): boolean {
if (input.type !== "keyDown") return false;

const key = (input.key ?? "").toLowerCase();
if (key === "f12") return true;

const commandOrControl = platform === "darwin" ? !!input.meta : !!input.control;
if (!commandOrControl) return false;

if (input.alt && key === "i") return true;
if (input.shift && key === "j") return true;

return false;
}
20 changes: 9 additions & 11 deletions electron/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execSync } from "child_process";
import { app, BrowserWindow, clipboard, globalShortcut, ipcMain, Menu, session, shell, systemPreferences } from "electron";
import { app, BrowserWindow, clipboard, ipcMain, Menu, session, shell, systemPreferences } from "electron";
import path from "path";
import http from "http";
import contextMenu from "electron-context-menu";
Expand All @@ -25,6 +25,7 @@ import { migrateFromOpenAcpUi } from "./lib/migration";
import { glassEnabled, liquidGlass } from "./lib/glass";
import { initAutoUpdater, getIsInstallingUpdate } from "./lib/updater";
import { initPostHog, shutdownPostHog, reinitPostHog, captureEvent } from "./lib/posthog";
import { isDevToolsShortcut } from "./lib/devtools-shortcuts";
import { sessions } from "./ipc/claude-sessions";
import { acpSessions, getAcpAnalyticsPropertiesForSession } from "./ipc/acp-sessions";
import { terminals } from "./ipc/terminal";
Expand Down Expand Up @@ -149,6 +150,13 @@ function createWindow(): void {
}
});
}

mainWindow.webContents.on("before-input-event", (event, input) => {
if (!isDevToolsShortcut(input, process.platform)) return;
Comment on lines +154 to +155
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle DevTools shortcuts for attached webview contents

This listener is attached only to mainWindow.webContents, so shortcuts are processed only when the main renderer has focus. In this app, the Browser panel uses an Electron <webview> (a separate guest WebContents), and key events in that guest do not route through the host page’s before-input-event handler; as a result, F12 / CmdOrCtrl+Alt+I / CmdOrCtrl+Shift+J stop working while focus is inside the webview, which regresses the prior in-app shortcut behavior. Consider also wiring the same shortcut handler to guest webcontents (e.g., via did-attach-webview / web-contents-created).

Useful? React with 👍 / 👎.

event.preventDefault();
log("DEVTOOLS", `Shortcut ${input.key} triggered`);
openDevToolsWindow();
});
Comment on lines +154 to +159
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

before-input-event will fire repeatedly on key repeat. With the current implementation, holding the shortcut can call openDevToolsWindow() multiple times before the first call finishes (especially in the glassEnabled path where it does an async HTTP fetch), which can result in multiple concurrent requests and potentially multiple DevTools windows. Consider ignoring input.isAutoRepeat and/or adding a re-entrancy/pending-open guard around openDevToolsWindow().

Copilot uses AI. Check for mistakes.
}

// Renderer uses this to decide whether the transparency toggle is available.
Expand Down Expand Up @@ -345,19 +353,9 @@ app.whenReady().then(() => {
app.dock.setIcon(path.join(__dirname, "../../build/icon.png"));
}

const shortcuts = ["CommandOrControl+Alt+I", "F12", "CommandOrControl+Shift+J"];
for (const shortcut of shortcuts) {
const ok = globalShortcut.register(shortcut, () => {
log("DEVTOOLS", `Shortcut ${shortcut} triggered`);
openDevToolsWindow();
});
log("DEVTOOLS", `Register ${shortcut}: ${ok ? "OK" : "FAILED"}`);
}
});

app.on("will-quit", (event) => {
globalShortcut.unregisterAll();

// When an update is being installed, let the updater control the quit lifecycle.
// In that case, fire-and-forget PostHog shutdown and do not delay quit.
if (getIsInstallingUpdate()) {
Expand Down
Loading