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
18 changes: 18 additions & 0 deletions src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ pub(crate) struct AppSettings {
rename = "composerReasoningShortcut"
)]
pub(crate) composer_reasoning_shortcut: Option<String>,
#[serde(default = "default_interrupt_shortcut", rename = "interruptShortcut")]
pub(crate) interrupt_shortcut: Option<String>,
#[serde(
default = "default_composer_collaboration_shortcut",
rename = "composerCollaborationShortcut"
Expand Down Expand Up @@ -505,6 +507,15 @@ fn default_composer_reasoning_shortcut() -> Option<String> {
Some("cmd+shift+r".to_string())
}

fn default_interrupt_shortcut() -> Option<String> {
let value = if cfg!(target_os = "macos") {
"ctrl+c"
} else {
"ctrl+shift+c"
};
Some(value.to_string())
}

fn default_composer_collaboration_shortcut() -> Option<String> {
Some("shift+tab".to_string())
}
Expand Down Expand Up @@ -694,6 +705,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(),
composer_collaboration_shortcut: default_composer_collaboration_shortcut(),
new_agent_shortcut: default_new_agent_shortcut(),
new_worktree_agent_shortcut: default_new_worktree_agent_shortcut(),
Expand Down Expand Up @@ -768,6 +780,12 @@ mod tests {
settings.composer_collaboration_shortcut.as_deref(),
Some("shift+tab")
);
let expected_interrupt = if cfg!(target_os = "macos") {
"ctrl+c"
} else {
"ctrl+shift+c"
};
assert_eq!(settings.interrupt_shortcut.as_deref(), Some(expected_interrupt));
assert_eq!(
settings.toggle_debug_panel_shortcut.as_deref(),
Some("cmd+shift+d")
Expand Down
9 changes: 9 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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 { useLiquidGlassEffect } from "./features/app/hooks/useLiquidGlassEffect";
import { useCopyThread } from "./features/threads/hooks/useCopyThread";
import { useTerminalController } from "./features/terminal/hooks/useTerminalController";
Expand Down Expand Up @@ -1208,6 +1209,14 @@ function MainApp() {
onDropPaths: handleDropWorkspacePaths,
});

useInterruptShortcut({
isEnabled: canInterrupt,
shortcut: appSettings.interruptShortcut,
onTrigger: () => {
void interruptTurn();
},
});

const {
handleSelectPullRequest,
resetPullRequestSelection,
Expand Down
40 changes: 40 additions & 0 deletions src/features/app/hooks/useInterruptShortcut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useEffect } from "react";
import { matchesShortcut } from "../../../utils/shortcuts";

type UseInterruptShortcutOptions = {
isEnabled: boolean;
shortcut: string | null;
onTrigger: () => void | Promise<void>;
};

export function useInterruptShortcut({
isEnabled,
shortcut,
onTrigger,
}: UseInterruptShortcutOptions) {
useEffect(() => {
if (!isEnabled || !shortcut) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.repeat || event.defaultPrevented) {
return;
}
const target = event.target;
if (
target instanceof HTMLElement &&
(target.isContentEditable ||
target.closest("input, textarea, select, [contenteditable='true']"))
) {
return;
}
if (!matchesShortcut(event, shortcut)) {
Comment thread
Dimillian marked this conversation as resolved.
return;
}
event.preventDefault();
void onTrigger();
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [isEnabled, onTrigger, shortcut]);
}
1 change: 1 addition & 0 deletions src/features/settings/components/SettingsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const baseSettings: AppSettings = {
composerAccessShortcut: null,
composerReasoningShortcut: null,
composerCollaborationShortcut: null,
interruptShortcut: null,
newAgentShortcut: null,
newWorktreeAgentShortcut: null,
newCloneAgentShortcut: null,
Expand Down
36 changes: 35 additions & 1 deletion src/features/settings/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import type {
WorkspaceInfo,
} from "../../../types";
import { formatDownloadSize } from "../../../utils/formatting";
import { buildShortcutValue, formatShortcut } from "../../../utils/shortcuts";
import {
buildShortcutValue,
formatShortcut,
getDefaultInterruptShortcut,
} from "../../../utils/shortcuts";
import { clampUiScale } from "../../../utils/uiScale";
import { getCodexConfigPath } from "../../../services/tauri";
import {
Expand Down Expand Up @@ -174,6 +178,7 @@ type ShortcutSettingKey =
| "composerAccessShortcut"
| "composerReasoningShortcut"
| "composerCollaborationShortcut"
| "interruptShortcut"
| "newAgentShortcut"
| "newWorktreeAgentShortcut"
| "newCloneAgentShortcut"
Expand All @@ -190,6 +195,7 @@ type ShortcutDraftKey =
| "access"
| "reasoning"
| "collaboration"
| "interrupt"
| "newAgent"
| "newWorktreeAgent"
| "newCloneAgent"
Expand All @@ -209,6 +215,7 @@ const shortcutDraftKeyBySetting: Record<ShortcutSettingKey, ShortcutDraftKey> =
composerAccessShortcut: "access",
composerReasoningShortcut: "reasoning",
composerCollaborationShortcut: "collaboration",
interruptShortcut: "interrupt",
newAgentShortcut: "newAgent",
newWorktreeAgentShortcut: "newWorktreeAgent",
newCloneAgentShortcut: "newCloneAgent",
Expand Down Expand Up @@ -304,6 +311,7 @@ export function SettingsView({
access: appSettings.composerAccessShortcut ?? "",
reasoning: appSettings.composerReasoningShortcut ?? "",
collaboration: appSettings.composerCollaborationShortcut ?? "",
interrupt: appSettings.interruptShortcut ?? "",
newAgent: appSettings.newAgentShortcut ?? "",
newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "",
newCloneAgent: appSettings.newCloneAgentShortcut ?? "",
Expand Down Expand Up @@ -405,6 +413,7 @@ export function SettingsView({
access: appSettings.composerAccessShortcut ?? "",
reasoning: appSettings.composerReasoningShortcut ?? "",
collaboration: appSettings.composerCollaborationShortcut ?? "",
interrupt: appSettings.interruptShortcut ?? "",
newAgent: appSettings.newAgentShortcut ?? "",
newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "",
newCloneAgent: appSettings.newCloneAgentShortcut ?? "",
Expand All @@ -422,6 +431,7 @@ export function SettingsView({
appSettings.composerModelShortcut,
appSettings.composerReasoningShortcut,
appSettings.composerCollaborationShortcut,
appSettings.interruptShortcut,
appSettings.newAgentShortcut,
appSettings.newWorktreeAgentShortcut,
appSettings.newCloneAgentShortcut,
Expand Down Expand Up @@ -2022,6 +2032,30 @@ export function SettingsView({
Default: {formatShortcut("shift+tab")}
</div>
</div>
<div className="settings-field">
<div className="settings-field-label">Stop active run</div>
<div className="settings-field-row">
<input
className="settings-input settings-input--shortcut"
value={formatShortcut(shortcutDrafts.interrupt)}
onKeyDown={(event) =>
handleShortcutKeyDown(event, "interruptShortcut")
}
placeholder="Type shortcut"
readOnly
/>
<button
type="button"
className="ghost settings-button-compact"
onClick={() => void updateShortcut("interruptShortcut", null)}
>
Clear
</button>
</div>
<div className="settings-help">
Default: {formatShortcut(getDefaultInterruptShortcut())}
</div>
</div>
<div className="settings-divider" />
<div className="settings-subsection-title">Panels</div>
<div className="settings-subsection-subtitle">
Expand Down
1 change: 1 addition & 0 deletions src/features/settings/hooks/useAppSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ describe("useAppSettings", () => {
expect(result.current.settings.codeFontFamily).toContain("SF Mono");
expect(result.current.settings.backendMode).toBe("local");
expect(result.current.settings.dictationModelId).toBe("base");
expect(result.current.settings.interruptShortcut).toBeTruthy();
});

it("persists settings via updateAppSettings and updates local state", async () => {
Expand Down
2 changes: 2 additions & 0 deletions src/features/settings/hooks/useAppSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
OPEN_APP_STORAGE_KEY,
} from "../../app/constants";
import { normalizeOpenAppTargets } from "../../app/utils/openApp";
import { getDefaultInterruptShortcut } from "../../../utils/shortcuts";

const allowedThemes = new Set(["system", "light", "dark"]);

Expand All @@ -29,6 +30,7 @@ const defaultSettings: AppSettings = {
composerAccessShortcut: "cmd+shift+a",
composerReasoningShortcut: "cmd+shift+r",
composerCollaborationShortcut: "shift+tab",
interruptShortcut: getDefaultInterruptShortcut(),
newAgentShortcut: "cmd+n",
newWorktreeAgentShortcut: "cmd+shift+n",
newCloneAgentShortcut: "cmd+alt+n",
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export type AppSettings = {
composerAccessShortcut: string | null;
composerReasoningShortcut: string | null;
composerCollaborationShortcut: string | null;
interruptShortcut: string | null;
newAgentShortcut: string | null;
newWorktreeAgentShortcut: string | null;
newCloneAgentShortcut: string | null;
Expand Down
11 changes: 11 additions & 0 deletions src/utils/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,17 @@ export function matchesShortcut(event: KeyboardEvent, value: string | null | und
);
}

export function isMacPlatform(): boolean {
if (typeof navigator === "undefined") {
return false;
}
return /Mac|iPhone|iPad|iPod/.test(navigator.platform);
}

export function getDefaultInterruptShortcut(): string {
return isMacPlatform() ? "ctrl+c" : "ctrl+shift+c";
}

export function toMenuAccelerator(value: string | null | undefined): string | null {
const parsed = parseShortcut(value);
if (!parsed) {
Expand Down