diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index e858998c3..e7714accf 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -340,6 +340,11 @@ pub(crate) struct AppSettings { rename = "newCloneAgentShortcut" )] pub(crate) new_clone_agent_shortcut: Option, + #[serde( + default = "default_archive_thread_shortcut", + rename = "archiveThreadShortcut" + )] + pub(crate) archive_thread_shortcut: Option, #[serde( default = "default_toggle_projects_sidebar_shortcut", rename = "toggleProjectsSidebarShortcut" @@ -541,6 +546,10 @@ fn default_new_clone_agent_shortcut() -> Option { Some("cmd+alt+n".to_string()) } +fn default_archive_thread_shortcut() -> Option { + Some("cmd+ctrl+a".to_string()) +} + fn default_toggle_projects_sidebar_shortcut() -> Option { Some("cmd+shift+p".to_string()) } @@ -719,6 +728,7 @@ impl Default for AppSettings { 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(), + archive_thread_shortcut: default_archive_thread_shortcut(), toggle_projects_sidebar_shortcut: default_toggle_projects_sidebar_shortcut(), toggle_git_sidebar_shortcut: default_toggle_git_sidebar_shortcut(), toggle_debug_panel_shortcut: default_toggle_debug_panel_shortcut(), @@ -795,6 +805,10 @@ mod tests { "ctrl+shift+c" }; assert_eq!(settings.interrupt_shortcut.as_deref(), Some(expected_interrupt)); + assert_eq!( + settings.archive_thread_shortcut.as_deref(), + Some("cmd+ctrl+a") + ); assert_eq!( settings.toggle_debug_panel_shortcut.as_deref(), Some("cmd+shift+d") diff --git a/src/App.tsx b/src/App.tsx index 59a200861..3cbbe2df7 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -85,6 +85,7 @@ 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 { useArchiveShortcut } from "./features/app/hooks/useArchiveShortcut"; import { useLiquidGlassEffect } from "./features/app/hooks/useLiquidGlassEffect"; import { useCopyThread } from "./features/threads/hooks/useCopyThread"; import { useTerminalController } from "./features/terminal/hooks/useTerminalController"; @@ -1307,6 +1308,21 @@ function MainApp() { onDropPaths: handleDropWorkspacePaths, }); + const handleArchiveActiveThread = useCallback(() => { + if (!activeWorkspaceId || !activeThreadId) { + return; + } + removeThread(activeWorkspaceId, activeThreadId); + clearDraftForThread(activeThreadId); + removeImagesForThread(activeThreadId); + }, [ + activeThreadId, + activeWorkspaceId, + clearDraftForThread, + removeImagesForThread, + removeThread, + ]); + useInterruptShortcut({ isEnabled: canInterrupt, shortcut: appSettings.interruptShortcut, @@ -1417,6 +1433,13 @@ function MainApp() { ? centerMode === "chat" || centerMode === "diff" : (isTablet ? tabletTab : activeTab) === "codex") && !showWorkspaceHome; const showGitDetail = Boolean(selectedDiffPath) && isPhone; + const isThreadOpen = Boolean(activeThreadId && showComposer); + + useArchiveShortcut({ + isEnabled: isThreadOpen, + shortcut: appSettings.archiveThreadShortcut, + onTrigger: handleArchiveActiveThread, + }); const { handleCycleAgent, handleCycleWorkspace } = useWorkspaceCycling({ workspaces, diff --git a/src/features/app/hooks/useArchiveShortcut.ts b/src/features/app/hooks/useArchiveShortcut.ts new file mode 100644 index 000000000..11539caae --- /dev/null +++ b/src/features/app/hooks/useArchiveShortcut.ts @@ -0,0 +1,49 @@ +import { useEffect } from "react"; +import { matchesShortcut } from "../../../utils/shortcuts"; + +type UseArchiveShortcutOptions = { + isEnabled: boolean; + shortcut: string | null; + onTrigger: () => void; +}; + +function isEditableTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) { + return false; + } + if (target.isContentEditable) { + return true; + } + return Boolean( + target.closest( + 'input, textarea, select, [contenteditable=""], [contenteditable="true"], [role="textbox"]', + ), + ); +} + +export function useArchiveShortcut({ + isEnabled, + shortcut, + onTrigger, +}: UseArchiveShortcutOptions) { + useEffect(() => { + if (!isEnabled || !shortcut) { + return; + } + const handleKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.repeat) { + return; + } + if (isEditableTarget(event.target)) { + return; + } + if (!matchesShortcut(event, shortcut)) { + return; + } + event.preventDefault(); + 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 0be55fa9f..48879bca6 100644 --- a/src/features/settings/components/SettingsView.test.tsx +++ b/src/features/settings/components/SettingsView.test.tsx @@ -32,6 +32,7 @@ const baseSettings: AppSettings = { newAgentShortcut: null, newWorktreeAgentShortcut: null, newCloneAgentShortcut: null, + archiveThreadShortcut: null, toggleProjectsSidebarShortcut: null, toggleGitSidebarShortcut: null, toggleDebugPanelShortcut: null, diff --git a/src/features/settings/components/SettingsView.tsx b/src/features/settings/components/SettingsView.tsx index b2cfe9cce..ff0f52fe0 100644 --- a/src/features/settings/components/SettingsView.tsx +++ b/src/features/settings/components/SettingsView.tsx @@ -182,6 +182,7 @@ type ShortcutSettingKey = | "newAgentShortcut" | "newWorktreeAgentShortcut" | "newCloneAgentShortcut" + | "archiveThreadShortcut" | "toggleProjectsSidebarShortcut" | "toggleGitSidebarShortcut" | "toggleDebugPanelShortcut" @@ -199,6 +200,7 @@ type ShortcutDraftKey = | "newAgent" | "newWorktreeAgent" | "newCloneAgent" + | "archiveThread" | "projectsSidebar" | "gitSidebar" | "debugPanel" @@ -219,6 +221,7 @@ const shortcutDraftKeyBySetting: Record = newAgentShortcut: "newAgent", newWorktreeAgentShortcut: "newWorktreeAgent", newCloneAgentShortcut: "newCloneAgent", + archiveThreadShortcut: "archiveThread", toggleProjectsSidebarShortcut: "projectsSidebar", toggleGitSidebarShortcut: "gitSidebar", toggleDebugPanelShortcut: "debugPanel", @@ -315,6 +318,7 @@ export function SettingsView({ newAgent: appSettings.newAgentShortcut ?? "", newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "", newCloneAgent: appSettings.newCloneAgentShortcut ?? "", + archiveThread: appSettings.archiveThreadShortcut ?? "", projectsSidebar: appSettings.toggleProjectsSidebarShortcut ?? "", gitSidebar: appSettings.toggleGitSidebarShortcut ?? "", debugPanel: appSettings.toggleDebugPanelShortcut ?? "", @@ -417,6 +421,7 @@ export function SettingsView({ newAgent: appSettings.newAgentShortcut ?? "", newWorktreeAgent: appSettings.newWorktreeAgentShortcut ?? "", newCloneAgent: appSettings.newCloneAgentShortcut ?? "", + archiveThread: appSettings.archiveThreadShortcut ?? "", projectsSidebar: appSettings.toggleProjectsSidebarShortcut ?? "", gitSidebar: appSettings.toggleGitSidebarShortcut ?? "", debugPanel: appSettings.toggleDebugPanelShortcut ?? "", @@ -435,6 +440,7 @@ export function SettingsView({ appSettings.newAgentShortcut, appSettings.newWorktreeAgentShortcut, appSettings.newCloneAgentShortcut, + appSettings.archiveThreadShortcut, appSettings.toggleProjectsSidebarShortcut, appSettings.toggleGitSidebarShortcut, appSettings.toggleDebugPanelShortcut, @@ -1931,6 +1937,30 @@ export function SettingsView({ Default: {formatShortcut("cmd+alt+n")} +
+
Archive active thread
+
+ + handleShortcutKeyDown(event, "archiveThreadShortcut") + } + placeholder="Type shortcut" + readOnly + /> + +
+
+ Default: {formatShortcut("cmd+ctrl+a")} +
+
Composer
diff --git a/src/features/settings/hooks/useAppSettings.ts b/src/features/settings/hooks/useAppSettings.ts index d41e9e550..2f5054c18 100644 --- a/src/features/settings/hooks/useAppSettings.ts +++ b/src/features/settings/hooks/useAppSettings.ts @@ -34,6 +34,7 @@ const defaultSettings: AppSettings = { newAgentShortcut: "cmd+n", newWorktreeAgentShortcut: "cmd+shift+n", newCloneAgentShortcut: "cmd+alt+n", + archiveThreadShortcut: "cmd+ctrl+a", toggleProjectsSidebarShortcut: "cmd+shift+p", toggleGitSidebarShortcut: "cmd+shift+g", toggleDebugPanelShortcut: "cmd+shift+d", diff --git a/src/types.ts b/src/types.ts index 21ae155c4..01809cb7c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -122,6 +122,7 @@ export type AppSettings = { newAgentShortcut: string | null; newWorktreeAgentShortcut: string | null; newCloneAgentShortcut: string | null; + archiveThreadShortcut: string | null; toggleProjectsSidebarShortcut: string | null; toggleGitSidebarShortcut: string | null; toggleDebugPanelShortcut: string | null;