Skip to content

Commit d401551

Browse files
committed
improvement(ux): command + enter to run workflow
1 parent 434d495 commit d401551

File tree

2 files changed

+137
-38
lines changed

2 files changed

+137
-38
lines changed

sim/app/w/[id]/components/control-bar/control-bar.tsx

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { useGeneralStore } from '@/stores/settings/general/store'
4444
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
4545
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
4646
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
47+
import { getKeyboardShortcutText, useKeyboardShortcuts } from '../../hooks/use-keyboard-shortcuts'
4748
import { useWorkflowExecution } from '../../hooks/use-workflow-execution'
4849
import { DeploymentControls } from './components/deployment-controls/deployment-controls'
4950
import { HistoryDropdownItem } from './components/history-dropdown-item/history-dropdown-item'
@@ -108,6 +109,20 @@ export function ControlBar() {
108109
const [isCancelling, setIsCancelling] = useState(false)
109110
const cancelFlagRef = useRef(false)
110111

112+
// Register keyboard shortcut for running workflow
113+
useKeyboardShortcuts(
114+
() => {
115+
if (!isExecuting && !isMultiRunning && !isCancelling) {
116+
if (isDebugModeEnabled) {
117+
handleRunWorkflow()
118+
} else {
119+
handleMultipleRuns()
120+
}
121+
}
122+
},
123+
isExecuting || isMultiRunning || isCancelling
124+
)
125+
111126
// Get notifications for current workflow
112127
const workflowNotifications = activeWorkflowId
113128
? getWorkflowNotifications(activeWorkflowId)
@@ -820,44 +835,58 @@ export function ControlBar() {
820835

821836
<div className="flex ml-1">
822837
{/* Main Run/Debug Button */}
823-
<Button
824-
className={cn(
825-
'gap-2 font-medium',
826-
'bg-[#802FFF] hover:bg-[#7028E6]',
827-
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
828-
'text-white transition-all duration-200',
829-
(isExecuting || isMultiRunning) &&
830-
!isCancelling &&
831-
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
832-
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none',
833-
isDebugModeEnabled || isMultiRunning
834-
? 'rounded py-2 px-4 h-10'
835-
: 'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
836-
)}
837-
onClick={isDebugModeEnabled ? handleRunWorkflow : handleMultipleRuns}
838-
disabled={isExecuting || isMultiRunning || isCancelling}
839-
>
840-
{isCancelling ? (
841-
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
842-
) : isDebugModeEnabled ? (
843-
<Bug className={cn('h-3.5 w-3.5 mr-1.5', 'fill-current stroke-current')} />
844-
) : (
845-
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
846-
)}
847-
{isCancelling
848-
? 'Cancelling...'
849-
: isMultiRunning
850-
? `Running (${completedRuns}/${runCount})`
851-
: isExecuting
852-
? isDebugging
853-
? 'Debugging'
854-
: 'Running'
855-
: isDebugModeEnabled
856-
? 'Debug'
857-
: runCount === 1
858-
? 'Run'
859-
: `Run (${runCount})`}
860-
</Button>
838+
<Tooltip>
839+
<TooltipTrigger asChild>
840+
<Button
841+
className={cn(
842+
'gap-2 font-medium',
843+
'bg-[#802FFF] hover:bg-[#7028E6]',
844+
'shadow-[0_0_0_0_#802FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
845+
'text-white transition-all duration-200',
846+
(isExecuting || isMultiRunning) &&
847+
!isCancelling &&
848+
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
849+
'disabled:opacity-50 disabled:hover:bg-[#802FFF] disabled:hover:shadow-none',
850+
isDebugModeEnabled || isMultiRunning
851+
? 'rounded py-2 px-4 h-10'
852+
: 'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
853+
)}
854+
onClick={isDebugModeEnabled ? handleRunWorkflow : handleMultipleRuns}
855+
disabled={isExecuting || isMultiRunning || isCancelling}
856+
>
857+
{isCancelling ? (
858+
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
859+
) : isDebugModeEnabled ? (
860+
<Bug className={cn('h-3.5 w-3.5 mr-1.5', 'fill-current stroke-current')} />
861+
) : (
862+
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
863+
)}
864+
{isCancelling
865+
? 'Cancelling...'
866+
: isMultiRunning
867+
? `Running (${completedRuns}/${runCount})`
868+
: isExecuting
869+
? isDebugging
870+
? 'Debugging'
871+
: 'Running'
872+
: isDebugModeEnabled
873+
? 'Debug'
874+
: runCount === 1
875+
? 'Run'
876+
: `Run (${runCount})`}
877+
</Button>
878+
</TooltipTrigger>
879+
<TooltipContent>
880+
{isDebugModeEnabled
881+
? 'Debug Workflow'
882+
: runCount === 1
883+
? 'Run Workflow'
884+
: `Run Workflow ${runCount} times`}
885+
<span className="text-xs text-muted-foreground ml-1">
886+
{getKeyboardShortcutText('Enter', true)}
887+
</span>
888+
</TooltipContent>
889+
</Tooltip>
861890

862891
{/* Dropdown Trigger - Only show when not in debug mode and not multi-running */}
863892
{!isDebugModeEnabled && !isMultiRunning && (
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use client'
2+
3+
import { useEffect, useMemo } from 'react'
4+
5+
/**
6+
* Detect if the current platform is Mac
7+
*/
8+
export function isMacPlatform() {
9+
if (typeof navigator === 'undefined') return false
10+
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
11+
}
12+
13+
/**
14+
* Get a formatted keyboard shortcut string for display
15+
* @param key The key part of the shortcut (e.g., "Enter")
16+
* @param requiresCmd Whether the shortcut requires Cmd/Ctrl
17+
* @param requiresShift Whether the shortcut requires Shift
18+
* @param requiresAlt Whether the shortcut requires Alt/Option
19+
*/
20+
export function getKeyboardShortcutText(
21+
key: string,
22+
requiresCmd = false,
23+
requiresShift = false,
24+
requiresAlt = false
25+
) {
26+
const isMac = isMacPlatform()
27+
const cmdKey = isMac ? '⌘' : 'Ctrl'
28+
const altKey = isMac ? '⌥' : 'Alt'
29+
const shiftKey = '⇧'
30+
31+
const parts: string[] = []
32+
if (requiresCmd) parts.push(cmdKey)
33+
if (requiresShift) parts.push(shiftKey)
34+
if (requiresAlt) parts.push(altKey)
35+
parts.push(key)
36+
37+
return parts.join('+')
38+
}
39+
40+
/**
41+
* Hook to manage keyboard shortcuts
42+
* @param onRunWorkflow - Function to run when Cmd/Ctrl+Enter is pressed
43+
* @param isDisabled - Whether shortcuts should be disabled
44+
*/
45+
export function useKeyboardShortcuts(onRunWorkflow: () => void, isDisabled = false) {
46+
// Memoize the platform detection
47+
const isMac = useMemo(() => isMacPlatform(), [])
48+
49+
useEffect(() => {
50+
const handleKeyDown = (event: KeyboardEvent) => {
51+
// Run workflow with Cmd+Enter (Mac) or Ctrl+Enter (Windows/Linux)
52+
if (event.key === 'Enter' && ((isMac && event.metaKey) || (!isMac && event.ctrlKey))) {
53+
// Don't trigger if user is typing in an input, textarea, or contenteditable element
54+
const activeElement = document.activeElement
55+
const isEditableElement =
56+
activeElement instanceof HTMLInputElement ||
57+
activeElement instanceof HTMLTextAreaElement ||
58+
activeElement?.hasAttribute('contenteditable')
59+
60+
if (!isEditableElement && !isDisabled) {
61+
event.preventDefault()
62+
onRunWorkflow()
63+
}
64+
}
65+
}
66+
67+
window.addEventListener('keydown', handleKeyDown)
68+
return () => window.removeEventListener('keydown', handleKeyDown)
69+
}, [onRunWorkflow, isDisabled, isMac])
70+
}

0 commit comments

Comments
 (0)