diff --git a/sim/app/w/[id]/components/control-bar/control-bar.tsx b/sim/app/w/[id]/components/control-bar/control-bar.tsx
index 65e708a78e..75f6c9fdc9 100644
--- a/sim/app/w/[id]/components/control-bar/control-bar.tsx
+++ b/sim/app/w/[id]/components/control-bar/control-bar.tsx
@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation'
import { formatDistanceToNow } from 'date-fns'
-import { Bell, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react'
+import { Bell, ChevronDown, History, Loader2, Play, Rocket, Store, Trash2 } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,
@@ -22,6 +22,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
+import { Progress } from '@/components/ui/progress'
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
@@ -35,6 +36,9 @@ import { NotificationDropdownItem } from './components/notification-dropdown-ite
const logger = createLogger('ControlBar')
+// Predefined run count options
+const RUN_COUNT_OPTIONS = [1, 5, 10, 25, 50, 100]
+
/**
* Control bar for managing workflows - handles editing, deletion, deployment,
* history, notifications and execution.
@@ -76,6 +80,12 @@ export function ControlBar() {
// Marketplace modal state
const [isMarketplaceModalOpen, setIsMarketplaceModalOpen] = useState(false)
+ // Multiple runs state
+ const [runCount, setRunCount] = useState(1)
+ const [completedRuns, setCompletedRuns] = useState(0)
+ const [isMultiRunning, setIsMultiRunning] = useState(false)
+ const [showRunProgress, setShowRunProgress] = useState(false)
+
// Get notifications for current workflow
const workflowNotifications = activeWorkflowId
? getWorkflowNotifications(activeWorkflowId)
@@ -286,6 +296,35 @@ export function ControlBar() {
setIsMarketplaceModalOpen(true)
}
+ /**
+ * Handle multiple workflow runs
+ */
+ const handleMultipleRuns = async () => {
+ if (isExecuting || isMultiRunning || runCount <= 0) return
+
+ // Reset state for a new batch of runs
+ setCompletedRuns(0)
+ setIsMultiRunning(true)
+ setShowRunProgress(runCount > 1)
+
+ try {
+ // Run the workflow multiple times sequentially
+ for (let i = 0; i < runCount; i++) {
+ await handleRunWorkflow()
+ setCompletedRuns(i + 1)
+ }
+ } catch (error) {
+ logger.error('Error during multiple workflow runs:', { error })
+ addNotification('error', 'Failed to complete all workflow runs', activeWorkflowId)
+ } finally {
+ setIsMultiRunning(false)
+ // Keep progress visible for a moment after completion
+ if (runCount > 1) {
+ setTimeout(() => setShowRunProgress(false), 2000)
+ }
+ }
+ }
+
/**
* Render workflow name section (editable/non-editable)
*/
@@ -449,44 +488,51 @@ export function ControlBar() {
/**
* Render notifications dropdown
*/
- const renderNotificationsDropdown = () => (
-
-
-
-
-
-
-
- {!notificationsOpen && Notifications}
-
+ const renderNotificationsDropdown = () => {
+ // Ensure we're only showing notifications for the current workflow
+ const currentWorkflowNotifications = activeWorkflowId
+ ? notifications.filter((n) => n.workflowId === activeWorkflowId)
+ : []
- {workflowNotifications.length === 0 ? (
-
-
- No new notifications
-
-
- ) : (
-
- {[...workflowNotifications]
- .sort((a, b) => b.timestamp - a.timestamp)
- .map((notification) => (
-
- ))}
-
- )}
-
- )
+ return (
+
+
+
+
+
+
+
+ {!notificationsOpen && Notifications}
+
+
+ {currentWorkflowNotifications.length === 0 ? (
+
+
+ No new notifications
+
+
+ ) : (
+
+ {[...currentWorkflowNotifications]
+ .sort((a, b) => b.timestamp - a.timestamp)
+ .map((notification) => (
+
+ ))}
+
+ )}
+
+ )
+ }
/**
* Render publish button
@@ -520,31 +566,78 @@ export function ControlBar() {
)
/**
- * Render run workflow button
+ * Render run workflow button with multi-run dropdown
*/
const renderRunButton = () => (
-
+
+
+ {/* Main Run Button */}
+
+
+ {/* Dropdown Trigger */}
+
+
+
+
+
+ {RUN_COUNT_OPTIONS.map((count) => (
+ setRunCount(count)}
+ className={cn('justify-center', runCount === count && 'bg-muted')}
+ >
+ {count}
+
+ ))}
+
+
+
+
)
return (
diff --git a/sim/app/w/[id]/components/notifications/notifications.tsx b/sim/app/w/[id]/components/notifications/notifications.tsx
index 15f66b1848..c477be9777 100644
--- a/sim/app/w/[id]/components/notifications/notifications.tsx
+++ b/sim/app/w/[id]/components/notifications/notifications.tsx
@@ -18,6 +18,7 @@ import { createLogger } from '@/lib/logs/console-logger'
import { cn } from '@/lib/utils'
import { useNotificationStore } from '@/stores/notifications/store'
import { Notification } from '@/stores/notifications/types'
+import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('Notifications')
@@ -92,19 +93,22 @@ function DeleteApiConfirmation({
export function NotificationList() {
// Store access
const { notifications, hideNotification } = useNotificationStore()
+ const { activeWorkflowId } = useWorkflowRegistry()
// Local state
const [fadingNotifications, setFadingNotifications] = useState>(new Set())
- // Filter to only show visible notifications
- const visibleNotifications = notifications.filter((n) => n.isVisible)
+ // Filter to only show visible notifications for the current workflow
+ const visibleNotifications = notifications.filter(
+ (n) => n.isVisible && n.workflowId === activeWorkflowId
+ )
// Handle auto-dismissal of non-persistent notifications
useEffect(() => {
// Setup timers for each notification
const timers: ReturnType[] = []
- notifications.forEach((notification) => {
+ visibleNotifications.forEach((notification) => {
// Skip if already hidden or marked as persistent
if (!notification.isVisible || notification.options?.isPersistent) return
@@ -128,7 +132,7 @@ export function NotificationList() {
// Cleanup timers on unmount or when notifications change
return () => timers.forEach(clearTimeout)
- }, [notifications, hideNotification])
+ }, [visibleNotifications, hideNotification])
// Early return if no notifications to show
if (visibleNotifications.length === 0) return null
diff --git a/sim/components/ui/progress.tsx b/sim/components/ui/progress.tsx
new file mode 100644
index 0000000000..e41fb24df8
--- /dev/null
+++ b/sim/components/ui/progress.tsx
@@ -0,0 +1,24 @@
+'use client'
+
+import * as React from 'react'
+import * as ProgressPrimitive from '@radix-ui/react-progress'
+import { cn } from '@/lib/utils'
+
+const Progress = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, value, ...props }, ref) => (
+
+
+
+))
+Progress.displayName = ProgressPrimitive.Root.displayName
+
+export { Progress }
diff --git a/sim/package-lock.json b/sim/package-lock.json
index 957e593378..e312645e8a 100644
--- a/sim/package-lock.json
+++ b/sim/package-lock.json
@@ -7,6 +7,7 @@
"": {
"name": "sim",
"version": "0.1.0",
+ "license": "Apache-2.0",
"workspaces": [
"packages/@sim/*",
"packages/simstudio"
@@ -23,6 +24,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.2",
@@ -3753,6 +3755,30 @@
}
}
},
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.2.tgz",
+ "integrity": "sha512-u1IgJFQ4zNAUTjGdDL5dcl/U8ntOR6jsnhxKb5RKp5Ozwl88xKR9EqRZOe/Mk8tnx0x5tNUe2F+MzsyjqMg0MA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz",
diff --git a/sim/package.json b/sim/package.json
index a7d7050424..771d0e332f 100644
--- a/sim/package.json
+++ b/sim/package.json
@@ -38,6 +38,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.4",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.5",
+ "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.2",