Send log request message after manual workflow run

This commit is contained in:
Theodore Li
2026-03-11 11:36:13 -07:00
parent 50923788ef
commit 0fb92751f0
5 changed files with 97 additions and 17 deletions
@@ -8,6 +8,7 @@ import {
} from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
import { Table } from '@/app/workspace/[workspaceId]/tables/[tableId]/components'
import type { ExecutionResult } from '@/executor/types'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
const Workflow = lazy(() => import('@/app/workspace/[workspaceId]/w/[workflowId]/workflow'))
@@ -24,6 +25,7 @@ interface ResourceContentProps {
workspaceId: string
resource: MothershipResource
previewMode?: PreviewMode
onWorkflowRunComplete?: (workflowName: string, result: ExecutionResult) => Promise<void>
}
/**
@@ -31,7 +33,12 @@ interface ResourceContentProps {
* Handles table, file, and workflow resource types with appropriate
* embedded rendering for each.
*/
export function ResourceContent({ workspaceId, resource, previewMode }: ResourceContentProps) {
export function ResourceContent({
workspaceId,
resource,
previewMode,
onWorkflowRunComplete,
}: ResourceContentProps) {
switch (resource.type) {
case 'table':
return <Table key={resource.id} workspaceId={workspaceId} tableId={resource.id} embedded />
@@ -49,7 +56,17 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource
case 'workflow':
return (
<Suspense fallback={LOADING_SKELETON}>
<Workflow key={resource.id} workspaceId={workspaceId} workflowId={resource.id} embedded />
<Workflow
key={resource.id}
workspaceId={workspaceId}
workflowId={resource.id}
embedded
onManualRunComplete={
onWorkflowRunComplete
? (result) => onWorkflowRunComplete(resource.title, result)
: undefined
}
/>
</Suspense>
)
@@ -5,6 +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 type { ExecutionResult } from '@/executor/types'
import { ResourceContent, ResourceTabs } from './components'
const PREVIEWABLE_EXTENSIONS = new Set(['md', 'html', 'htm', 'csv'])
@@ -21,6 +22,7 @@ interface MothershipViewProps {
resources: MothershipResource[]
activeResourceId: string | null
onSelectResource: (id: string) => void
onWorkflowRunComplete?: (workflowName: string, result: ExecutionResult) => Promise<void>
onCollapse: () => void
isCollapsed: boolean
className?: string
@@ -36,6 +38,7 @@ export function MothershipView({
resources,
activeResourceId,
onSelectResource,
onWorkflowRunComplete,
onCollapse,
isCollapsed,
className,
@@ -76,6 +79,7 @@ export function MothershipView({
workspaceId={workspaceId}
resource={active}
previewMode={isActivePreviewable ? previewMode : undefined}
onWorkflowRunComplete={onWorkflowRunComplete}
/>
)}
</div>
@@ -165,6 +165,7 @@ export function Home({ chatId }: HomeProps = {}) {
messages,
isSending,
sendMessage,
sendWorkflowResultToChat,
stopGeneration,
resources,
activeResourceId,
@@ -348,6 +349,7 @@ export function Home({ chatId }: HomeProps = {}) {
resources={resources}
activeResourceId={activeResourceId}
onSelectResource={setActiveResourceId}
onWorkflowRunComplete={sendWorkflowResultToChat}
onCollapse={collapseResource}
isCollapsed={isResourceCollapsed}
className={animateResourcePanel ? 'animate-slide-in-right' : undefined}
@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname } from 'next/navigation'
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
import type { ExecutionResult } from '@/executor/types'
import { tableKeys } from '@/hooks/queries/tables'
import {
type TaskChatHistory,
@@ -40,6 +41,7 @@ export interface UseChatReturn {
isSending: boolean
error: string | null
sendMessage: (message: string, fileAttachments?: FileAttachmentForApi[]) => Promise<void>
sendWorkflowResultToChat: (workflowName: string, result: ExecutionResult) => Promise<void>
stopGeneration: () => Promise<void>
resources: MothershipResource[]
activeResourceId: string | null
@@ -51,6 +53,10 @@ const STATE_TO_STATUS: Record<string, ToolCallStatus> = {
error: 'error',
} as const
function formatWorkflowResultMessage(_workflowName: string, _result: ExecutionResult): string {
return 'I ran the workflow, please check the logs'
}
function mapStoredBlock(block: TaskStoredContentBlock): ContentBlock {
const mapped: ContentBlock = {
type: block.type as ContentBlockType,
@@ -146,6 +152,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
const [error, setError] = useState<string | null>(null)
const [resources, setResources] = useState<MothershipResource[]>([])
const [activeResourceId, setActiveResourceId] = useState<string | null>(null)
const [queuedMessages, setQueuedMessages] = useState<string[]>([])
const abortControllerRef = useRef<AbortController | null>(null)
const chatIdRef = useRef<string | undefined>(initialChatId)
const appliedChatIdRef = useRef<string | undefined>(undefined)
@@ -682,6 +689,28 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
[workspaceId, queryClient, processSSEStream, finalize, persistPartialResponse]
)
const sendWorkflowResultToChat = useCallback(
async (workflowName: string, result: ExecutionResult) => {
const message = formatWorkflowResultMessage(workflowName, result)
if (sendingRef.current || isSending) {
setQueuedMessages((prev) => [...prev, message])
return
}
await sendMessage(message)
},
[isSending, sendMessage]
)
useEffect(() => {
if (isSending || queuedMessages.length === 0) return
const [nextMessage, ...rest] = queuedMessages
setQueuedMessages(rest)
void sendMessage(nextMessage)
}, [isSending, queuedMessages, sendMessage])
const stopGeneration = useCallback(async () => {
if (sendingRef.current) {
await persistPartialResponse()
@@ -719,6 +748,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
isSending,
error,
sendMessage,
sendWorkflowResultToChat,
stopGeneration,
resources,
activeResourceId,
@@ -39,6 +39,7 @@ import { CanvasMenu } from '@/app/workspace/[workspaceId]/w/[workflowId]/compone
import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors'
import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index'
import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block'
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
import type { SubflowNodeData } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/subflow-node'
import { TrainingModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/training-modal/training-modal'
import { WorkflowBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block'
@@ -71,6 +72,7 @@ import {
import { useSocket } from '@/app/workspace/providers/socket-provider'
import { getBlock } from '@/blocks'
import { isAnnotationOnlyBlock } from '@/executor/constants'
import type { ExecutionResult } from '@/executor/types'
import { useWorkspaceEnvironment } from '@/hooks/queries/environment'
import { useAutoConnect, useSnapToGridSize } from '@/hooks/queries/general-settings'
import { useCanvasViewport } from '@/hooks/use-canvas-viewport'
@@ -91,7 +93,6 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { getUniqueBlockName, prepareBlockState } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
import type { BlockState } from '@/stores/workflows/workflow/types'
import { useUsageLimits } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/hooks'
/** Lazy-loaded components for non-critical UI that can load after initial render */
const LazyChat = lazy(() =>
@@ -234,6 +235,7 @@ interface WorkflowContentProps {
workspaceId?: string
workflowId?: string
embedded?: boolean
onManualRunComplete?: (result: ExecutionResult) => Promise<void> | void
}
const WorkflowContent = React.memo(
@@ -241,6 +243,7 @@ const WorkflowContent = React.memo(
workspaceId: propWorkspaceId,
workflowId: propWorkflowId,
embedded,
onManualRunComplete,
}: WorkflowContentProps = {}) => {
const [isCanvasReady, setIsCanvasReady] = useState(false)
const [potentialParentId, setPotentialParentId] = useState<string | null>(null)
@@ -859,7 +862,7 @@ const WorkflowContent = React.memo(
const getLastExecutionSnapshot = useExecutionStore((s) => s.getLastExecutionSnapshot)
const canRunWorkflow = effectivePermissions.canRead
const isRunButtonDisabled = !isExecuting && (!canRunWorkflow && !effectivePermissions.isLoading)
const isRunButtonDisabled = !isExecuting && !canRunWorkflow && !effectivePermissions.isLoading
const openSubscriptionSettings = useCallback(() => {
navigateToSettings({ section: 'subscription' })
@@ -876,11 +879,27 @@ const WorkflowContent = React.memo(
return
}
await handleRunWorkflow()
const result = await handleRunWorkflow()
if (
!onManualRunComplete ||
!result ||
!('success' in result) ||
'stream' in result ||
result.status === 'cancelled' ||
result.status === 'paused'
) {
return
}
Promise.resolve(onManualRunComplete(result)).catch((error) => {
logger.error('Failed to send embedded workflow result to mothership chat', { error })
})
}, [
handleCancelExecution,
handleRunWorkflow,
isExecuting,
onManualRunComplete,
openSubscriptionSettings,
usageExceeded,
])
@@ -3873,9 +3892,9 @@ const WorkflowContent = React.memo(
{showTrainingModal && <TrainingModal />}
{embedded && (
<div className='absolute bottom-[28px] left-1/2 z-[31] flex -translate-x-1/2 items-center gap-[2px] rounded-[10px] border border-[var(--border)] bg-[var(--surface-3)] p-[2px] shadow-sm backdrop-blur-sm'>
<div className='-translate-x-1/2 absolute bottom-[28px] left-1/2 z-[31] flex items-center gap-[2px] rounded-[10px] border border-[var(--border)] bg-[var(--surface-3)] p-[2px] shadow-sm backdrop-blur-sm'>
<Button
className='h-[30px] rounded-[8px] gap-[8px] px-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
className='h-[30px] gap-[8px] rounded-[8px] px-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
variant='ghost'
onClick={() => void handleEmbeddedRun()}
disabled={!isExecuting && isRunButtonDisabled}
@@ -3888,7 +3907,7 @@ const WorkflowContent = React.memo(
{isExecuting ? 'Stop' : 'Run'}
</Button>
<Button
className='h-[30px] rounded-[8px] gap-[8px] px-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
className='h-[30px] gap-[8px] rounded-[8px] px-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-4)] hover:text-[var(--text-primary)]'
variant='ghost'
onClick={handleOpenFullWorkflow}
>
@@ -4081,18 +4100,26 @@ interface WorkflowProps {
workspaceId?: string
workflowId?: string
embedded?: boolean
onManualRunComplete?: (result: ExecutionResult) => Promise<void> | void
}
/** Workflow page with ReactFlowProvider and error boundary wrapper. */
const Workflow = React.memo(({ workspaceId, workflowId, embedded }: WorkflowProps = {}) => {
return (
<ReactFlowProvider>
<ErrorBoundary>
<WorkflowContent workspaceId={workspaceId} workflowId={workflowId} embedded={embedded} />
</ErrorBoundary>
</ReactFlowProvider>
)
})
const Workflow = React.memo(
({ workspaceId, workflowId, embedded, onManualRunComplete }: WorkflowProps = {}) => {
return (
<ReactFlowProvider>
<ErrorBoundary>
<WorkflowContent
workspaceId={workspaceId}
workflowId={workflowId}
embedded={embedded}
onManualRunComplete={onManualRunComplete}
/>
</ErrorBoundary>
</ReactFlowProvider>
)
}
)
Workflow.displayName = 'Workflow'