diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 3569d521..0ab780fa 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -391,7 +391,7 @@ fn build_macos_menu( MENU_ITEM_COMMAND_PALETTE_ID, "Command Palette", true, - Some("Ctrl+X"), + Some("Cmd+K"), )?; // File menu items @@ -400,7 +400,7 @@ fn build_macos_menu( MENU_ITEM_NEW_SESSION_ID, "New Session", true, - Some("Ctrl+N"), + Some("Cmd+N"), )?; let worktree_creator = MenuItem::with_id( @@ -408,7 +408,7 @@ fn build_macos_menu( MENU_ITEM_WORKTREE_CREATOR_ID, "New Worktree", true, - Some("Ctrl+Shift+N"), + Some("Cmd+Shift+N"), )?; let change_workspace = MenuItem::with_id( @@ -420,15 +420,20 @@ fn build_macos_menu( )?; // View menu items - let open_git_tab = - MenuItem::with_id(app, MENU_ITEM_OPEN_GIT_TAB_ID, "Git", true, Some("Ctrl+G"))?; + let open_git_tab = MenuItem::with_id( + app, + MENU_ITEM_OPEN_GIT_TAB_ID, + "Git", + true, + Some("Cmd+G"), + )?; let open_diff_tab = MenuItem::with_id( app, MENU_ITEM_OPEN_DIFF_TAB_ID, "Diff", true, - Some("Ctrl+E"), + Some("Cmd+E"), )?; let open_terminal_tab = MenuItem::with_id( @@ -436,7 +441,7 @@ fn build_macos_menu( MENU_ITEM_OPEN_TERMINAL_TAB_ID, "Terminal", true, - Some("Ctrl+T"), + Some("Cmd+T"), )?; let theme_light = MenuItem::with_id( @@ -468,7 +473,7 @@ fn build_macos_menu( MENU_ITEM_TOGGLE_SIDEBAR_ID, "Toggle Session Sidebar", true, - Some("Ctrl+L"), + Some("Cmd+L"), )?; let toggle_memory_debug = MenuItem::with_id( @@ -485,7 +490,7 @@ fn build_macos_menu( MENU_ITEM_HELP_DIALOG_ID, "Keyboard Shortcuts", true, - Some("Ctrl+H"), + Some("Cmd+H"), )?; let download_logs = MenuItem::with_id( @@ -493,7 +498,7 @@ fn build_macos_menu( MENU_ITEM_DOWNLOAD_LOGS_ID, "Download Logs", true, - Some("Ctrl+Shift+L"), + Some("Cmd+Shift+L"), )?; let report_bug = MenuItem::with_id( @@ -1237,6 +1242,7 @@ async fn handle_agent_route( // Get working directory for project-level agent detection let working_directory = state.opencode.get_working_directory(); + match method { Method::GET => { match opencode_config::get_agent_sources(&name, Some(&working_directory)).await { @@ -1270,6 +1276,7 @@ async fn handle_agent_route( Err(resp) => return Ok(resp), }; + // Extract scope from payload if present let scope = payload .get("scope") @@ -1418,6 +1425,7 @@ async fn handle_skill_list_route(state: &ServerState) -> Result, let working_directory = state.opencode.get_working_directory(); let discovered = opencode_config::discover_skills(Some(&working_directory)); + let mut skills = Vec::new(); for skill in discovered { match opencode_config::get_skill_sources(&skill.name, Some(&working_directory)).await { @@ -1439,10 +1447,7 @@ async fn handle_skill_list_route(state: &ServerState) -> Result, } } - Ok(json_response( - StatusCode::OK, - serde_json::json!({ "skills": skills }), - )) + Ok(json_response(StatusCode::OK, serde_json::json!({ "skills": skills }))) } async fn handle_skill_route( @@ -1454,6 +1459,7 @@ async fn handle_skill_route( ) -> Result, StatusCode> { let working_directory = state.opencode.get_working_directory(); + // Handle file operations: /api/config/skills/:name/files/* if let Some(ref fp) = file_path { match method { @@ -1502,10 +1508,7 @@ async fn handle_skill_route( Ok(data) => data, Err(resp) => return Ok(resp), }; - let content = payload - .get("content") - .and_then(|v| v.as_str()) - .unwrap_or(""); + let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or(""); match opencode_config::get_skill_sources(&name, Some(&working_directory)).await { Ok(sources) => { @@ -1618,8 +1621,7 @@ async fn handle_skill_route( Err(resp) => return Ok(resp), }; - let scope = payload - .get("scope") + let scope = payload.get("scope") .and_then(|v| v.as_str()) .and_then(|s| match s { "project" => Some(opencode_config::SkillScope::Project), @@ -1748,6 +1750,7 @@ async fn handle_command_route( // Get working directory for project-level command detection let working_directory = state.opencode.get_working_directory(); + match method { Method::GET => { match opencode_config::get_command_sources(&name, Some(&working_directory)).await { @@ -1781,6 +1784,7 @@ async fn handle_command_route( Err(resp) => return Ok(resp), }; + // Extract scope from payload if present let scope = payload .get("scope") @@ -2055,6 +2059,7 @@ async fn handle_config_routes( .await; } + let trimmed = rest.trim(); if trimmed.is_empty() { return Ok(config_error_response( diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index c0497a16..12bccbdf 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -13,6 +13,7 @@ import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup'; import { GitPollingProvider } from '@/hooks/useGitPolling'; import { useConfigStore } from '@/stores/useConfigStore'; +import { hasModifier } from '@/lib/utils'; import { useSessionStore } from '@/stores/useSessionStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { opencodeClient } from '@/lib/opencode/client'; @@ -156,7 +157,7 @@ function App({ apis }: AppProps) { React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'M') { + if (hasModifier(e) && e.shiftKey && e.key === 'M') { e.preventDefault(); setShowMemoryDebug(prev => !prev); } diff --git a/packages/ui/src/components/layout/Header.tsx b/packages/ui/src/components/layout/Header.tsx index 4f5a3559..b48fa2f9 100644 --- a/packages/ui/src/components/layout/Header.tsx +++ b/packages/ui/src/components/layout/Header.tsx @@ -12,7 +12,7 @@ import { useConfigStore } from '@/stores/useConfigStore'; import { useSessionStore } from '@/stores/useSessionStore'; import { ContextUsageDisplay } from '@/components/ui/ContextUsageDisplay'; import { useDeviceInfo } from '@/lib/device'; -import { cn } from '@/lib/utils'; +import { cn, getModifierLabel, hasModifier } from '@/lib/utils'; import { useDiffFileCount } from '@/components/views/DiffView'; interface TabConfig { @@ -277,7 +277,7 @@ export const Header: React.FC = () => { useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if ((e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { + if (hasModifier(e) && !e.shiftKey && !e.altKey) { const num = parseInt(e.key, 10); if (num >= 1 && num <= tabs.length) { e.preventDefault(); @@ -410,7 +410,7 @@ export const Header: React.FC = () => { -

Command Palette (Ctrl+X)

+

Command Palette ({getModifierLabel()}+K)

@@ -425,7 +425,7 @@ export const Header: React.FC = () => { -

Keyboard Shortcuts (Ctrl+H)

+

Keyboard Shortcuts ({getModifierLabel()}+H)

diff --git a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx index 1c6f25b8..fba5ac05 100644 --- a/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx +++ b/packages/ui/src/components/sections/openchamber/OpenChamberVisualSettings.tsx @@ -5,7 +5,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem'; import type { ThemeMode } from '@/types/theme'; import { useUIStore } from '@/stores/useUIStore'; import { useMessageQueueStore } from '@/stores/messageQueueStore'; -import { cn } from '@/lib/utils'; +import { cn, getModifierLabel } from '@/lib/utils'; import { ButtonSmall } from '@/components/ui/button-small'; import { NumberInput } from '@/components/ui/number-input'; import { isVSCodeRuntime } from '@/lib/desktop'; @@ -355,8 +355,8 @@ export const OpenChamberVisualSettings: React.FC

{queueModeEnabled - ? 'Enter queues messages, Ctrl+Enter sends immediately.' - : 'Enter sends immediately, Ctrl+Enter queues messages.'} + ? `Enter queues messages, ${getModifierLabel()}+Enter sends immediately.` + : `Enter sends immediately, ${getModifierLabel()}+Enter queues messages.`}

)} diff --git a/packages/ui/src/components/ui/CommandPalette.tsx b/packages/ui/src/components/ui/CommandPalette.tsx index 525455ad..b7a7bd93 100644 --- a/packages/ui/src/components/ui/CommandPalette.tsx +++ b/packages/ui/src/components/ui/CommandPalette.tsx @@ -16,6 +16,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useDeviceInfo } from '@/lib/device'; import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiMoonLine, RiQuestionLine, RiRestartLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react'; import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore'; +import { getModifierLabel } from '@/lib/utils'; export const CommandPalette: React.FC = () => { const { @@ -121,42 +122,42 @@ export const CommandPalette: React.FC = () => { Open Session List - Ctrl + L + {getModifierLabel()} + L New Session - Ctrl + N + {getModifierLabel()} + N New Session with Worktree - Shift + Ctrl + N + Shift + {getModifierLabel()} + N Keyboard Shortcuts - Ctrl + H + {getModifierLabel()} + H Open Diff Panel - Ctrl + E + {getModifierLabel()} + E Open Git Panel - Ctrl + G + {getModifierLabel()} + G Open Terminal - Ctrl + T + {getModifierLabel()} + T Open Settings - Ctrl + , + {getModifierLabel()} + , diff --git a/packages/ui/src/components/ui/HelpDialog.tsx b/packages/ui/src/components/ui/HelpDialog.tsx index 02737eb5..f1a2f10a 100644 --- a/packages/ui/src/components/ui/HelpDialog.tsx +++ b/packages/ui/src/components/ui/HelpDialog.tsx @@ -1,41 +1,66 @@ -import React from 'react'; +import React from "react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, -} from '@/components/ui/dialog'; -import { useUIStore } from '@/stores/useUIStore'; -import { RiAddLine, RiArrowUpSLine, RiArrowUpWideLine, RiBrainAi3Line, RiCloseCircleLine, RiCodeLine, RiCommandLine, RiGitBranchLine, RiLayoutLeftLine, RiPaletteLine, RiQuestionLine, RiSettings3Line, RiTerminalBoxLine, RiText } from '@remixicon/react'; +} from "@/components/ui/dialog"; +import { useUIStore } from "@/stores/useUIStore"; +import { + RiAddLine, + RiArrowUpSLine, + RiBrainAi3Line, + RiCloseCircleLine, + RiCodeLine, + RiCommandLine, + RiGitBranchLine, + RiLayoutLeftLine, + RiPaletteLine, + RiQuestionLine, + RiSettings3Line, + RiTerminalBoxLine, + RiText, +} from "@remixicon/react"; +import { getModifierLabel } from "@/lib/utils"; const renderKeyToken = (token: string, index: number) => { const normalized = token.trim().toLowerCase(); - if (normalized === 'ctrl' || normalized === 'control') { + if (normalized === "ctrl" || normalized === "control") { return ; } - if (normalized === 'shift' || normalized === '⇧') { - return ; - } - - if (normalized === '⌘' || normalized === 'cmd' || normalized === 'command' || normalized === 'meta') { + if ( + normalized === "⌘" || + normalized === "cmd" || + normalized === "command" || + normalized === "meta" + ) { return ; } - return {token.trim()}; + return ( + + {token.trim()} + + ); }; const renderKeyCombo = (combo: string) => { - const tokens = combo.split('+').map((token) => token.trim()).filter(Boolean); + const tokens = combo + .split("+") + .map((token) => token.trim()) + .filter(Boolean); if (tokens.length === 0) { return combo.trim(); } return tokens.map((token, index) => ( - {index > 0 && +} + {index > 0 && ( + + + )} {renderKeyToken(token, index)} )); @@ -57,35 +82,85 @@ type ShortcutSection = { export const HelpDialog: React.FC = () => { const { isHelpDialogOpen, setHelpDialogOpen } = useUIStore(); + const mod = getModifierLabel(); + const shortcuts: ShortcutSection[] = [ { category: "Navigation & Commands", items: [ - { keys: ["Ctrl + X"], description: "Open Command Palette", icon: RiCommandLine }, - { keys: ["Ctrl + H"], description: "Show Keyboard Shortcuts (this dialog)", icon: RiQuestionLine }, - { keys: ["Ctrl + L"], description: "Toggle Session Sidebar", icon: RiLayoutLeftLine }, - { keys: ["Ctrl + M"], description: "Open Model Selector", icon: RiBrainAi3Line }, - ] + { + keys: [`${mod} + K`], + description: "Open Command Palette", + icon: RiCommandLine, + }, + { + keys: [`${mod} + H`], + description: "Show Keyboard Shortcuts (this dialog)", + icon: RiQuestionLine, + }, + { + keys: [`${mod} + L`], + description: "Toggle Session Sidebar", + icon: RiLayoutLeftLine, + }, + { + keys: [`${mod} + M`], + description: "Open Model Selector", + icon: RiBrainAi3Line, + }, + ], }, { category: "Session Management", items: [ - { keys: ["Ctrl + N"], description: "Create New Session", icon: RiAddLine }, - { keys: ["Shift + Ctrl + N"], description: "Open Worktree Creator", icon: RiGitBranchLine }, - { keys: ["Ctrl + I"], description: "Focus Chat Input", icon: RiText }, - { keys: ["Esc + Esc"], description: "Abort active run (double press)", icon: RiCloseCircleLine }, - ] + { + keys: [`${mod} + N`], + description: "Create New Session", + icon: RiAddLine, + }, + { + keys: [`Shift + ${mod} + N`], + description: "Open Worktree Creator", + icon: RiGitBranchLine, + }, + { keys: [`${mod} + I`], description: "Focus Chat Input", icon: RiText }, + { + keys: ["Esc + Esc"], + description: "Abort active run (double press)", + icon: RiCloseCircleLine, + }, + ], }, { category: "Interface", items: [ - { keys: ["⌘ + /", "Ctrl + /"], description: "Cycle Theme (Light → Dark → System)", icon: RiPaletteLine }, - { keys: ["Ctrl + E"], description: "Open Diff Panel", icon: RiCodeLine }, - { keys: ["Ctrl + G"], description: "Open Git Panel", icon: RiGitBranchLine }, - { keys: ["Ctrl + T"], description: "Open Terminal", icon: RiTerminalBoxLine }, - { keys: ["Ctrl + ,"], description: "Open Settings", icon: RiSettings3Line }, - ] - } + { + keys: [`${mod} + /`], + description: "Cycle Theme (Light → Dark → System)", + icon: RiPaletteLine, + }, + { + keys: [`${mod} + E`], + description: "Open Diff Panel", + icon: RiCodeLine, + }, + { + keys: [`${mod} + G`], + description: "Open Git Panel", + icon: RiGitBranchLine, + }, + { + keys: [`${mod} + T`], + description: "Open Terminal", + icon: RiTerminalBoxLine, + }, + { + keys: [`${mod} + ,`], + description: "Open Settings", + icon: RiSettings3Line, + }, + ], + }, ]; return ( @@ -117,12 +192,21 @@ export const HelpDialog: React.FC = () => { {shortcut.icon && ( )} - {shortcut.description} + + {shortcut.description} +
- {(Array.isArray(shortcut.keys) ? shortcut.keys : shortcut.keys.split(' / ')).map((keyCombo: string, i: number) => ( + {(Array.isArray(shortcut.keys) + ? shortcut.keys + : shortcut.keys.split(" / ") + ).map((keyCombo: string, i: number) => ( - {i > 0 && or} + {i > 0 && ( + + or + + )} {renderKeyCombo(keyCombo)} @@ -141,11 +225,18 @@ export const HelpDialog: React.FC = () => {

Pro Tips:

-
    -
  • • Use Command Palette (Ctrl + X) to quickly access all actions
  • -
  • • The 5 most recent sessions appear in the Command Palette
  • -
  • • Theme cycling remembers your preference across sessions
  • -
+
    +
  • + • Use Command Palette ({mod} + K) to quickly access all + actions +
  • +
  • + • The 5 most recent sessions appear in the Command Palette +
  • +
  • + • Theme cycling remembers your preference across sessions +
  • +
diff --git a/packages/ui/src/components/views/SettingsView.tsx b/packages/ui/src/components/views/SettingsView.tsx index 5307ffcb..fa08a973 100644 --- a/packages/ui/src/components/views/SettingsView.tsx +++ b/packages/ui/src/components/views/SettingsView.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { cn } from '@/lib/utils'; +import { cn, getModifierLabel } from '@/lib/utils'; import { SIDEBAR_SECTIONS } from '@/constants/sidebar'; import type { SidebarSection } from '@/constants/sidebar'; import { RiArrowLeftSLine, RiCloseLine } from '@remixicon/react'; @@ -244,7 +244,7 @@ export const SettingsView: React.FC = ({ onClose, forceMobile }; // Keyboard shortcut display based on platform - const shortcutKey = isMacPlatform ? '⌘' : 'Ctrl'; + const shortcutKey = getModifierLabel(); // Desktop padding for Mac titlebar area const desktopPaddingClass = React.useMemo(() => { diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index f05324c3..96f59300 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -4,6 +4,7 @@ import { useUIStore } from '@/stores/useUIStore'; import { useThemeSystem } from '@/contexts/useThemeSystem'; import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; +import { hasModifier } from '@/lib/utils'; export const useKeyboardShortcuts = () => { const { openNewSessionDraft, abortCurrentOperation, armAbortPrompt, clearAbortPrompt, currentSessionId } = useSessionStore(); @@ -35,12 +36,12 @@ export const useKeyboardShortcuts = () => { React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.ctrlKey && e.key === 'x') { + if (hasModifier(e) && e.key === 'k') { e.preventDefault(); toggleCommandPalette(); } - if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'l') { + if (hasModifier(e) && e.shiftKey && e.key.toLowerCase() === 'l') { const runtimeAPIs = getRegisteredRuntimeAPIs(); const diagnostics = runtimeAPIs?.diagnostics; if (!diagnostics) { @@ -73,12 +74,12 @@ export const useKeyboardShortcuts = () => { return; } - if (e.ctrlKey && e.key === 'h') { + if (hasModifier(e) && e.key === 'h') { e.preventDefault(); toggleHelpDialog(); } - if (e.ctrlKey && !e.metaKey && e.key.toLowerCase() === 'n') { + if (hasModifier(e) && e.key.toLowerCase() === 'n') { e.preventDefault(); if (e.shiftKey) { setSessionCreateDialogOpen(true); @@ -90,7 +91,7 @@ export const useKeyboardShortcuts = () => { openNewSessionDraft(); } - if ((e.metaKey || e.ctrlKey) && e.key === '/') { + if (hasModifier(e) && e.key === '/') { e.preventDefault(); const modes: Array<'light' | 'dark' | 'system'> = ['light', 'dark', 'system']; const currentIndex = modes.indexOf(themeMode); @@ -98,35 +99,35 @@ export const useKeyboardShortcuts = () => { setThemeMode(modes[nextIndex]); } - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'g') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'g') { e.preventDefault(); const { activeMainTab } = useUIStore.getState(); setActiveMainTab(activeMainTab === 'git' ? 'chat' : 'git'); return; } - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'e') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'e') { e.preventDefault(); const { activeMainTab } = useUIStore.getState(); setActiveMainTab(activeMainTab === 'diff' ? 'chat' : 'diff'); return; } - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 't') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 't') { e.preventDefault(); const { activeMainTab } = useUIStore.getState(); setActiveMainTab(activeMainTab === 'terminal' ? 'chat' : 'terminal'); return; } - if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === ',') { + if (hasModifier(e) && !e.shiftKey && e.key === ',') { e.preventDefault(); const { isSettingsDialogOpen } = useUIStore.getState(); setSettingsDialogOpen(!isSettingsDialogOpen); return; } - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'l') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'l') { e.preventDefault(); const { isMobile, isSessionSwitcherOpen } = useUIStore.getState(); if (isMobile) { @@ -137,7 +138,7 @@ export const useKeyboardShortcuts = () => { return; } - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'i') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'i') { e.preventDefault(); const textarea = document.querySelector('textarea[data-chat-input="true"]'); textarea?.focus(); @@ -145,7 +146,7 @@ export const useKeyboardShortcuts = () => { } // Ctrl+M: Open model selector (same conditions as double-ESC: chat tab, no overlays) - if (e.ctrlKey && !e.metaKey && !e.shiftKey && e.key.toLowerCase() === 'm') { + if (hasModifier(e) && !e.shiftKey && e.key.toLowerCase() === 'm') { const { isSettingsDialogOpen, isCommandPaletteOpen, diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts index 3bad1b39..b7933ad7 100644 --- a/packages/ui/src/lib/utils.ts +++ b/packages/ui/src/lib/utils.ts @@ -1,10 +1,38 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; +import { isDesktopRuntime } from "@/lib/desktop"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +/** + * Detects if the current platform is macOS. + * Uses navigator.userAgent in browser environments. + */ +export const isMacOS = (): boolean => { + if (typeof navigator === 'undefined') return false; + return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); +}; + +/** + * Checks if the platform-appropriate modifier key is pressed. + * On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey). + * Browser intercepts Cmd shortcuts, so we only use Cmd in Tauri desktop app. + */ +export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => { + return isMacOS() && isDesktopRuntime() ? e.metaKey : e.ctrlKey; +}; + +/** + * Returns the platform-appropriate modifier key label. + * On macOS desktop app: "⌘", on other platforms or web: "Ctrl" + * Browser intercepts Cmd shortcuts, so we only show Cmd in Tauri desktop app. + */ +export const getModifierLabel = (): string => { + return isMacOS() && isDesktopRuntime() ? '⌘' : 'Ctrl'; +}; + export const truncatePathMiddle = ( value: string, options?: { maxLength?: number }