diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 1574b5363..5a1e62f33 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( @@ -446,6 +448,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()) } @@ -569,6 +575,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(), @@ -635,6 +642,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/App.tsx b/src/App.tsx index deab2e6ef..ef21265bc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,6 +78,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"; @@ -1118,6 +1119,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..5255f8a21 --- /dev/null +++ b/src/features/app/hooks/useInterruptShortcut.ts @@ -0,0 +1,32 @@ +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) { + useEffect(() => { + if (!isEnabled || !shortcut) { + return; + } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.repeat) { + return; + } + if (!matchesShortcut(event, shortcut)) { + return; + } + event.preventDefault(); + void onTrigger(); + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [isEnabled, onTrigger, shortcut]); +} diff --git a/src/features/settings/components/SettingsView.test.tsx b/src/features/settings/components/SettingsView.test.tsx index 7f0e32769..4e1562da6 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 93265ee68..e9d4b1876 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -134,6 +134,7 @@ type ShortcutSettingKey = | "composerModelShortcut" | "composerAccessShortcut" | "composerReasoningShortcut" + | "interruptShortcut" | "newAgentShortcut" | "newWorktreeAgentShortcut" | "newCloneAgentShortcut" @@ -149,6 +150,7 @@ type ShortcutDraftKey = | "model" | "access" | "reasoning" + | "interrupt" | "newAgent" | "newWorktreeAgent" | "newCloneAgent" @@ -165,6 +167,7 @@ const shortcutDraftKeyBySetting: Record = composerModelShortcut: "model", composerAccessShortcut: "access", composerReasoningShortcut: "reasoning", + interruptShortcut: "interrupt", newAgentShortcut: "newAgent", newWorktreeAgentShortcut: "newWorktreeAgent", newCloneAgentShortcut: "newCloneAgent", @@ -230,6 +233,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 ?? "", @@ -317,6 +321,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 ?? "", @@ -333,6 +338,7 @@ export function SettingsView({ appSettings.composerAccessShortcut, appSettings.composerModelShortcut, appSettings.composerReasoningShortcut, + appSettings.interruptShortcut, appSettings.newAgentShortcut, appSettings.newWorktreeAgentShortcut, appSettings.newCloneAgentShortcut, @@ -1755,6 +1761,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 2448b4e02..10a2102fb 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 8c0cdbff9..6fe9a0790 100644 --- a/src/types.ts +++ b/src/types.ts @@ -97,6 +97,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;