diff --git a/apps/desktop/src-electron/main/menu.ts b/apps/desktop/src-electron/main/menu.ts index 2fea0f26..cb601a90 100644 --- a/apps/desktop/src-electron/main/menu.ts +++ b/apps/desktop/src-electron/main/menu.ts @@ -41,6 +41,7 @@ const nativeMenuCommands = new Set([ "editor:heading-6", "editor:heading-0", "editor:toggle-live-preview", + "developer:new-terminal-tab", "layout:toggle-sidebar", "layout:toggle-right-panel", "nav:command-palette", @@ -180,6 +181,11 @@ function buildApplicationMenu() { submenu: [ commandItem("vault:new-note", "New Note", "CommandOrControl+N"), commandItem("editor:new-tab", "New Tab", "CommandOrControl+T"), + commandItem( + "developer:new-terminal-tab", + "New Terminal", + "CommandOrControl+R", + ), commandItem("vault:open", "Open Vault...", "Shift+CommandOrControl+O"), separator(), commandItem("editor:close-tab", "Close Tab", "CommandOrControl+W"), diff --git a/apps/desktop/src/App.noteWindow.test.tsx b/apps/desktop/src/App.noteWindow.test.tsx index f289f35f..5d03e3e6 100644 --- a/apps/desktop/src/App.noteWindow.test.tsx +++ b/apps/desktop/src/App.noteWindow.test.tsx @@ -13,6 +13,7 @@ import { useCommandStore } from "./features/command-palette/store/commandStore"; import { isTerminalTab, useEditorStore } from "./app/store/editorStore"; import { useSettingsStore } from "./app/store/settingsStore"; import { useVaultStore } from "./app/store/vaultStore"; +import { getDesktopPlatform } from "./app/utils/platform"; import { resetTerminalRuntimeStoreForTests, useTerminalRuntimeStore, @@ -209,6 +210,69 @@ describe("App note window", () => { expect(activeTab && isTerminalTab(activeTab)).toBe(true); }); + it("opens workspace terminals from the developer terminal shortcut", async () => { + detachedWindowMock.label = "main"; + detachedWindowMock.mode = "main"; + window.history.replaceState({}, "", "/"); + useSettingsStore.setState({ + developerModeEnabled: true, + developerTerminalEnabled: true, + }); + + renderComponent(); + await flushPromises(); + + const platform = getDesktopPlatform(); + + await act(async () => { + window.dispatchEvent( + new KeyboardEvent("keydown", { + key: "r", + metaKey: platform === "macos", + ctrlKey: platform !== "macos", + }), + ); + await Promise.resolve(); + }); + await flushPromises(); + + const activeTab = useEditorStore + .getState() + .tabs.find( + (tab) => tab.id === useEditorStore.getState().activeTabId, + ); + expect(activeTab && isTerminalTab(activeTab)).toBe(true); + }); + + it("does not open workspace terminals from the shortcut when disabled", async () => { + detachedWindowMock.label = "main"; + detachedWindowMock.mode = "main"; + window.history.replaceState({}, "", "/"); + useSettingsStore.setState({ + developerModeEnabled: true, + developerTerminalEnabled: false, + }); + + renderComponent(); + await flushPromises(); + + const platform = getDesktopPlatform(); + + await act(async () => { + window.dispatchEvent( + new KeyboardEvent("keydown", { + key: "r", + metaKey: platform === "macos", + ctrlKey: platform !== "macos", + }), + ); + await Promise.resolve(); + }); + await flushPromises(); + + expect(useEditorStore.getState().tabs.some(isTerminalTab)).toBe(false); + }); + it("starts workspace terminal runtimes inside detached note windows", async () => { mockInvoke().mockResolvedValue({ sessionId: "devterm-note-1", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 311eca0c..3b13b876 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -550,6 +550,7 @@ function useRegisterCommands( const openVaultShortcut = getShortcutDefinition("open_vault"); const newNoteShortcut = getShortcutDefinition("new_note"); const newAgentShortcut = getShortcutDefinition("new_agent"); + const newTerminalShortcut = getShortcutDefinition("new_terminal"); const closeTabShortcut = getShortcutDefinition("close_tab"); const newTabShortcut = getShortcutDefinition("new_tab"); const reopenClosedTabShortcut = @@ -934,8 +935,9 @@ function useRegisterCommands( register({ id: "developer:new-terminal-tab", - label: "New Terminal", - category: "Developer", + label: newTerminalShortcut.label, + shortcut: formatShortcutAction(newTerminalShortcut.id, platform), + category: newTerminalShortcut.category, when: developerModeEnabled, execute: () => { useEditorStore.getState().openTerminal(); @@ -1042,6 +1044,12 @@ function useGlobalShortcuts(openSettings: () => void) { return; } + if (matchesShortcutAction(e, "new_terminal", platform)) { + e.preventDefault(); + useCommandStore.getState().execute("developer:new-terminal-tab"); + return; + } + if (matchesShortcutAction(e, "close_tab", platform)) { e.preventDefault(); useCommandStore.getState().execute("editor:close-tab"); diff --git a/apps/desktop/src/app/shortcuts/format.test.ts b/apps/desktop/src/app/shortcuts/format.test.ts index 25bec7fc..89102c20 100644 --- a/apps/desktop/src/app/shortcuts/format.test.ts +++ b/apps/desktop/src/app/shortcuts/format.test.ts @@ -19,6 +19,8 @@ describe("shortcut registry formatting", () => { expect(formatShortcutAction("new_agent", "windows")).toBe( "Ctrl+Shift+N", ); + expect(formatShortcutAction("new_terminal", "macos")).toBe("⌘R"); + expect(formatShortcutAction("new_terminal", "windows")).toBe("Ctrl+R"); expect(formatShortcutAction("zoom_in", "macos")).toBe("⌘="); expect(formatShortcutAction("zoom_out", "windows")).toBe("Ctrl+-"); expect(formatShortcutAction("reset_zoom", "macos")).toBe("⌘0"); @@ -59,6 +61,13 @@ describe("shortcut registry formatting", () => { shortcut: "Ctrl+Shift+N", }, ); + expect( + entries.find((entry) => entry.id === "new_terminal"), + ).toMatchObject({ + label: "New Terminal", + category: "Developer", + shortcut: "Ctrl+R", + }); expect(entries.find((entry) => entry.id === "zoom_in")).toMatchObject({ label: "Zoom In", category: "View", @@ -242,6 +251,24 @@ describe("shortcut registry matching", () => { ).toBe(true); }); + it("matches new terminal on both platforms", () => { + const macEvent = new KeyboardEvent("keydown", { + key: "R", + metaKey: true, + }); + const windowsEvent = new KeyboardEvent("keydown", { + key: "r", + ctrlKey: true, + }); + + expect(matchesShortcutAction(macEvent, "new_terminal", "macos")).toBe( + true, + ); + expect( + matchesShortcutAction(windowsEvent, "new_terminal", "windows"), + ).toBe(true); + }); + it("matches zoom in aliases on both platforms", () => { const macEvent = new KeyboardEvent("keydown", { key: "+", diff --git a/apps/desktop/src/app/shortcuts/registry.ts b/apps/desktop/src/app/shortcuts/registry.ts index 0eec7834..f409fe2a 100644 --- a/apps/desktop/src/app/shortcuts/registry.ts +++ b/apps/desktop/src/app/shortcuts/registry.ts @@ -15,6 +15,7 @@ export type ShortcutActionId = | "open_vault" | "new_note" | "new_agent" + | "new_terminal" | "new_tab" | "close_tab" | "reopen_closed_tab" @@ -121,6 +122,15 @@ const shortcutDefinitions = [ windows: [{ key: "n", modifiers: ["ctrl", "shift"] }], }, }, + { + id: "new_terminal", + label: "New Terminal", + category: "Developer", + bindings: { + macos: [{ key: "r", modifiers: ["meta"] }], + windows: [{ key: "r", modifiers: ["ctrl"] }], + }, + }, { id: "new_tab", label: "New Tab", @@ -408,6 +418,7 @@ export const SHORTCUT_SETTINGS_ORDER: ShortcutActionId[] = [ "open_vault", "new_note", "new_agent", + "new_terminal", "new_tab", "reopen_closed_tab", "find_in_note",