From 966b20b9b96666db8ab4f06c8b691ddf8ec3a197 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 23 Dec 2025 23:41:54 -0800 Subject: [PATCH 1/3] fix(shortcut): fixed global commands provider to follow best practices --- .../providers/global-commands-provider.tsx | 78 ++++--------------- .../w/[workflowId]/components/chat/chat.tsx | 2 +- .../w/[workflowId]/components/panel/panel.tsx | 15 ++-- .../hooks/use-workflow-execution.ts | 15 ++++ 4 files changed, 37 insertions(+), 73 deletions(-) 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..4efa8b6b17 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([]) + commandsRef.current = typeof commands === 'function' ? commands() : commands + useEffect(() => { const list = typeof commands === 'function' ? commands() : commands - const unregister = ctx.register(list) + const wrappedCommands = list.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 { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 4e33ffe78e..c5835572cb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -886,6 +886,21 @@ export function useWorkflowExecution() { const activeBlocksSet = new Set() const streamedContent = new Map() + // #region agent log + fetch('http://127.0.0.1:7243/ingest/77a2b2bc-808d-4bfd-a366-739b0b04635d', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + location: 'use-workflow-execution.ts:executeWorkflow:apiCall', + message: 'About to call executionStream.execute', + data: { activeWorkflowId, startBlockId, hasInput: !!finalWorkflowInput }, + timestamp: Date.now(), + sessionId: 'debug-session', + hypothesisId: 'D', + }), + }).catch(() => {}) + // #endregion + // Execute the workflow try { await executionStream.execute({ From 84e1f86d079f0655b019323fd862a98e5ccfa855 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 23 Dec 2025 23:52:20 -0800 Subject: [PATCH 2/3] cleanup --- .../[workflowId]/hooks/use-workflow-execution.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index c5835572cb..4e33ffe78e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -886,21 +886,6 @@ export function useWorkflowExecution() { const activeBlocksSet = new Set() const streamedContent = new Map() - // #region agent log - fetch('http://127.0.0.1:7243/ingest/77a2b2bc-808d-4bfd-a366-739b0b04635d', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - location: 'use-workflow-execution.ts:executeWorkflow:apiCall', - message: 'About to call executionStream.execute', - data: { activeWorkflowId, startBlockId, hasInput: !!finalWorkflowInput }, - timestamp: Date.now(), - sessionId: 'debug-session', - hypothesisId: 'D', - }), - }).catch(() => {}) - // #endregion - // Execute the workflow try { await executionStream.execute({ From dc11d72bfb8ef99d53bd22a31e14b8197570378f Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 24 Dec 2025 00:18:24 -0800 Subject: [PATCH 3/3] ack PR comment --- .../[workspaceId]/providers/global-commands-provider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 4efa8b6b17..dc51696cf2 100644 --- a/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx +++ b/apps/sim/app/workspace/[workspaceId]/providers/global-commands-provider.tsx @@ -148,11 +148,11 @@ export function useRegisterGlobalCommands(commands: GlobalCommand[] | (() => Glo } const commandsRef = useRef([]) - commandsRef.current = typeof commands === 'function' ? commands() : commands + const list = typeof commands === 'function' ? commands() : commands + commandsRef.current = list useEffect(() => { - const list = typeof commands === 'function' ? commands() : commands - const wrappedCommands = list.map((cmd) => ({ + const wrappedCommands = commandsRef.current.map((cmd) => ({ ...cmd, handler: (event: KeyboardEvent) => { const currentCmd = commandsRef.current.find((c) => c.id === cmd.id)