Feat/add mothership manual workflow runs (#3520)

* Add run and open workflow buttons in workflow preview

* Send log request message after manual workflow run

* Make edges in embedded workflow non-editable

* Change chat to pass in log as additional context

* Revert "Change chat to pass in log as additional context"

This reverts commit e957dffb2f.

* Revert "Send log request message after manual workflow run"

This reverts commit 0fb92751f0.

* Move run and workflow icons to tab bar

* Simplify boolean condition

---------

Co-authored-by: Theodore Li <theo@sim.ai>
This commit is contained in:
Theodore Li
2026-03-11 12:59:56 -07:00
committed by GitHub
parent 10e8eeda67
commit d347b8c4af
6 changed files with 137 additions and 36 deletions

View File

@@ -1,2 +1,2 @@
export { ResourceContent } from './resource-content'
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'
export { ResourceTabs } from './resource-tabs'

View File

@@ -1 +1 @@
export { ResourceContent } from './resource-content'
export { EmbeddedWorkflowActions, ResourceContent } from './resource-content'

View File

@@ -1,14 +1,23 @@
'use client'
import { lazy, Suspense, useMemo } from 'react'
import { Skeleton } from '@/components/emcn'
import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react'
import { Square } from 'lucide-react'
import { useRouter } from 'next/navigation'
import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn'
import { WorkflowIcon } from '@/components/icons'
import {
FileViewer,
type PreviewMode,
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution'
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useCurrentWorkflowExecution } from '@/stores/execution'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const Workflow = lazy(() => import('@/app/workspace/[workspaceId]/w/[workflowId]/workflow'))
@@ -58,6 +67,88 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource
}
}
interface EmbeddedWorkflowActionsProps {
workspaceId: string
workflowId: string
}
export function EmbeddedWorkflowActions({
workspaceId,
workflowId,
}: EmbeddedWorkflowActionsProps) {
const router = useRouter()
const { navigateToSettings } = useSettingsNavigation()
const { userPermissions: effectivePermissions } = useWorkspacePermissionsContext()
const setActiveWorkflow = useWorkflowRegistry((state) => state.setActiveWorkflow)
const { handleRunWorkflow, handleCancelExecution } = useWorkflowExecution()
const { isExecuting } = useCurrentWorkflowExecution()
const { usageExceeded } = useUsageLimits()
useEffect(() => {
setActiveWorkflow(workflowId)
}, [setActiveWorkflow, workflowId])
const isRunButtonDisabled = !isExecuting && (!effectivePermissions.canRead && !effectivePermissions.isLoading)
const handleRun = useCallback(async () => {
if (isExecuting) {
await handleCancelExecution()
return
}
if (usageExceeded) {
navigateToSettings({ section: 'subscription' })
return
}
await handleRunWorkflow()
}, [handleCancelExecution, handleRunWorkflow, isExecuting, navigateToSettings, usageExceeded])
const handleOpenWorkflow = useCallback(() => {
router.push(`/workspace/${workspaceId}/w/${workflowId}`)
}, [router, workspaceId, workflowId])
return (
<>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={handleOpenWorkflow}
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
aria-label='Open workflow'
>
<WorkflowIcon className='h-[16px] w-[16px] text-[var(--text-icon)]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Open Workflow</p>
</Tooltip.Content>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={() => void handleRun()}
disabled={isRunButtonDisabled}
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
aria-label={isExecuting ? 'Stop workflow' : 'Run workflow'}
>
{isExecuting ? (
<Square className='h-[16px] w-[16px] text-[var(--text-icon)]' />
) : (
<PlayOutline className='h-[16px] w-[16px] text-[var(--text-icon)]' />
)}
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>{isExecuting ? 'Stop' : 'Run'}</p>
</Tooltip.Content>
</Tooltip.Root>
</>
)
}
interface EmbeddedFileProps {
workspaceId: string
fileId: string

View File

@@ -1,6 +1,6 @@
'use client'
import type { ElementType, SVGProps } from 'react'
import type { ElementType, ReactNode, SVGProps } from 'react'
import { Button, Tooltip } from '@/components/emcn'
import { PanelLeft, Table as TableIcon } from '@/components/emcn/icons'
import { WorkflowIcon } from '@/components/icons'
@@ -48,6 +48,7 @@ interface ResourceTabsProps {
onCollapse: () => void
previewMode?: PreviewMode
onCyclePreviewMode?: () => void
actions?: ReactNode
}
const RESOURCE_ICONS: Record<Exclude<MothershipResourceType, 'file'>, ElementType> = {
@@ -73,6 +74,7 @@ export function ResourceTabs({
onCollapse,
previewMode,
onCyclePreviewMode,
actions,
}: ResourceTabsProps) {
return (
<div className='flex shrink-0 items-center border-[var(--border)] border-b px-[16px] py-[8.5px]'>
@@ -118,26 +120,29 @@ export function ResourceTabs({
)
})}
</div>
{previewMode && onCyclePreviewMode && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={onCyclePreviewMode}
className='ml-auto shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
aria-label='Cycle preview mode'
>
<PreviewModeIcon
mode={previewMode}
className='h-[16px] w-[16px] text-[var(--text-icon)]'
/>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Preview mode</p>
</Tooltip.Content>
</Tooltip.Root>
)}
<div className='ml-auto flex shrink-0 items-center gap-[6px]'>
{actions}
{previewMode && onCyclePreviewMode && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='subtle'
onClick={onCyclePreviewMode}
className='shrink-0 bg-transparent px-[8px] py-[5px] text-[12px]'
aria-label='Cycle preview mode'
>
<PreviewModeIcon
mode={previewMode}
className='h-[16px] w-[16px] text-[var(--text-icon)]'
/>
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='bottom'>
<p>Preview mode</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
</div>
)
}

View File

@@ -5,7 +5,7 @@ import { cn } from '@/lib/core/utils/cn'
import { getFileExtension } from '@/lib/uploads/utils/file-utils'
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import { ResourceContent, ResourceTabs } from './components'
import { EmbeddedWorkflowActions, ResourceContent, ResourceTabs } from './components'
const PREVIEWABLE_EXTENSIONS = new Set(['md', 'html', 'htm', 'csv'])
const PREVIEW_ONLY_EXTENSIONS = new Set(['html', 'htm'])
@@ -53,6 +53,11 @@ export function MothershipView({
const isActivePreviewable =
active?.type === 'file' && PREVIEWABLE_EXTENSIONS.has(getFileExtension(active.title))
const headerActions =
active?.type === 'workflow' ? (
<EmbeddedWorkflowActions workspaceId={workspaceId} workflowId={active.id} />
) : null
return (
<div
className={cn(
@@ -67,6 +72,7 @@ export function MothershipView({
activeId={active?.id ?? null}
onSelect={onSelectResource}
onCollapse={onCollapse}
actions={headerActions}
previewMode={isActivePreviewable ? previewMode : undefined}
onCyclePreviewMode={isActivePreviewable ? handleCyclePreview : undefined}
/>

View File

@@ -379,7 +379,8 @@ const WorkflowContent = React.memo(
const showTrainingModal = useCopilotTrainingStore((state) => state.showModal)
const { handleRunFromBlock, handleRunUntilBlock } = useWorkflowExecution()
const { handleRunFromBlock, handleRunUntilBlock, handleRunWorkflow, handleCancelExecution } =
useWorkflowExecution()
const snapToGridSize = useSnapToGridSize()
const snapToGrid = snapToGridSize > 0
@@ -612,7 +613,6 @@ const WorkflowContent = React.memo(
const { userPermissions, workspacePermissions, permissionsError } =
useWorkspacePermissionsContext()
/** Returns read-only permissions when viewing snapshot, otherwise user permissions. */
const effectivePermissions = useMemo(() => {
if (currentWorkflow.isSnapshotView) {
@@ -625,7 +625,6 @@ const WorkflowContent = React.memo(
}
return userPermissions
}, [userPermissions, currentWorkflow.isSnapshotView])
const {
collaborativeBatchAddEdges,
collaborativeBatchRemoveEdges,
@@ -3836,9 +3835,9 @@ const WorkflowContent = React.memo(
edges={edgesWithSelection}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={effectivePermissions.canEdit ? onConnect : undefined}
onConnectStart={effectivePermissions.canEdit ? onConnectStart : undefined}
onConnectEnd={effectivePermissions.canEdit ? onConnectEnd : undefined}
onConnect={!embedded && effectivePermissions.canEdit ? onConnect : undefined}
onConnectStart={!embedded && effectivePermissions.canEdit ? onConnectStart : undefined}
onConnectEnd={!embedded && effectivePermissions.canEdit ? onConnectEnd : undefined}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
onMouseDown={handleCanvasMouseDown}
@@ -3859,7 +3858,7 @@ const WorkflowContent = React.memo(
connectionLineStyle={connectionLineStyle}
connectionLineType={ConnectionLineType.SmoothStep}
onPaneClick={onPaneClick}
onEdgeClick={onEdgeClick}
onEdgeClick={embedded ? undefined : onEdgeClick}
onNodeClick={handleNodeClick}
onPaneContextMenu={handlePaneContextMenu}
onNodeContextMenu={handleNodeContextMenu}
@@ -3876,8 +3875,8 @@ const WorkflowContent = React.memo(
nodesDraggable={!embedded && effectivePermissions.canEdit}
draggable={false}
noWheelClassName='allow-scroll'
edgesFocusable={true}
edgesUpdatable={effectivePermissions.canEdit}
edgesFocusable={!embedded}
edgesUpdatable={!embedded && effectivePermissions.canEdit}
className={`workflow-container h-full bg-[var(--bg)] transition-opacity duration-150 ${reactFlowStyles} ${isCanvasReady ? 'opacity-100' : 'opacity-0'} ${isHandMode ? 'canvas-mode-hand' : 'canvas-mode-cursor'}`}
onNodeDrag={effectivePermissions.canEdit ? onNodeDrag : undefined}
onNodeDragStop={effectivePermissions.canEdit ? onNodeDragStop : undefined}
@@ -3985,7 +3984,7 @@ const WorkflowContent = React.memo(
{!embedded && <DiffControls />}
</div>
{!embedded && <Terminal />}
<Terminal />
</div>
{!embedded && <Panel />}