Skip to content

Commit 45455c4

Browse files
charlesvienjonathanlab
authored andcommitted
Implement the ability to quickly switch between tasks with CMD+0-9 keyboard shortcut
1 parent 5d4a514 commit 45455c4

5 files changed

Lines changed: 94 additions & 12 deletions

File tree

apps/twig/src/renderer/components/GlobalEventHandlers.tsx

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
22
import { useRightSidebarStore } from "@features/right-sidebar";
3+
import { usePinnedTasksStore } from "@features/sidebar/stores/pinnedTasksStore";
34
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
5+
import { useTasks } from "@features/tasks/hooks/useTasks";
46
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
57
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
68
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
79
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
10+
import type { Task } from "@shared/types";
811
import { useNavigationStore } from "@stores/navigationStore";
9-
import { useCallback, useEffect } from "react";
12+
import { useCallback, useEffect, useMemo } from "react";
1013
import { useHotkeys } from "react-hotkeys-hook";
1114
import { trpcReact } from "@/renderer/trpc";
1215

@@ -25,6 +28,7 @@ export function GlobalEventHandlers({
2528
const navigateToTaskInput = useNavigationStore(
2629
(state) => state.navigateToTaskInput,
2730
);
31+
const navigateToTask = useNavigationStore((state) => state.navigateToTask);
2832
const navigateToFolderSettings = useNavigationStore(
2933
(state) => state.navigateToFolderSettings,
3034
);
@@ -37,6 +41,40 @@ export function GlobalEventHandlers({
3741
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
3842
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);
3943

44+
const { data: allTasks = [] } = useTasks();
45+
const pinnedTaskIds = usePinnedTasksStore((state) => state.pinnedTaskIds);
46+
47+
// Build ordered task list for CMD+0-9 switching (pinned → active → recent)
48+
const orderedTasks = useMemo((): Task[] => {
49+
if (allTasks.length === 0) return [];
50+
51+
const sortedByActivity = [...allTasks].sort(
52+
(a, b) =>
53+
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
54+
);
55+
56+
const pinned = sortedByActivity.filter((t) => pinnedTaskIds.has(t.id));
57+
const unpinned = sortedByActivity.filter((t) => !pinnedTaskIds.has(t.id));
58+
59+
return [...pinned, ...unpinned];
60+
}, [allTasks, pinnedTaskIds]);
61+
62+
const handleSwitchTask = useCallback(
63+
(index: number) => {
64+
if (index === 0) {
65+
// mod+0 goes to home/task input
66+
navigateToTaskInput();
67+
} else {
68+
// mod+1-9 switches to task at that index (1-based)
69+
const task = orderedTasks[index - 1];
70+
if (task) {
71+
navigateToTask(task);
72+
}
73+
}
74+
},
75+
[orderedTasks, navigateToTask, navigateToTaskInput],
76+
);
77+
4078
const handleOpenSettings = useCallback(() => {
4179
toggleSettings();
4280
}, [toggleSettings]);
@@ -91,6 +129,21 @@ export function GlobalEventHandlers({
91129
useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions);
92130
useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions);
93131

132+
// Task switching with mod+0-9
133+
useHotkeys(
134+
SHORTCUTS.SWITCH_TASK,
135+
(event, handler) => {
136+
if (event.ctrlKey && !event.metaKey) return;
137+
138+
const keyPressed = handler.keys?.[0];
139+
if (!keyPressed) return;
140+
const index = parseInt(keyPressed, 10);
141+
handleSwitchTask(index);
142+
},
143+
globalOptions,
144+
[handleSwitchTask],
145+
);
146+
94147
// Mouse back/forward buttons
95148
useEffect(() => {
96149
const handleMouseButton = (event: MouseEvent) => {

apps/twig/src/renderer/components/Providers.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { createTrpcClient, trpcReact } from "@renderer/trpc";
44
import { QueryClientProvider } from "@tanstack/react-query";
55
import type React from "react";
66
import { useState } from "react";
7+
import { HotkeysProvider } from "react-hotkeys-hook";
78

89
interface ProvidersProps {
910
children: React.ReactNode;
@@ -13,10 +14,12 @@ export const Providers: React.FC<ProvidersProps> = ({ children }) => {
1314
const [trpcClient] = useState(() => createTrpcClient());
1415

1516
return (
16-
<trpcReact.Provider client={trpcClient} queryClient={queryClient}>
17-
<QueryClientProvider client={queryClient}>
18-
<ThemeWrapper>{children}</ThemeWrapper>
19-
</QueryClientProvider>
20-
</trpcReact.Provider>
17+
<HotkeysProvider>
18+
<trpcReact.Provider client={trpcClient} queryClient={queryClient}>
19+
<QueryClientProvider client={queryClient}>
20+
<ThemeWrapper>{children}</ThemeWrapper>
21+
</QueryClientProvider>
22+
</trpcReact.Provider>
23+
</HotkeysProvider>
2124
);
2225
};

apps/twig/src/renderer/constants/keyboard-shortcuts.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export const SHORTCUTS = {
88
TOGGLE_LEFT_SIDEBAR: "mod+b",
99
TOGGLE_RIGHT_SIDEBAR: "mod+shift+b",
1010
CLOSE_TAB: "mod+w",
11-
SWITCH_TAB: "mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
11+
SWITCH_TAB: "ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9",
12+
SWITCH_TASK: "mod+0,mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
1213
OPEN_IN_EDITOR: "mod+o",
1314
COPY_PATH: "mod+shift+c",
1415
TASK_REFRESH: "mod+r",
@@ -57,6 +58,12 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
5758
description: "Show keyboard shortcuts",
5859
category: "general",
5960
},
61+
{
62+
id: "switch-task",
63+
keys: "mod+0-9",
64+
description: "Switch to task 1-9 (0 = home)",
65+
category: "navigation",
66+
},
6067
{
6168
id: "go-back",
6269
keys: SHORTCUTS.GO_BACK,
@@ -83,7 +90,7 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
8390
},
8491
{
8592
id: "switch-tab",
86-
keys: "mod+1-9",
93+
keys: "ctrl+1-9",
8794
description: "Switch to tab 1-9",
8895
category: "panels",
8996
context: "Task detail",

apps/twig/src/renderer/features/panels/hooks/usePanelKeyboardShortcuts.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ export function usePanelKeyboardShortcuts(taskId: string): void {
3232
);
3333
}
3434
},
35-
{ enabled: !!layout, enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"] },
35+
{
36+
enabled: !!layout,
37+
enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"],
38+
enableOnContentEditable: true,
39+
scopes: ["taskDetail"],
40+
},
3641
[taskId],
3742
);
3843

@@ -59,7 +64,12 @@ export function usePanelKeyboardShortcuts(taskId: string): void {
5964
state.closeTab(taskId, currentFocusedPanelId, activeTab.id);
6065
}
6166
},
62-
{ enabled: !!layout, enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"] },
67+
{
68+
enabled: !!layout,
69+
enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"],
70+
enableOnContentEditable: true,
71+
scopes: ["taskDetail"],
72+
},
6373
[taskId],
6474
);
6575
}

apps/twig/src/renderer/features/task-detail/components/TaskDetail.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { useStatusBar } from "@hooks/useStatusBar";
1313
import { GitBranch, Laptop } from "@phosphor-icons/react";
1414
import { Box, Code, Flex, Text, Tooltip } from "@radix-ui/themes";
1515
import type { Task } from "@shared/types";
16-
import { useMemo, useState } from "react";
17-
import { useHotkeys } from "react-hotkeys-hook";
16+
import { useEffect, useMemo, useState } from "react";
17+
import { useHotkeys, useHotkeysContext } from "react-hotkeys-hook";
1818
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
1919
import { WorktreePathDisplay } from "./WorktreePathDisplay";
2020

@@ -31,6 +31,15 @@ export function TaskDetail({ task: initialTask }: TaskDetailProps) {
3131

3232
const [filePickerOpen, setFilePickerOpen] = useState(false);
3333

34+
const { enableScope, disableScope } = useHotkeysContext();
35+
36+
useEffect(() => {
37+
enableScope("taskDetail");
38+
return () => {
39+
disableScope("taskDetail");
40+
};
41+
}, [enableScope, disableScope]);
42+
3443
useHotkeys("mod+p", () => setFilePickerOpen(true), {
3544
enableOnContentEditable: true,
3645
enableOnFormTags: true,

0 commit comments

Comments
 (0)