diff --git a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx index 43c196f2d6..dc51696cf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx @@ -14,11 +14,6 @@ import { createLogger } from '@/lib/logs/console/logger' const logger = createLogger('GlobalCommands') -/** - * Detects if the current platform is macOS. - * - * @returns True if running on macOS, false otherwise - */ function isMacPlatform(): boolean { if (typeof window === 'undefined') return false return ( @@ -27,18 +22,6 @@ function isMacPlatform(): boolean { ) } -/** - * Represents a parsed keyboard shortcut. - * - * We support the following modifiers: - * - Mod: maps to Meta on macOS, Ctrl on other platforms - * - Ctrl, Meta, Shift, Alt - * - * Examples: - * - "Mod+A" - * - "Mod+Shift+T" - * - "Meta+K" - */ export interface ParsedShortcut { key: string mod?: boolean @@ -48,24 +31,10 @@ export interface ParsedShortcut { alt?: boolean } -/** - * Declarative command registration. - */ export interface GlobalCommand { - /** Unique id for the command. If omitted, one is generated. */ id?: string - /** Shortcut string in the form "Mod+Shift+T", "Mod+A", "Meta+K", etc. */ shortcut: string - /** - * Whether to allow the command to run inside editable elements like inputs, - * textareas or contenteditable. Defaults to true to ensure browser defaults - * are overridden when desired. - */ allowInEditable?: boolean - /** - * Handler invoked when the shortcut is matched. Use this to trigger actions - * like navigation or dispatching application events. - */ handler: (event: KeyboardEvent) => void } @@ -80,16 +49,13 @@ interface GlobalCommandsContextValue { const GlobalCommandsContext = createContext(null) -/** - * Parses a human-readable shortcut into a structured representation. - */ function parseShortcut(shortcut: string): ParsedShortcut { const parts = shortcut.split('+').map((p) => p.trim()) const modifiers = new Set(parts.slice(0, -1).map((p) => p.toLowerCase())) const last = parts[parts.length - 1] return { - key: last.length === 1 ? last.toLowerCase() : last, // keep non-letter keys verbatim + key: last.length === 1 ? last.toLowerCase() : last, mod: modifiers.has('mod'), ctrl: modifiers.has('ctrl'), meta: modifiers.has('meta') || modifiers.has('cmd') || modifiers.has('command'), @@ -98,16 +64,10 @@ function parseShortcut(shortcut: string): ParsedShortcut { } } -/** - * Checks if a KeyboardEvent matches a parsed shortcut, honoring platform-specific - * interpretation of "Mod" (Meta on macOS, Ctrl elsewhere). - */ function matchesShortcut(e: KeyboardEvent, parsed: ParsedShortcut): boolean { const isMac = isMacPlatform() const expectedCtrl = parsed.ctrl || (parsed.mod ? !isMac : false) const expectedMeta = parsed.meta || (parsed.mod ? isMac : false) - - // Normalize key for comparison: for letters compare lowercase const eventKey = e.key.length === 1 ? e.key.toLowerCase() : e.key return ( @@ -119,10 +79,6 @@ function matchesShortcut(e: KeyboardEvent, parsed: ParsedShortcut): boolean { ) } -/** - * Provider that captures global keyboard shortcuts and routes them to - * registered commands. Commands can be registered from any descendant component. - */ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { const registryRef = useRef>(new Map()) const isMac = useMemo(() => isMacPlatform(), []) @@ -140,13 +96,11 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { allowInEditable: cmd.allowInEditable ?? true, }) createdIds.push(id) - logger.info('Registered global command', { id, shortcut: cmd.shortcut }) } return () => { for (const id of createdIds) { registryRef.current.delete(id) - logger.info('Unregistered global command', { id }) } } }, []) @@ -155,8 +109,6 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { const onKeyDown = (e: KeyboardEvent) => { if (e.isComposing) return - // Evaluate matches in registration order (latest registration wins naturally - // due to replacement on same id). Break on first match. for (const [, cmd] of registryRef.current) { if (!cmd.allowInEditable) { const ae = document.activeElement @@ -168,16 +120,8 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { } if (matchesShortcut(e, cmd.parsed)) { - // Always override default browser behavior for matched commands. e.preventDefault() e.stopPropagation() - logger.info('Executing global command', { - id: cmd.id, - shortcut: cmd.shortcut, - key: e.key, - isMac, - path: typeof window !== 'undefined' ? window.location.pathname : undefined, - }) try { cmd.handler(e) } catch (err) { @@ -197,22 +141,28 @@ export function GlobalCommandsProvider({ children }: { children: ReactNode }) { return {children} } -/** - * Registers a set of global commands for the lifetime of the component. - * - * Returns nothing; cleanup is automatic on unmount. - */ export function useRegisterGlobalCommands(commands: GlobalCommand[] | (() => GlobalCommand[])) { const ctx = useContext(GlobalCommandsContext) if (!ctx) { throw new Error('useRegisterGlobalCommands must be used within GlobalCommandsProvider') } + const commandsRef = useRef([]) + const list = typeof commands === 'function' ? commands() : commands + commandsRef.current = list + useEffect(() => { - const list = typeof commands === 'function' ? commands() : commands - const unregister = ctx.register(list) + const wrappedCommands = commandsRef.current.map((cmd) => ({ + ...cmd, + handler: (event: KeyboardEvent) => { + const currentCmd = commandsRef.current.find((c) => c.id === cmd.id) + if (currentCmd) { + currentCmd.handler(event) + } + }, + })) + const unregister = ctx.register(wrappedCommands) return unregister - // We intentionally want to register once for the given commands // eslint-disable-next-line react-hooks/exhaustive-deps }, []) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index f40e29617e..6a609d8dce 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -1055,7 +1055,7 @@ export function Chat() { {isStreaming ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index fd214a521f..3aebf6e75b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -133,6 +133,13 @@ export function Panel() { } } + /** + * Cancels the currently executing workflow + */ + const cancelWorkflow = useCallback(async () => { + await handleCancelExecution() + }, [handleCancelExecution]) + /** * Runs the workflow with usage limit check */ @@ -144,13 +151,6 @@ export function Panel() { await handleRunWorkflow() }, [usageExceeded, handleRunWorkflow]) - /** - * Cancels the currently executing workflow - */ - const cancelWorkflow = useCallback(async () => { - await handleCancelExecution() - }, [handleCancelExecution]) - // Chat state const { isChatOpen, setIsChatOpen } = useChatStore() const { isOpen: isVariablesOpen, setIsOpen: setVariablesOpen } = useVariablesStore() @@ -300,7 +300,6 @@ export function Panel() { { id: 'run-workflow', handler: () => { - // Do exactly what the Run button does if (isExecuting) { void cancelWorkflow() } else {