From 8d4b6fcf2182f5356ebdfa901d516e5c1a49290d Mon Sep 17 00:00:00 2001 From: Sword <55425346+shaw-baobao@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:52:34 +0800 Subject: [PATCH 1/2] feat: add Ctrl+C interrupt shortcut --- src/App.tsx | 9 +++++ .../app/hooks/useInterruptShortcut.ts | 39 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/features/app/hooks/useInterruptShortcut.ts diff --git a/src/App.tsx b/src/App.tsx index 963f5b09b..d7c548625 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -77,6 +77,7 @@ import { useAppMenuEvents } from "./features/app/hooks/useAppMenuEvents"; import { useWorkspaceActions } from "./features/app/hooks/useWorkspaceActions"; import { useWorkspaceCycling } from "./features/app/hooks/useWorkspaceCycling"; import { useThreadRows } from "./features/app/hooks/useThreadRows"; +import { useInterruptShortcut } from "./features/app/hooks/useInterruptShortcut"; import { useCopyThread } from "./features/threads/hooks/useCopyThread"; import { useTerminalController } from "./features/terminal/hooks/useTerminalController"; import { useGitCommitController } from "./features/app/hooks/useGitCommitController"; @@ -1089,6 +1090,14 @@ function MainApp() { onDropPaths: handleDropWorkspacePaths, }); + useInterruptShortcut({ + isEnabled: canInterrupt, + shortcut: appSettings.interruptShortcut, + onTrigger: () => { + void interruptTurn(); + }, + }); + const { handleSelectPullRequest, resetPullRequestSelection, diff --git a/src/features/app/hooks/useInterruptShortcut.ts b/src/features/app/hooks/useInterruptShortcut.ts new file mode 100644 index 000000000..093a58285 --- /dev/null +++ b/src/features/app/hooks/useInterruptShortcut.ts @@ -0,0 +1,39 @@ +import { useEffect, useMemo } from "react"; + +type UseInterruptShortcutOptions = { + isEnabled: boolean; + onTrigger: () => void | Promise; +}; + +export function useInterruptShortcut({ + isEnabled, + onTrigger, +}: UseInterruptShortcutOptions) { + const isMac = useMemo(() => { + if (typeof navigator === "undefined") { + return false; + } + return /Mac|iPhone|iPad|iPod/.test(navigator.platform); + }, []); + + useEffect(() => { + if (!isEnabled || !isMac) { + return; + } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.repeat) { + return; + } + if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) { + return; + } + if (event.key.toLowerCase() !== "c") { + return; + } + event.preventDefault(); + void onTrigger(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isEnabled, isMac, onTrigger]); +} From 1074989cba4813a22612cd91f4571d831f08a958 Mon Sep 17 00:00:00 2001 From: Sword <55425346+shaw-baobao@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:28:18 +0800 Subject: [PATCH 2/2] feat: add configurable stop shortcut --- src-tauri/src/types.rs | 8 +++++ .../app/hooks/useInterruptShortcut.ts | 21 +++++-------- .../settings/components/SettingsView.test.tsx | 1 + .../settings/components/SettingsView.tsx | 30 +++++++++++++++++++ src/features/settings/hooks/useAppSettings.ts | 1 + src/types.ts | 1 + 6 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index e07d3e6bf..9968a0ff1 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -267,6 +267,8 @@ pub(crate) struct AppSettings { rename = "composerReasoningShortcut" )] pub(crate) composer_reasoning_shortcut: Option, + #[serde(default = "default_interrupt_shortcut", rename = "interruptShortcut")] + pub(crate) interrupt_shortcut: Option, #[serde(default = "default_new_agent_shortcut", rename = "newAgentShortcut")] pub(crate) new_agent_shortcut: Option, #[serde( @@ -425,6 +427,10 @@ fn default_composer_reasoning_shortcut() -> Option { Some("cmd+shift+r".to_string()) } +fn default_interrupt_shortcut() -> Option { + Some("ctrl+c".to_string()) +} + fn default_new_agent_shortcut() -> Option { Some("cmd+n".to_string()) } @@ -512,6 +518,7 @@ impl Default for AppSettings { composer_model_shortcut: default_composer_model_shortcut(), composer_access_shortcut: default_composer_access_shortcut(), composer_reasoning_shortcut: default_composer_reasoning_shortcut(), + interrupt_shortcut: default_interrupt_shortcut(), new_agent_shortcut: default_new_agent_shortcut(), new_worktree_agent_shortcut: default_new_worktree_agent_shortcut(), new_clone_agent_shortcut: default_new_clone_agent_shortcut(), @@ -569,6 +576,7 @@ mod tests { settings.composer_reasoning_shortcut.as_deref(), Some("cmd+shift+r") ); + assert_eq!(settings.interrupt_shortcut.as_deref(), Some("ctrl+c")); assert_eq!( settings.toggle_debug_panel_shortcut.as_deref(), Some("cmd+shift+d") diff --git a/src/features/app/hooks/useInterruptShortcut.ts b/src/features/app/hooks/useInterruptShortcut.ts index 093a58285..5255f8a21 100644 --- a/src/features/app/hooks/useInterruptShortcut.ts +++ b/src/features/app/hooks/useInterruptShortcut.ts @@ -1,33 +1,26 @@ -import { useEffect, useMemo } from "react"; +import { useEffect } from "react"; +import { matchesShortcut } from "../../../utils/shortcuts"; type UseInterruptShortcutOptions = { isEnabled: boolean; + shortcut: string | null; onTrigger: () => void | Promise; }; export function useInterruptShortcut({ isEnabled, + shortcut, onTrigger, }: UseInterruptShortcutOptions) { - const isMac = useMemo(() => { - if (typeof navigator === "undefined") { - return false; - } - return /Mac|iPhone|iPad|iPod/.test(navigator.platform); - }, []); - useEffect(() => { - if (!isEnabled || !isMac) { + if (!isEnabled || !shortcut) { return; } const handleKeyDown = (event: KeyboardEvent) => { if (event.repeat) { return; } - if (!event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) { - return; - } - if (event.key.toLowerCase() !== "c") { + if (!matchesShortcut(event, shortcut)) { return; } event.preventDefault(); @@ -35,5 +28,5 @@ export function useInterruptShortcut({ }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); - }, [isEnabled, isMac, onTrigger]); + }, [isEnabled, onTrigger, shortcut]); } diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index b93bba907..7dc740340 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -26,6 +26,7 @@ const baseSettings: AppSettings = { composerModelShortcut: null, composerAccessShortcut: null, composerReasoningShortcut: null, + interruptShortcut: null, newAgentShortcut: null, newWorktreeAgentShortcut: null, newCloneAgentShortcut: null, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index 89d27ef48..1929e7093 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -80,6 +80,7 @@ type ShortcutSettingKey = | "composerModelShortcut" | "composerAccessShortcut" | "composerReasoningShortcut" + | "interruptShortcut" | "newAgentShortcut" | "newWorktreeAgentShortcut" | "newCloneAgentShortcut" @@ -95,6 +96,7 @@ type ShortcutDraftKey = | "model" | "access" | "reasoning" + | "interrupt" | "newAgent" | "newWorktreeAgent" | "newCloneAgent" @@ -111,6 +113,7 @@ const shortcutDraftKeyBySetting: Record = composerModelShortcut: "model", composerAccessShortcut: "access", composerReasoningShortcut: "reasoning", + interruptShortcut: "interrupt", newAgentShortcut: "newAgent", newWorktreeAgentShortcut: "newWorktreeAgent", newCloneAgentShortcut: "newCloneAgent", @@ -176,6 +179,7 @@ export function SettingsView({ model: appSettings.composerModelShortcut ?? "", access: appSettings.composerAccessShortcut ?? "", reasoning: appSettings.composerReasoningShortcut ?? "", + interrupt: appSettings.interruptShortcut ?? "", newAgent: appSettings.newAgentShortcut ?? "", newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "", newCloneAgent: appSettings.newCloneAgentShortcut ?? "", @@ -236,6 +240,7 @@ export function SettingsView({ model: appSettings.composerModelShortcut ?? "", access: appSettings.composerAccessShortcut ?? "", reasoning: appSettings.composerReasoningShortcut ?? "", + interrupt: appSettings.interruptShortcut ?? "", newAgent: appSettings.newAgentShortcut ?? "", newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "", newCloneAgent: appSettings.newCloneAgentShortcut ?? "", @@ -252,6 +257,7 @@ export function SettingsView({ appSettings.composerAccessShortcut, appSettings.composerModelShortcut, appSettings.composerReasoningShortcut, + appSettings.interruptShortcut, appSettings.newAgentShortcut, appSettings.newWorktreeAgentShortcut, appSettings.newCloneAgentShortcut, @@ -1444,6 +1450,30 @@ export function SettingsView({ Default: {formatShortcut("cmd+shift+r")} +
+
Stop active run
+
+ + handleShortcutKeyDown(event, "interruptShortcut") + } + placeholder="Type shortcut" + readOnly + /> + +
+
+ Default: {formatShortcut("ctrl+c")} +
+
Panels
diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index 5bd59ed1f..2a1486396 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -21,6 +21,7 @@ const defaultSettings: AppSettings = { composerModelShortcut: "cmd+shift+m", composerAccessShortcut: "cmd+shift+a", composerReasoningShortcut: "cmd+shift+r", + interruptShortcut: "ctrl+c", newAgentShortcut: "cmd+n", newWorktreeAgentShortcut: "cmd+shift+n", newCloneAgentShortcut: "cmd+alt+n", diff --git a/src/types.ts b/src/types.ts index af50073c0..cc8cf40b2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,6 +82,7 @@ export type AppSettings = { composerModelShortcut: string | null; composerAccessShortcut: string | null; composerReasoningShortcut: string | null; + interruptShortcut: string | null; newAgentShortcut: string | null; newWorktreeAgentShortcut: string | null; newCloneAgentShortcut: string | null;