diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index e3757c205..d361015e7 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -63,6 +63,7 @@ import { loadDesktopSettings } from "@/desktop/settings/desktop-settings";
import { RosettaCalloutSource } from "@/desktop/updates/rosetta-callout-source";
import { UpdateCalloutSource } from "@/desktop/updates/update-callout-source";
import { useActiveWorktreeNewAction } from "@/hooks/use-active-worktree-new-action";
+import { useGlobalNewWorkspaceAction } from "@/hooks/use-global-new-workspace-action";
import { useFaviconStatus } from "@/hooks/use-favicon-status";
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
import { useCompactWebViewportZoomLock } from "@/hooks/use-compact-web-viewport-zoom-lock";
@@ -461,6 +462,7 @@ function AppContainer({
});
useActiveWorktreeNewAction();
+ useGlobalNewWorkspaceAction();
const content = (
diff --git a/packages/app/src/components/left-sidebar.tsx b/packages/app/src/components/left-sidebar.tsx
index cc88a88ac..b6fe074e7 100644
--- a/packages/app/src/components/left-sidebar.tsx
+++ b/packages/app/src/components/left-sidebar.tsx
@@ -69,6 +69,7 @@ import {
buildSettingsRoute,
mapPathnameToServer,
} from "@/utils/host-routes";
+import type { ShortcutKey } from "@/utils/format-shortcut";
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
import { SidebarCalloutSlot } from "./sidebar-callout-slot";
import { SidebarWorkspaceList } from "./sidebar-workspace-list";
@@ -112,6 +113,7 @@ interface SidebarSharedProps {
active: boolean;
onPress: () => void;
}) => ReactElement;
+ newWorkspaceKeys: ShortcutKey[][] | null;
}
interface SidebarLabels {
@@ -285,6 +287,8 @@ export const LeftSidebar = memo(function LeftSidebar({
[pathname],
);
+ const newWorkspaceKeys = useShortcutKeys("new-workspace");
+
const labels = useMemo(
(): SidebarLabels => ({
addProject: t("sidebar.actions.addProject"),
@@ -320,6 +324,7 @@ export const LeftSidebar = memo(function LeftSidebar({
handleHostSelect,
renderHostOption,
labels,
+ newWorkspaceKeys,
};
if (isCompactLayout) {
@@ -598,6 +603,7 @@ function MobileSidebar({
handleRefresh,
handleHostSelect,
renderHostOption,
+ newWorkspaceKeys,
handleNewWorkspaceNavigate,
handleOpenProject,
handleHome,
@@ -809,6 +815,7 @@ function MobileSidebar({
onPress={handleNewWorkspace}
testID="sidebar-global-new-workspace"
variant="compact"
+ shortcutKeys={newWorkspaceKeys}
/>
-
+
-
+
{isInitialLoad ? (
@@ -1050,13 +1053,7 @@ function DesktopSidebar({
);
}
-function WorkspacesSectionHeader({
- serverId,
- onNewWorkspacePress,
-}: {
- serverId: string | null;
- onNewWorkspacePress: () => void;
-}) {
+function WorkspacesSectionHeader({ serverId }: { serverId: string | null }) {
const { theme } = useUnistyles();
const setCommandCenterOpen = useKeyboardShortcutsStore((state) => state.setCommandCenterOpen);
const commandCenterKeys = useShortcutKeys("toggle-command-center");
@@ -1073,29 +1070,6 @@ function WorkspacesSectionHeader({
Workspaces
-
-
-
- {({ hovered, pressed }) => (
-
- )}
-
-
-
-
-
-
({ color: theme.colors.foreground });
const foregroundMutedColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
@@ -26,6 +28,7 @@ interface SidebarHeaderRowProps {
* sit in a header group whose wrapper owns the single divider.
*/
variant?: SidebarHeaderRowVariant;
+ shortcutKeys?: ShortcutKey[][] | null;
}
export function SidebarHeaderRow({
@@ -37,6 +40,7 @@ export function SidebarHeaderRow({
nativeID,
accessibilityLabel,
variant = "header",
+ shortcutKeys = null,
}: SidebarHeaderRowProps) {
const ThemedIcon = useMemo(() => withUnistyles(Icon), [Icon]);
@@ -63,10 +67,13 @@ export function SidebarHeaderRow({
uniProps={isHighlighted ? foregroundColorMapping : foregroundMutedColorMapping}
/>
+ {shortcutKeys && Boolean(state.hovered) ? (
+
+ ) : null}
>
);
},
- [ThemedIcon, isActive, label],
+ [ThemedIcon, isActive, label, shortcutKeys],
);
return (
@@ -139,4 +146,7 @@ const styles = StyleSheet.create((theme) => ({
labelHighlighted: {
color: theme.colors.foreground,
},
+ shortcut: {
+ marginLeft: "auto",
+ },
}));
diff --git a/packages/app/src/hooks/use-global-new-workspace-action.ts b/packages/app/src/hooks/use-global-new-workspace-action.ts
new file mode 100644
index 000000000..c3500975c
--- /dev/null
+++ b/packages/app/src/hooks/use-global-new-workspace-action.ts
@@ -0,0 +1,29 @@
+import { useCallback } from "react";
+import { router } from "expo-router";
+import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
+import type { KeyboardActionId } from "@/keyboard/keyboard-action-dispatcher";
+import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
+import { buildHostNewWorkspaceRoute } from "@/utils/host-routes";
+
+const WORKSPACE_NEW_ACTIONS: readonly KeyboardActionId[] = ["workspace.new"];
+
+export function useGlobalNewWorkspaceAction() {
+ const selection = useActiveWorkspaceSelection();
+ const serverId = selection?.serverId ?? null;
+
+ const handle = useCallback(() => {
+ if (!serverId) {
+ return false;
+ }
+ router.navigate(buildHostNewWorkspaceRoute(serverId) as never);
+ return true;
+ }, [serverId]);
+
+ useKeyboardActionHandler({
+ handlerId: "workspace-new-global",
+ actions: WORKSPACE_NEW_ACTIONS,
+ enabled: serverId !== null,
+ priority: 0,
+ handle,
+ });
+}
diff --git a/packages/app/src/i18n/resources.test.ts b/packages/app/src/i18n/resources.test.ts
index e07732cdc..8ee055362 100644
--- a/packages/app/src/i18n/resources.test.ts
+++ b/packages/app/src/i18n/resources.test.ts
@@ -299,6 +299,7 @@ describe("translation resources", () => {
expect(en.settings.shortcuts.dialogTitle).toBe("Shortcuts");
expect(en.settings.shortcuts.sections.tabsPanes).toBe("Tabs & Panes");
expect(en.settings.shortcuts.help.toggleCommandCenter).toBe("Toggle command center");
+ expect(en.settings.shortcuts.help.newWorkspace).toBe("New workspace");
expect(en.settings.shortcuts.helpNotes.showKeyboardShortcuts).toBe(
"Available when focus is not in a text field or terminal.",
);
diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts
index d317596d3..9e0a58043 100644
--- a/packages/app/src/i18n/resources/ar.ts
+++ b/packages/app/src/i18n/resources/ar.ts
@@ -1528,6 +1528,7 @@ export const ar: TranslationResources = {
},
help: {
openProject: "مشروع مفتوح",
+ newWorkspace: "مساحة عمل جديدة",
newWorktree: "شجرة عمل جديدة",
archiveWorktree: "أرشفة شجرة العمل",
newTab: "علامة تبويب جديدة",
diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts
index 4ea3bf0ff..0282b2425 100644
--- a/packages/app/src/i18n/resources/en.ts
+++ b/packages/app/src/i18n/resources/en.ts
@@ -1533,6 +1533,7 @@ export const en = {
},
help: {
openProject: "Open project",
+ newWorkspace: "New workspace",
newWorktree: "New worktree",
archiveWorktree: "Archive worktree",
newTab: "New tab",
diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts
index 0a9864ddf..f4978c73c 100644
--- a/packages/app/src/i18n/resources/es.ts
+++ b/packages/app/src/i18n/resources/es.ts
@@ -1565,6 +1565,7 @@ export const es: TranslationResources = {
},
help: {
openProject: "Abrir proyecto",
+ newWorkspace: "Nuevo espacio de trabajo",
newWorktree: "Nuevo árbol de trabajo",
archiveWorktree: "Árbol de trabajo de archivo",
newTab: "Nueva pestaña",
diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts
index 9990a6835..ff8c3383c 100644
--- a/packages/app/src/i18n/resources/fr.ts
+++ b/packages/app/src/i18n/resources/fr.ts
@@ -1569,6 +1569,7 @@ export const fr: TranslationResources = {
},
help: {
openProject: "Projet ouvert",
+ newWorkspace: "Nouvel espace de travail",
newWorktree: "Nouvel arbre de travail",
archiveWorktree: "Arbre de travail d'archivage",
newTab: "Nouvel onglet",
diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts
index ebf2319fd..fcba2642d 100644
--- a/packages/app/src/i18n/resources/ru.ts
+++ b/packages/app/src/i18n/resources/ru.ts
@@ -1557,6 +1557,7 @@ export const ru: TranslationResources = {
},
help: {
openProject: "Открыть проект",
+ newWorkspace: "Новое рабочее пространство",
newWorktree: "Новое рабочее дерево",
archiveWorktree: "Архив рабочего дерева",
newTab: "Новая вкладка",
diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts
index d9dab7b5c..98be203d5 100644
--- a/packages/app/src/i18n/resources/zh-CN.ts
+++ b/packages/app/src/i18n/resources/zh-CN.ts
@@ -1509,6 +1509,7 @@ export const zhCN: TranslationResources = {
},
help: {
openProject: "打开项目",
+ newWorkspace: "新建 workspace",
newWorktree: "新建 worktree",
archiveWorktree: "归档 worktree",
newTab: "新建标签",
diff --git a/packages/app/src/keyboard/actions.ts b/packages/app/src/keyboard/actions.ts
index 27024310c..f17e24797 100644
--- a/packages/app/src/keyboard/actions.ts
+++ b/packages/app/src/keyboard/actions.ts
@@ -41,6 +41,7 @@ export type KeyboardActionId =
| "command-center.toggle"
| "shortcuts.dialog.toggle"
| "workspace.terminal.new"
+ | "workspace.new"
| "worktree.new"
| "worktree.archive"
| "view.toggle.focus"
diff --git a/packages/app/src/keyboard/keyboard-action-dispatcher.ts b/packages/app/src/keyboard/keyboard-action-dispatcher.ts
index 0adc733e4..4ae487d99 100644
--- a/packages/app/src/keyboard/keyboard-action-dispatcher.ts
+++ b/packages/app/src/keyboard/keyboard-action-dispatcher.ts
@@ -26,6 +26,7 @@ export type KeyboardActionId =
| "workspace.pane.close"
| "workspace.terminal.new"
| "sidebar.toggle.right"
+ | "workspace.new"
| "worktree.new"
| "worktree.archive";
@@ -55,6 +56,7 @@ export type KeyboardActionDefinition =
| { id: "workspace.pane.close"; scope: KeyboardActionScope }
| { id: "workspace.terminal.new"; scope: KeyboardActionScope }
| { id: "sidebar.toggle.right"; scope: KeyboardActionScope }
+ | { id: "workspace.new"; scope: KeyboardActionScope }
| { id: "worktree.new"; scope: KeyboardActionScope }
| { id: "worktree.archive"; scope: KeyboardActionScope };
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
index de2bef18c..a797aea9b 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts
@@ -128,6 +128,18 @@ describe("keyboard-shortcuts", () => {
context: { isMac: true },
action: "agent.new",
},
+ {
+ name: "matches Cmd+N to create new workspace on mac",
+ event: { key: "n", code: "KeyN", metaKey: true },
+ context: { isMac: true, commandCenterOpen: false },
+ action: "workspace.new",
+ },
+ {
+ name: "matches Ctrl+N to create new workspace on non-mac",
+ event: { key: "n", code: "KeyN", ctrlKey: true },
+ context: { isMac: false, commandCenterOpen: false, focusScope: "other" },
+ action: "workspace.new",
+ },
{
name: "matches question-mark shortcut to toggle the shortcuts dialog",
event: { key: "?", code: "Slash", shiftKey: true },
@@ -564,6 +576,7 @@ describe("keyboard-shortcut help sections", () => {
context: { isMac: true, isDesktop: true },
expectedKeys: {
"new-agent": ["mod", "shift", "O"],
+ "new-workspace": ["mod", "N"],
"workspace-tab-new": ["mod", "T"],
"workspace-jump-index": ["mod", "1-9"],
"workspace-tab-jump-index": ["mod", "alt", "1-9"],
diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts
index 1ac31e8e9..d3fbbd088 100644
--- a/packages/app/src/keyboard/keyboard-shortcuts.ts
+++ b/packages/app/src/keyboard/keyboard-shortcuts.ts
@@ -120,6 +120,7 @@ const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record = {
const SHORTCUT_HELP_LABEL_KEYS: Record = {
"new-agent": "settings.shortcuts.help.openProject",
+ "new-workspace": "settings.shortcuts.help.newWorkspace",
"new-worktree": "settings.shortcuts.help.newWorktree",
"archive-worktree": "settings.shortcuts.help.archiveWorktree",
"workspace-tab-new": "settings.shortcuts.help.newTab",
@@ -190,6 +191,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [
},
},
+ // --- New workspace ---
+ {
+ id: "workspace-new-cmd-n-mac",
+ action: "workspace.new",
+ combo: "Cmd+N",
+ when: { mac: true, commandCenter: false },
+ help: {
+ id: "new-workspace",
+ section: "projects",
+ label: "New workspace",
+ keys: ["mod", "N"],
+ },
+ },
+ {
+ id: "workspace-new-ctrl-n-non-mac",
+ action: "workspace.new",
+ combo: "Ctrl+N",
+ when: { mac: false, commandCenter: false, terminal: false },
+ help: {
+ id: "new-workspace",
+ section: "projects",
+ label: "New workspace",
+ keys: ["mod", "N"],
+ },
+ },
+
// --- New worktree ---
{
id: "worktree-new-cmd-o-mac",
diff --git a/packages/app/src/keyboard/route-shortcut.test.ts b/packages/app/src/keyboard/route-shortcut.test.ts
index 9e40fd52d..ea569c264 100644
--- a/packages/app/src/keyboard/route-shortcut.test.ts
+++ b/packages/app/src/keyboard/route-shortcut.test.ts
@@ -29,6 +29,7 @@ describe("routeKeyboardShortcut — dispatch passthroughs", () => {
it.each([
["agent.interrupt", { id: "agent.interrupt", scope: "global" }],
["workspace.tab.new", { id: "workspace.tab.new", scope: "workspace" }],
+ ["workspace.new", { id: "workspace.new", scope: "sidebar" }],
["worktree.archive", { id: "worktree.archive", scope: "sidebar" }],
["worktree.new", { id: "worktree.new", scope: "sidebar" }],
["workspace.terminal.new", { id: "workspace.terminal.new", scope: "workspace" }],
diff --git a/packages/app/src/keyboard/route-shortcut.ts b/packages/app/src/keyboard/route-shortcut.ts
index fa1699f0b..03873a45b 100644
--- a/packages/app/src/keyboard/route-shortcut.ts
+++ b/packages/app/src/keyboard/route-shortcut.ts
@@ -45,6 +45,7 @@ const NONE: ShortcutAction = { kind: "none" };
const PASSTHROUGH_DISPATCH: Record = {
"agent.interrupt": { id: "agent.interrupt", scope: "global" },
"workspace.tab.new": { id: "workspace.tab.new", scope: "workspace" },
+ "workspace.new": { id: "workspace.new", scope: "sidebar" },
"worktree.archive": { id: "worktree.archive", scope: "sidebar" },
"worktree.new": { id: "worktree.new", scope: "sidebar" },
"workspace.terminal.new": { id: "workspace.terminal.new", scope: "workspace" },