Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion apps/twig/src/renderer/components/GlobalEventHandlers.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore";
import { useRightSidebarStore } from "@features/right-sidebar";
import { usePinnedTasksStore } from "@features/sidebar/stores/pinnedTasksStore";
import { useSidebarStore } from "@features/sidebar/stores/sidebarStore";
import { useTasks } from "@features/tasks/hooks/useTasks";
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts";
import { clearApplicationStorage } from "@renderer/lib/clearStorage";
import { useRegisteredFoldersStore } from "@renderer/stores/registeredFoldersStore";
import type { Task } from "@shared/types";
import { useNavigationStore } from "@stores/navigationStore";
import { useCallback, useEffect } from "react";
import { useCallback, useEffect, useMemo } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { trpcReact } from "@/renderer/trpc";

Expand All @@ -25,6 +28,7 @@ export function GlobalEventHandlers({
const navigateToTaskInput = useNavigationStore(
(state) => state.navigateToTaskInput,
);
const navigateToTask = useNavigationStore((state) => state.navigateToTask);
const navigateToFolderSettings = useNavigationStore(
(state) => state.navigateToFolderSettings,
);
Expand All @@ -37,6 +41,40 @@ export function GlobalEventHandlers({
const toggleLeftSidebar = useSidebarStore((state) => state.toggle);
const toggleRightSidebar = useRightSidebarStore((state) => state.toggle);

const { data: allTasks = [] } = useTasks();
const pinnedTaskIds = usePinnedTasksStore((state) => state.pinnedTaskIds);

// Build ordered task list for CMD+0-9 switching (pinned → active → recent)
const orderedTasks = useMemo((): Task[] => {
if (allTasks.length === 0) return [];

const sortedByActivity = [...allTasks].sort(
(a, b) =>
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime(),
);

const pinned = sortedByActivity.filter((t) => pinnedTaskIds.has(t.id));
const unpinned = sortedByActivity.filter((t) => !pinnedTaskIds.has(t.id));

return [...pinned, ...unpinned];
}, [allTasks, pinnedTaskIds]);

const handleSwitchTask = useCallback(
(index: number) => {
if (index === 0) {
// mod+0 goes to home/task input
navigateToTaskInput();
} else {
// mod+1-9 switches to task at that index (1-based)
const task = orderedTasks[index - 1];
if (task) {
navigateToTask(task);
}
}
},
[orderedTasks, navigateToTask, navigateToTaskInput],
);

const handleOpenSettings = useCallback(() => {
toggleSettings();
}, [toggleSettings]);
Expand Down Expand Up @@ -91,6 +129,21 @@ export function GlobalEventHandlers({
useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions);
useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions);

// Task switching with mod+0-9
useHotkeys(
SHORTCUTS.SWITCH_TASK,
(event, handler) => {
if (event.ctrlKey && !event.metaKey) return;

const keyPressed = handler.keys?.[0];
if (!keyPressed) return;
const index = parseInt(keyPressed, 10);
handleSwitchTask(index);
},
globalOptions,
[handleSwitchTask],
);

// Mouse back/forward buttons
useEffect(() => {
const handleMouseButton = (event: MouseEvent) => {
Expand Down
13 changes: 8 additions & 5 deletions apps/twig/src/renderer/components/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createTrpcClient, trpcReact } from "@renderer/trpc";
import { QueryClientProvider } from "@tanstack/react-query";
import type React from "react";
import { useState } from "react";
import { HotkeysProvider } from "react-hotkeys-hook";

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

return (
<trpcReact.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ThemeWrapper>{children}</ThemeWrapper>
</QueryClientProvider>
</trpcReact.Provider>
<HotkeysProvider>
<trpcReact.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<ThemeWrapper>{children}</ThemeWrapper>
</QueryClientProvider>
</trpcReact.Provider>
</HotkeysProvider>
);
};
11 changes: 9 additions & 2 deletions apps/twig/src/renderer/constants/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export const SHORTCUTS = {
TOGGLE_LEFT_SIDEBAR: "mod+b",
TOGGLE_RIGHT_SIDEBAR: "mod+shift+b",
CLOSE_TAB: "mod+w",
SWITCH_TAB: "mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
SWITCH_TAB: "ctrl+1,ctrl+2,ctrl+3,ctrl+4,ctrl+5,ctrl+6,ctrl+7,ctrl+8,ctrl+9",
SWITCH_TASK: "mod+0,mod+1,mod+2,mod+3,mod+4,mod+5,mod+6,mod+7,mod+8,mod+9",
OPEN_IN_EDITOR: "mod+o",
COPY_PATH: "mod+shift+c",
TASK_REFRESH: "mod+r",
Expand Down Expand Up @@ -57,6 +58,12 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
description: "Show keyboard shortcuts",
category: "general",
},
{
id: "switch-task",
keys: "mod+0-9",
description: "Switch to task 1-9 (0 = home)",
category: "navigation",
},
{
id: "go-back",
keys: SHORTCUTS.GO_BACK,
Expand All @@ -83,7 +90,7 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [
},
{
id: "switch-tab",
keys: "mod+1-9",
keys: "ctrl+1-9",
description: "Switch to tab 1-9",
category: "panels",
context: "Task detail",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,12 @@ export function usePanelKeyboardShortcuts(taskId: string): void {
);
}
},
{ enabled: !!layout, enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"] },
{
enabled: !!layout,
enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"],
enableOnContentEditable: true,
scopes: ["taskDetail"],
},
[taskId],
);

Expand All @@ -59,7 +64,12 @@ export function usePanelKeyboardShortcuts(taskId: string): void {
state.closeTab(taskId, currentFocusedPanelId, activeTab.id);
}
},
{ enabled: !!layout, enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"] },
{
enabled: !!layout,
enableOnFormTags: ["INPUT", "TEXTAREA", "SELECT"],
enableOnContentEditable: true,
scopes: ["taskDetail"],
},
[taskId],
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { useStatusBar } from "@hooks/useStatusBar";
import { GitBranch, Laptop } from "@phosphor-icons/react";
import { Box, Code, Flex, Text, Tooltip } from "@radix-ui/themes";
import type { Task } from "@shared/types";
import { useMemo, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { useEffect, useMemo, useState } from "react";
import { useHotkeys, useHotkeysContext } from "react-hotkeys-hook";
import { useWorkspaceStore } from "@/renderer/features/workspace/stores/workspaceStore";
import { WorktreePathDisplay } from "./WorktreePathDisplay";

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

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

const { enableScope, disableScope } = useHotkeysContext();

useEffect(() => {
enableScope("taskDetail");
return () => {
disableScope("taskDetail");
};
}, [enableScope, disableScope]);

useHotkeys("mod+p", () => setFilePickerOpen(true), {
enableOnContentEditable: true,
enableOnFormTags: true,
Expand Down
Loading