Add keyboard shortcut to create a new workspace

Cmd+N (Ctrl+N off mac) navigates to the new-workspace route through a
global handler keyed on the active host. The sidebar header row surfaces
the shortcut hint on hover, replacing the redundant "+" button that
duplicated the action in the Workspaces section header.
This commit is contained in:
Mohamed Boudra
2026-06-16 16:13:03 +07:00
parent e77c8e2bf3
commit 88b48c9d09
17 changed files with 106 additions and 39 deletions
+12 -38
View File
@@ -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}
/>
<SidebarHeaderRow
icon={Clock}
@@ -819,10 +826,7 @@ function MobileSidebar({
variant="compact"
/>
</View>
<WorkspacesSectionHeader
serverId={activeServerId}
onNewWorkspacePress={handleNewWorkspace}
/>
<WorkspacesSectionHeader serverId={activeServerId} />
<Pressable
style={styles.mobileCloseButton}
onPress={closeSidebar}
@@ -904,6 +908,7 @@ function DesktopSidebar({
handleRefresh,
handleHostSelect,
renderHostOption,
newWorkspaceKeys,
handleNewWorkspaceNavigate,
handleOpenProject,
handleHome,
@@ -990,6 +995,7 @@ function DesktopSidebar({
onPress={handleNewWorkspaceNavigate}
testID="sidebar-global-new-workspace"
variant="compact"
shortcutKeys={newWorkspaceKeys}
/>
<SidebarHeaderRow
icon={Clock}
@@ -1001,10 +1007,7 @@ function DesktopSidebar({
/>
</View>
</View>
<WorkspacesSectionHeader
serverId={activeServerId}
onNewWorkspacePress={handleNewWorkspaceNavigate}
/>
<WorkspacesSectionHeader serverId={activeServerId} />
{isInitialLoad ? (
<SidebarAgentListSkeleton />
@@ -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({
<View style={styles.workspacesSectionHeader}>
<Text style={styles.workspacesSectionTitle}>Workspaces</Text>
<View style={styles.workspacesSectionActions}>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Pressable
accessibilityRole="button"
accessibilityLabel="New workspace"
testID="sidebar-new-workspace"
style={searchButtonStyle}
onPress={onNewWorkspacePress}
>
{({ hovered, pressed }) => (
<Plus
size={14}
color={
hovered || pressed ? theme.colors.foreground : theme.colors.foregroundMuted
}
/>
)}
</Pressable>
</TooltipTrigger>
<TooltipContent side="bottom" align="center" offset={8}>
<HeaderIconTooltipContent label="New workspace" />
</TooltipContent>
</Tooltip>
<Tooltip delayDuration={300}>
<TooltipTrigger asChild>
<Pressable
@@ -5,6 +5,8 @@ import type { LucideIcon } from "lucide-react-native";
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
import { ICON_SIZE } from "@/styles/theme";
import type { Theme } from "@/styles/theme";
import { Shortcut } from "@/components/ui/shortcut";
import type { ShortcutKey } from "@/utils/format-shortcut";
const foregroundColorMapping = (theme: Theme) => ({ 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}
/>
<SidebarHeaderRowLabel label={label} isHighlighted={isHighlighted} />
{shortcutKeys && Boolean(state.hovered) ? (
<Shortcut chord={shortcutKeys} style={styles.shortcut} />
) : 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",
},
}));