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",