mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-15 08:55:05 -05:00
Compare commits
4 Commits
feat/mult-
...
active-exe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42d09ef8cb | ||
|
|
cbb98a0868 | ||
|
|
5b0532d473 | ||
|
|
3ef6b05035 |
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
|
|||||||
|
|
||||||
<BlockInfoCard
|
<BlockInfoCard
|
||||||
type="google_books"
|
type="google_books"
|
||||||
color="#FFFFFF"
|
color="#E0E0E0"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
## Usage Instructions
|
## Usage Instructions
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ Retrieve an object from an AWS S3 bucket
|
|||||||
| --------- | ---- | -------- | ----------- |
|
| --------- | ---- | -------- | ----------- |
|
||||||
| `accessKeyId` | string | Yes | Your AWS Access Key ID |
|
| `accessKeyId` | string | Yes | Your AWS Access Key ID |
|
||||||
| `secretAccessKey` | string | Yes | Your AWS Secret Access Key |
|
| `secretAccessKey` | string | Yes | Your AWS Secret Access Key |
|
||||||
|
| `region` | string | No | Optional region override when URL does not include region \(e.g., us-east-1, eu-west-1\) |
|
||||||
| `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) |
|
| `s3Uri` | string | Yes | S3 Object URL \(e.g., https://bucket.s3.region.amazonaws.com/path/to/file\) |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ Send messages to Slack channels or direct messages. Supports Slack mrkdwn format
|
|||||||
| `channel` | string | No | Slack channel ID \(e.g., C1234567890\) |
|
| `channel` | string | No | Slack channel ID \(e.g., C1234567890\) |
|
||||||
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
|
| `dmUserId` | string | No | Slack user ID for direct messages \(e.g., U1234567890\) |
|
||||||
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
| `text` | string | Yes | Message text to send \(supports Slack mrkdwn formatting\) |
|
||||||
| `thread_ts` | string | No | Thread timestamp to reply to \(creates thread reply\) |
|
| `threadTs` | string | No | Thread timestamp to reply to \(creates thread reply\) |
|
||||||
| `files` | file[] | No | Files to attach to the message |
|
| `files` | file[] | No | Files to attach to the message |
|
||||||
|
|
||||||
#### Output
|
#### Output
|
||||||
|
|||||||
@@ -238,6 +238,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg
|
|||||||
finalSystemPrompt += currentTimeContext
|
finalSystemPrompt += currentTimeContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (generationType === 'cron-expression') {
|
||||||
|
finalSystemPrompt +=
|
||||||
|
'\n\nIMPORTANT: Return ONLY the raw cron expression (e.g., "0 9 * * 1-5"). Do NOT wrap it in markdown code blocks, backticks, or quotes. Do NOT include any explanation or text before or after the expression.'
|
||||||
|
}
|
||||||
|
|
||||||
if (generationType === 'json-object') {
|
if (generationType === 'json-object') {
|
||||||
finalSystemPrompt +=
|
finalSystemPrompt +=
|
||||||
'\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.'
|
'\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.'
|
||||||
|
|||||||
@@ -239,8 +239,13 @@ export const ComboBox = memo(function ComboBox({
|
|||||||
*/
|
*/
|
||||||
const defaultOptionValue = useMemo(() => {
|
const defaultOptionValue = useMemo(() => {
|
||||||
if (defaultValue !== undefined) {
|
if (defaultValue !== undefined) {
|
||||||
|
// Validate that the default value exists in the available (filtered) options
|
||||||
|
const defaultInOptions = evaluatedOptions.find((opt) => getOptionValue(opt) === defaultValue)
|
||||||
|
if (defaultInOptions) {
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
// Default not available (e.g. provider disabled) — fall through to other fallbacks
|
||||||
|
}
|
||||||
|
|
||||||
// For model field, default to claude-sonnet-4-5 if available
|
// For model field, default to claude-sonnet-4-5 if available
|
||||||
if (subBlockId === 'model') {
|
if (subBlockId === 'model') {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
|
||||||
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
|
import { SubBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block'
|
||||||
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig as BlockSubBlockConfig } from '@/blocks/types'
|
||||||
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
|
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||||
|
|
||||||
interface ToolSubBlockRendererProps {
|
interface ToolSubBlockRendererProps {
|
||||||
blockId: string
|
blockId: string
|
||||||
@@ -44,53 +45,43 @@ export function ToolSubBlockRenderer({
|
|||||||
canonicalToggle,
|
canonicalToggle,
|
||||||
}: ToolSubBlockRendererProps) {
|
}: ToolSubBlockRendererProps) {
|
||||||
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
|
const syntheticId = `${subBlockId}-tool-${toolIndex}-${effectiveParamId}`
|
||||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, syntheticId)
|
|
||||||
|
|
||||||
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
|
const toolParamValue = toolParams?.[effectiveParamId] ?? ''
|
||||||
const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type)
|
const isObjectType = OBJECT_SUBBLOCK_TYPES.has(subBlock.type)
|
||||||
|
|
||||||
const lastPushedToStoreRef = useRef<string | null>(null)
|
const syncedRef = useRef<string | null>(null)
|
||||||
const lastPushedToParamsRef = useRef<string | null>(null)
|
const onParamChangeRef = useRef(onParamChange)
|
||||||
|
onParamChangeRef.current = onParamChange
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!toolParamValue && lastPushedToStoreRef.current === null) {
|
const unsub = useSubBlockStore.subscribe((state, prevState) => {
|
||||||
lastPushedToStoreRef.current = toolParamValue
|
const wfId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
lastPushedToParamsRef.current = toolParamValue
|
if (!wfId) return
|
||||||
return
|
const newVal = state.workflowValues[wfId]?.[blockId]?.[syntheticId]
|
||||||
}
|
const oldVal = prevState.workflowValues[wfId]?.[blockId]?.[syntheticId]
|
||||||
if (toolParamValue !== lastPushedToStoreRef.current) {
|
if (newVal === oldVal) return
|
||||||
lastPushedToStoreRef.current = toolParamValue
|
const stringified =
|
||||||
lastPushedToParamsRef.current = toolParamValue
|
newVal == null ? '' : typeof newVal === 'string' ? newVal : JSON.stringify(newVal)
|
||||||
|
if (stringified === syncedRef.current) return
|
||||||
|
syncedRef.current = stringified
|
||||||
|
onParamChangeRef.current(toolIndex, effectiveParamId, stringified)
|
||||||
|
})
|
||||||
|
return unsub
|
||||||
|
}, [blockId, syntheticId, toolIndex, effectiveParamId])
|
||||||
|
|
||||||
if (isObjectType && typeof toolParamValue === 'string' && toolParamValue) {
|
useEffect(() => {
|
||||||
|
if (toolParamValue === syncedRef.current) return
|
||||||
|
syncedRef.current = toolParamValue
|
||||||
|
if (isObjectType && toolParamValue) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(toolParamValue)
|
const parsed = JSON.parse(toolParamValue)
|
||||||
if (typeof parsed === 'object' && parsed !== null) {
|
if (typeof parsed === 'object' && parsed !== null) {
|
||||||
setStoreValue(parsed)
|
useSubBlockStore.getState().setValue(blockId, syntheticId, parsed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {}
|
||||||
// Not valid JSON — fall through to set as string
|
|
||||||
}
|
}
|
||||||
}
|
useSubBlockStore.getState().setValue(blockId, syntheticId, toolParamValue)
|
||||||
setStoreValue(toolParamValue)
|
}, [toolParamValue, blockId, syntheticId, isObjectType])
|
||||||
}
|
|
||||||
}, [toolParamValue, setStoreValue, isObjectType])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (storeValue == null && lastPushedToParamsRef.current === null) return
|
|
||||||
const stringValue =
|
|
||||||
storeValue == null
|
|
||||||
? ''
|
|
||||||
: typeof storeValue === 'string'
|
|
||||||
? storeValue
|
|
||||||
: JSON.stringify(storeValue)
|
|
||||||
if (stringValue !== lastPushedToParamsRef.current) {
|
|
||||||
lastPushedToParamsRef.current = stringValue
|
|
||||||
lastPushedToStoreRef.current = stringValue
|
|
||||||
onParamChange(toolIndex, effectiveParamId, stringValue)
|
|
||||||
}
|
|
||||||
}, [storeValue, toolIndex, effectiveParamId, onParamChange])
|
|
||||||
|
|
||||||
const visibility = subBlock.paramVisibility ?? 'user-or-llm'
|
const visibility = subBlock.paramVisibility ?? 'user-or-llm'
|
||||||
const isOptionalForUser = visibility !== 'user-only'
|
const isOptionalForUser = visibility !== 'user-only'
|
||||||
|
|||||||
@@ -1741,10 +1741,22 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
) : null
|
) : null
|
||||||
})()}
|
})()}
|
||||||
|
|
||||||
{requiresOAuth && oauthConfig && (
|
{(() => {
|
||||||
<div className='relative min-w-0 space-y-[6px]'>
|
const renderedElements: React.ReactNode[] = []
|
||||||
|
|
||||||
|
const showOAuth =
|
||||||
|
requiresOAuth && oauthConfig && tool.params?.authMethod !== 'bot_token'
|
||||||
|
|
||||||
|
const renderOAuthAccount = (): React.ReactNode => {
|
||||||
|
if (!showOAuth || !oauthConfig) return null
|
||||||
|
const credentialSubBlock = toolBlock?.subBlocks?.find(
|
||||||
|
(s) => s.type === 'oauth-input'
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<div key='oauth-account' className='relative min-w-0 space-y-[6px]'>
|
||||||
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
|
<div className='font-medium text-[13px] text-[var(--text-primary)]'>
|
||||||
Account <span className='ml-0.5'>*</span>
|
{credentialSubBlock?.title || 'Account'}{' '}
|
||||||
|
<span className='ml-0.5'>*</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full min-w-0'>
|
<div className='w-full min-w-0'>
|
||||||
<ToolCredentialSelector
|
<ToolCredentialSelector
|
||||||
@@ -1754,8 +1766,7 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
}
|
}
|
||||||
provider={oauthConfig.provider as OAuthProvider}
|
provider={oauthConfig.provider as OAuthProvider}
|
||||||
requiredScopes={
|
requiredScopes={
|
||||||
toolBlock?.subBlocks?.find((sb) => sb.id === 'credential')
|
credentialSubBlock?.requiredScopes ||
|
||||||
?.requiredScopes ||
|
|
||||||
getCanonicalScopesForProvider(oauthConfig.provider)
|
getCanonicalScopesForProvider(oauthConfig.provider)
|
||||||
}
|
}
|
||||||
serviceId={oauthConfig.provider}
|
serviceId={oauthConfig.provider}
|
||||||
@@ -1763,29 +1774,10 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{(() => {
|
|
||||||
const renderedElements: React.ReactNode[] = []
|
|
||||||
|
|
||||||
if (useSubBlocks && displaySubBlocks.length > 0) {
|
|
||||||
const coveredParamIds = new Set(
|
|
||||||
displaySubBlocks.flatMap((sb) => {
|
|
||||||
const ids = [sb.id]
|
|
||||||
if (sb.canonicalParamId) ids.push(sb.canonicalParamId)
|
|
||||||
const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
|
|
||||||
if (cId) {
|
|
||||||
const group = toolCanonicalIndex?.groupsById[cId]
|
|
||||||
if (group) {
|
|
||||||
if (group.basicId) ids.push(group.basicId)
|
|
||||||
ids.push(...group.advancedIds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ids
|
|
||||||
})
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
displaySubBlocks.forEach((sb) => {
|
const renderSubBlock = (sb: BlockSubBlockConfig): React.ReactNode => {
|
||||||
const effectiveParamId = sb.id
|
const effectiveParamId = sb.id
|
||||||
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
|
const canonicalId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
|
||||||
const canonicalGroup = canonicalId
|
const canonicalGroup = canonicalId
|
||||||
@@ -1806,8 +1798,7 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
? {
|
? {
|
||||||
mode: canonicalMode,
|
mode: canonicalMode,
|
||||||
onToggle: () => {
|
onToggle: () => {
|
||||||
const nextMode =
|
const nextMode = canonicalMode === 'advanced' ? 'basic' : 'advanced'
|
||||||
canonicalMode === 'advanced' ? 'basic' : 'advanced'
|
|
||||||
collaborativeSetBlockCanonicalMode(
|
collaborativeSetBlockCanonicalMode(
|
||||||
blockId,
|
blockId,
|
||||||
`${tool.type}:${canonicalId}`,
|
`${tool.type}:${canonicalId}`,
|
||||||
@@ -1821,7 +1812,7 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
? sb
|
? sb
|
||||||
: { ...sb, title: formatParameterLabel(effectiveParamId) }
|
: { ...sb, title: formatParameterLabel(effectiveParamId) }
|
||||||
|
|
||||||
renderedElements.push(
|
return (
|
||||||
<ToolSubBlockRenderer
|
<ToolSubBlockRenderer
|
||||||
key={sb.id}
|
key={sb.id}
|
||||||
blockId={blockId}
|
blockId={blockId}
|
||||||
@@ -1835,7 +1826,65 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
canonicalToggle={canonicalToggleProp}
|
canonicalToggle={canonicalToggleProp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useSubBlocks && displaySubBlocks.length > 0) {
|
||||||
|
const allBlockSubBlocks = toolBlock?.subBlocks || []
|
||||||
|
const coveredParamIds = new Set(
|
||||||
|
allBlockSubBlocks.flatMap((sb) => {
|
||||||
|
const ids = [sb.id]
|
||||||
|
if (sb.canonicalParamId) ids.push(sb.canonicalParamId)
|
||||||
|
const cId = toolCanonicalIndex?.canonicalIdBySubBlockId[sb.id]
|
||||||
|
if (cId) {
|
||||||
|
const group = toolCanonicalIndex?.groupsById[cId]
|
||||||
|
if (group) {
|
||||||
|
if (group.basicId) ids.push(group.basicId)
|
||||||
|
ids.push(...group.advancedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
})
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
type RenderItem =
|
||||||
|
| { kind: 'subblock'; sb: BlockSubBlockConfig }
|
||||||
|
| { kind: 'oauth' }
|
||||||
|
|
||||||
|
const renderOrder: RenderItem[] = displaySubBlocks.map((sb) => ({
|
||||||
|
kind: 'subblock' as const,
|
||||||
|
sb,
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (showOAuth) {
|
||||||
|
const credentialIdx = allBlockSubBlocks.findIndex(
|
||||||
|
(sb) => sb.type === 'oauth-input'
|
||||||
|
)
|
||||||
|
if (credentialIdx >= 0) {
|
||||||
|
const sbPositions = new Map(allBlockSubBlocks.map((sb, i) => [sb.id, i]))
|
||||||
|
const insertAt = renderOrder.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.kind === 'subblock' &&
|
||||||
|
(sbPositions.get(item.sb.id) ?? Number.POSITIVE_INFINITY) >
|
||||||
|
credentialIdx
|
||||||
|
)
|
||||||
|
if (insertAt === -1) {
|
||||||
|
renderOrder.push({ kind: 'oauth' })
|
||||||
|
} else {
|
||||||
|
renderOrder.splice(insertAt, 0, { kind: 'oauth' })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
renderOrder.unshift({ kind: 'oauth' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of renderOrder) {
|
||||||
|
if (item.kind === 'oauth') {
|
||||||
|
const el = renderOAuthAccount()
|
||||||
|
if (el) renderedElements.push(el)
|
||||||
|
} else {
|
||||||
|
renderedElements.push(renderSubBlock(item.sb))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const uncoveredParams = displayParams.filter(
|
const uncoveredParams = displayParams.filter(
|
||||||
(param) =>
|
(param) =>
|
||||||
@@ -1873,6 +1922,11 @@ export const ToolInput = memo(function ToolInput({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const el = renderOAuthAccount()
|
||||||
|
if (el) renderedElements.push(el)
|
||||||
|
}
|
||||||
|
|
||||||
const filteredParams = displayParams.filter((param) =>
|
const filteredParams = displayParams.filter((param) =>
|
||||||
evaluateParameterCondition(param, tool)
|
evaluateParameterCondition(param, tool)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,21 +88,38 @@ export function useTerminalFilters() {
|
|||||||
let result = entries
|
let result = entries
|
||||||
|
|
||||||
if (hasActiveFilters) {
|
if (hasActiveFilters) {
|
||||||
result = entries.filter((entry) => {
|
// Determine which top-level entries pass the filters
|
||||||
// Block ID filter
|
const visibleBlockIds = new Set<string>()
|
||||||
if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) {
|
for (const entry of entries) {
|
||||||
return false
|
if (entry.parentWorkflowBlockId) continue
|
||||||
}
|
|
||||||
|
|
||||||
// Status filter
|
let passes = true
|
||||||
if (filters.statuses.size > 0) {
|
if (filters.blockIds.size > 0 && !filters.blockIds.has(entry.blockId)) {
|
||||||
|
passes = false
|
||||||
|
}
|
||||||
|
if (passes && filters.statuses.size > 0) {
|
||||||
const isError = !!entry.error
|
const isError = !!entry.error
|
||||||
const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info')
|
const hasStatus = isError ? filters.statuses.has('error') : filters.statuses.has('info')
|
||||||
if (!hasStatus) return false
|
if (!hasStatus) passes = false
|
||||||
|
}
|
||||||
|
if (passes) {
|
||||||
|
visibleBlockIds.add(entry.blockId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
// Propagate visibility to child workflow entries (handles arbitrary nesting).
|
||||||
})
|
// Keep iterating until no new children are discovered.
|
||||||
|
let prevSize = 0
|
||||||
|
while (visibleBlockIds.size !== prevSize) {
|
||||||
|
prevSize = visibleBlockIds.size
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.parentWorkflowBlockId && visibleBlockIds.has(entry.parentWorkflowBlockId)) {
|
||||||
|
visibleBlockIds.add(entry.blockId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = entries.filter((entry) => visibleBlockIds.has(entry.blockId))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by executionOrder (monotonically increasing integer from server)
|
// Sort by executionOrder (monotonically increasing integer from server)
|
||||||
|
|||||||
@@ -339,7 +339,8 @@ const SubflowNodeRow = memo(function SubflowNodeRow({
|
|||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry node component - dispatches to appropriate component based on node type
|
* Entry node component - dispatches to appropriate component based on node type.
|
||||||
|
* Handles recursive rendering for workflow nodes with arbitrarily nested children.
|
||||||
*/
|
*/
|
||||||
const EntryNodeRow = memo(function EntryNodeRow({
|
const EntryNodeRow = memo(function EntryNodeRow({
|
||||||
node,
|
node,
|
||||||
@@ -380,6 +381,98 @@ const EntryNodeRow = memo(function EntryNodeRow({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nodeType === 'workflow') {
|
||||||
|
const { entry, children } = node
|
||||||
|
const BlockIcon = getBlockIcon(entry.blockType)
|
||||||
|
const hasError = Boolean(entry.error) || children.some((c) => c.entry.error)
|
||||||
|
const bgColor = getBlockColor(entry.blockType)
|
||||||
|
const nodeId = entry.id
|
||||||
|
const isExpanded = expandedNodes.has(nodeId)
|
||||||
|
const hasChildren = children.length > 0
|
||||||
|
const isSelected = selectedEntryId === entry.id
|
||||||
|
const isRunning = Boolean(entry.isRunning)
|
||||||
|
const isCanceled = Boolean(entry.isCanceled)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='flex min-w-0 flex-col'>
|
||||||
|
{/* Workflow Block Header */}
|
||||||
|
<div
|
||||||
|
data-entry-id={entry.id}
|
||||||
|
className={clsx(
|
||||||
|
ROW_STYLES.base,
|
||||||
|
'h-[26px]',
|
||||||
|
isSelected ? ROW_STYLES.selected : ROW_STYLES.hover
|
||||||
|
)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (hasChildren) {
|
||||||
|
onToggleNode(nodeId)
|
||||||
|
}
|
||||||
|
onSelectEntry(entry)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
|
||||||
|
<div
|
||||||
|
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
|
||||||
|
style={{ background: bgColor }}
|
||||||
|
>
|
||||||
|
{BlockIcon && <BlockIcon className='h-[9px] w-[9px] text-white' />}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'min-w-0 truncate font-medium text-[13px]',
|
||||||
|
hasError
|
||||||
|
? 'text-[var(--text-error)]'
|
||||||
|
: isSelected || isExpanded
|
||||||
|
? 'text-[var(--text-primary)]'
|
||||||
|
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{entry.blockName}
|
||||||
|
</span>
|
||||||
|
{hasChildren && (
|
||||||
|
<ChevronDown
|
||||||
|
className={clsx(
|
||||||
|
'h-[8px] w-[8px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]',
|
||||||
|
!isExpanded && '-rotate-90'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
'flex-shrink-0 font-medium text-[13px]',
|
||||||
|
!isRunning &&
|
||||||
|
(isCanceled ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]')
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<StatusDisplay
|
||||||
|
isRunning={isRunning}
|
||||||
|
isCanceled={isCanceled}
|
||||||
|
formattedDuration={formatDuration(entry.durationMs, { precision: 2 }) ?? '-'}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nested Child Workflow Blocks (recursive) */}
|
||||||
|
{isExpanded && hasChildren && (
|
||||||
|
<div className={ROW_STYLES.nested}>
|
||||||
|
{children.map((child) => (
|
||||||
|
<EntryNodeRow
|
||||||
|
key={child.entry.id}
|
||||||
|
node={child}
|
||||||
|
selectedEntryId={selectedEntryId}
|
||||||
|
onSelectEntry={onSelectEntry}
|
||||||
|
expandedNodes={expandedNodes}
|
||||||
|
onToggleNode={onToggleNode}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Regular block
|
// Regular block
|
||||||
return (
|
return (
|
||||||
<BlockRow
|
<BlockRow
|
||||||
@@ -555,6 +648,8 @@ export const Terminal = memo(function Terminal() {
|
|||||||
const uniqueBlocks = useMemo(() => {
|
const uniqueBlocks = useMemo(() => {
|
||||||
const blocksMap = new Map<string, { blockId: string; blockName: string; blockType: string }>()
|
const blocksMap = new Map<string, { blockId: string; blockName: string; blockType: string }>()
|
||||||
allWorkflowEntries.forEach((entry) => {
|
allWorkflowEntries.forEach((entry) => {
|
||||||
|
// Skip child workflow entries — they use synthetic IDs and shouldn't appear in filters
|
||||||
|
if (entry.parentWorkflowBlockId) return
|
||||||
if (!blocksMap.has(entry.blockId)) {
|
if (!blocksMap.has(entry.blockId)) {
|
||||||
blocksMap.set(entry.blockId, {
|
blocksMap.set(entry.blockId, {
|
||||||
blockId: entry.blockId,
|
blockId: entry.blockId,
|
||||||
@@ -667,19 +762,22 @@ export const Terminal = memo(function Terminal() {
|
|||||||
|
|
||||||
const newestExec = executionGroups[0]
|
const newestExec = executionGroups[0]
|
||||||
|
|
||||||
// Collect all node IDs that should be expanded (subflows and their iterations)
|
// Collect all expandable node IDs recursively (subflows, iterations, and workflow nodes)
|
||||||
const nodeIdsToExpand: string[] = []
|
const nodeIdsToExpand: string[] = []
|
||||||
for (const node of newestExec.entryTree) {
|
const collectExpandableNodes = (nodes: EntryNode[]) => {
|
||||||
if (node.nodeType === 'subflow' && node.children.length > 0) {
|
for (const node of nodes) {
|
||||||
|
if (node.children.length === 0) continue
|
||||||
|
if (
|
||||||
|
node.nodeType === 'subflow' ||
|
||||||
|
node.nodeType === 'iteration' ||
|
||||||
|
node.nodeType === 'workflow'
|
||||||
|
) {
|
||||||
nodeIdsToExpand.push(node.entry.id)
|
nodeIdsToExpand.push(node.entry.id)
|
||||||
// Also expand all iteration children
|
collectExpandableNodes(node.children)
|
||||||
for (const iterNode of node.children) {
|
|
||||||
if (iterNode.nodeType === 'iteration') {
|
|
||||||
nodeIdsToExpand.push(iterNode.entry.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
collectExpandableNodes(newestExec.entryTree)
|
||||||
|
|
||||||
if (nodeIdsToExpand.length > 0) {
|
if (nodeIdsToExpand.length > 0) {
|
||||||
setExpandedNodes((prev) => {
|
setExpandedNodes((prev) => {
|
||||||
|
|||||||
@@ -120,10 +120,10 @@ export function isSubflowBlockType(blockType: string): boolean {
|
|||||||
/**
|
/**
|
||||||
* Node type for the tree structure
|
* Node type for the tree structure
|
||||||
*/
|
*/
|
||||||
export type EntryNodeType = 'block' | 'subflow' | 'iteration'
|
export type EntryNodeType = 'block' | 'subflow' | 'iteration' | 'workflow'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Entry node for tree structure - represents a block, subflow, or iteration
|
* Entry node for tree structure - represents a block, subflow, iteration, or workflow
|
||||||
*/
|
*/
|
||||||
export interface EntryNode {
|
export interface EntryNode {
|
||||||
/** The console entry (for blocks) or synthetic entry (for subflows/iterations) */
|
/** The console entry (for blocks) or synthetic entry (for subflows/iterations) */
|
||||||
@@ -175,12 +175,17 @@ interface IterationGroup {
|
|||||||
* Sorts by start time to ensure chronological order.
|
* Sorts by start time to ensure chronological order.
|
||||||
*/
|
*/
|
||||||
function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
||||||
// Separate regular blocks from iteration entries
|
// Separate regular blocks from iteration entries and child workflow entries
|
||||||
const regularBlocks: ConsoleEntry[] = []
|
const regularBlocks: ConsoleEntry[] = []
|
||||||
const iterationEntries: ConsoleEntry[] = []
|
const iterationEntries: ConsoleEntry[] = []
|
||||||
|
const childWorkflowEntries = new Map<string, ConsoleEntry[]>()
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.iterationType && entry.iterationCurrent !== undefined) {
|
if (entry.parentWorkflowBlockId) {
|
||||||
|
const existing = childWorkflowEntries.get(entry.parentWorkflowBlockId) || []
|
||||||
|
existing.push(entry)
|
||||||
|
childWorkflowEntries.set(entry.parentWorkflowBlockId, existing)
|
||||||
|
} else if (entry.iterationType && entry.iterationCurrent !== undefined) {
|
||||||
iterationEntries.push(entry)
|
iterationEntries.push(entry)
|
||||||
} else {
|
} else {
|
||||||
regularBlocks.push(entry)
|
regularBlocks.push(entry)
|
||||||
@@ -338,12 +343,53 @@ function buildEntryTree(entries: ConsoleEntry[]): EntryNode[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build nodes for regular blocks
|
/**
|
||||||
const regularNodes: EntryNode[] = regularBlocks.map((entry) => ({
|
* Recursively builds child nodes for workflow blocks.
|
||||||
|
* Handles multi-level nesting where a child workflow block itself has children.
|
||||||
|
*/
|
||||||
|
const buildWorkflowChildNodes = (parentBlockId: string): EntryNode[] => {
|
||||||
|
const childEntries = childWorkflowEntries.get(parentBlockId)
|
||||||
|
if (!childEntries || childEntries.length === 0) return []
|
||||||
|
|
||||||
|
childEntries.sort((a, b) => {
|
||||||
|
const aTime = new Date(a.startedAt || a.timestamp).getTime()
|
||||||
|
const bTime = new Date(b.startedAt || b.timestamp).getTime()
|
||||||
|
return aTime - bTime
|
||||||
|
})
|
||||||
|
|
||||||
|
return childEntries.map((child) => {
|
||||||
|
const nestedChildren = buildWorkflowChildNodes(child.blockId)
|
||||||
|
if (nestedChildren.length > 0) {
|
||||||
|
return {
|
||||||
|
entry: child,
|
||||||
|
children: nestedChildren,
|
||||||
|
nodeType: 'workflow' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
entry: child,
|
||||||
|
children: [],
|
||||||
|
nodeType: 'block' as const,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build nodes for regular blocks, promoting workflow blocks with children to 'workflow' nodes
|
||||||
|
const regularNodes: EntryNode[] = regularBlocks.map((entry) => {
|
||||||
|
const childNodes = buildWorkflowChildNodes(entry.blockId)
|
||||||
|
if (childNodes.length > 0) {
|
||||||
|
return {
|
||||||
|
entry,
|
||||||
|
children: childNodes,
|
||||||
|
nodeType: 'workflow' as const,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
entry,
|
entry,
|
||||||
children: [],
|
children: [],
|
||||||
nodeType: 'block' as const,
|
nodeType: 'block' as const,
|
||||||
}))
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Combine all nodes and sort by executionOrder ascending (oldest first, top-down)
|
// Combine all nodes and sort by executionOrder ascending (oldest first, top-down)
|
||||||
const allNodes = [...subflowNodes, ...regularNodes]
|
const allNodes = [...subflowNodes, ...regularNodes]
|
||||||
|
|||||||
@@ -38,7 +38,11 @@ import { useCurrentWorkflowExecution, useExecutionStore } from '@/stores/executi
|
|||||||
import { useNotificationStore } from '@/stores/notifications'
|
import { useNotificationStore } from '@/stores/notifications'
|
||||||
import { useVariablesStore } from '@/stores/panel'
|
import { useVariablesStore } from '@/stores/panel'
|
||||||
import { useEnvironmentStore } from '@/stores/settings/environment'
|
import { useEnvironmentStore } from '@/stores/settings/environment'
|
||||||
import { useTerminalConsoleStore } from '@/stores/terminal'
|
import {
|
||||||
|
extractChildWorkflowEntries,
|
||||||
|
hasChildTraceSpans,
|
||||||
|
useTerminalConsoleStore,
|
||||||
|
} from '@/stores/terminal'
|
||||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { mergeSubblockState } from '@/stores/workflows/utils'
|
import { mergeSubblockState } from '@/stores/workflows/utils'
|
||||||
@@ -63,6 +67,7 @@ interface BlockEventHandlerConfig {
|
|||||||
executionIdRef: { current: string }
|
executionIdRef: { current: string }
|
||||||
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
|
workflowEdges: Array<{ id: string; target: string; sourceHandle?: string | null }>
|
||||||
activeBlocksSet: Set<string>
|
activeBlocksSet: Set<string>
|
||||||
|
activeBlockRefCounts: Map<string, number>
|
||||||
accumulatedBlockLogs: BlockLog[]
|
accumulatedBlockLogs: BlockLog[]
|
||||||
accumulatedBlockStates: Map<string, BlockState>
|
accumulatedBlockStates: Map<string, BlockState>
|
||||||
executedBlockIds: Set<string>
|
executedBlockIds: Set<string>
|
||||||
@@ -309,6 +314,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -328,9 +334,18 @@ export function useWorkflowExecution() {
|
|||||||
const updateActiveBlocks = (blockId: string, isActive: boolean) => {
|
const updateActiveBlocks = (blockId: string, isActive: boolean) => {
|
||||||
if (!workflowId) return
|
if (!workflowId) return
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
|
const count = activeBlockRefCounts.get(blockId) ?? 0
|
||||||
|
activeBlockRefCounts.set(blockId, count + 1)
|
||||||
activeBlocksSet.add(blockId)
|
activeBlocksSet.add(blockId)
|
||||||
} else {
|
} else {
|
||||||
|
const count = activeBlockRefCounts.get(blockId) ?? 1
|
||||||
|
const next = count - 1
|
||||||
|
if (next <= 0) {
|
||||||
|
activeBlockRefCounts.delete(blockId)
|
||||||
activeBlocksSet.delete(blockId)
|
activeBlocksSet.delete(blockId)
|
||||||
|
} else {
|
||||||
|
activeBlockRefCounts.set(blockId, next)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setActiveBlocks(workflowId, new Set(activeBlocksSet))
|
setActiveBlocks(workflowId, new Set(activeBlocksSet))
|
||||||
}
|
}
|
||||||
@@ -506,6 +521,20 @@ export function useWorkflowExecution() {
|
|||||||
addConsoleEntry(data, data.output as NormalizedBlockOutput)
|
addConsoleEntry(data, data.output as NormalizedBlockOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract child workflow trace spans into separate console entries
|
||||||
|
if (data.blockType === 'workflow' && hasChildTraceSpans(data.output)) {
|
||||||
|
const childEntries = extractChildWorkflowEntries({
|
||||||
|
parentBlockId: data.blockId,
|
||||||
|
executionId: executionIdRef.current,
|
||||||
|
executionOrder: data.executionOrder,
|
||||||
|
workflowId: workflowId!,
|
||||||
|
childTraceSpans: data.output.childTraceSpans,
|
||||||
|
})
|
||||||
|
for (const entry of childEntries) {
|
||||||
|
addConsole(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (onBlockCompleteCallback) {
|
if (onBlockCompleteCallback) {
|
||||||
onBlockCompleteCallback(data.blockId, data.output).catch((error) => {
|
onBlockCompleteCallback(data.blockId, data.output).catch((error) => {
|
||||||
logger.error('Error in onBlockComplete callback:', error)
|
logger.error('Error in onBlockComplete callback:', error)
|
||||||
@@ -1280,6 +1309,7 @@ export function useWorkflowExecution() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
const streamedContent = new Map<string, string>()
|
const streamedContent = new Map<string, string>()
|
||||||
const accumulatedBlockLogs: BlockLog[] = []
|
const accumulatedBlockLogs: BlockLog[] = []
|
||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
@@ -1292,6 +1322,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -1902,6 +1933,7 @@ export function useWorkflowExecution() {
|
|||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
const executedBlockIds = new Set<string>()
|
const executedBlockIds = new Set<string>()
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const blockHandlers = buildBlockEventHandlers({
|
const blockHandlers = buildBlockEventHandlers({
|
||||||
@@ -1909,6 +1941,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
@@ -2104,6 +2137,7 @@ export function useWorkflowExecution() {
|
|||||||
|
|
||||||
const workflowEdges = useWorkflowStore.getState().edges
|
const workflowEdges = useWorkflowStore.getState().edges
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
const accumulatedBlockLogs: BlockLog[] = []
|
const accumulatedBlockLogs: BlockLog[] = []
|
||||||
const accumulatedBlockStates = new Map<string, BlockState>()
|
const accumulatedBlockStates = new Map<string, BlockState>()
|
||||||
const executedBlockIds = new Set<string>()
|
const executedBlockIds = new Set<string>()
|
||||||
@@ -2115,6 +2149,7 @@ export function useWorkflowExecution() {
|
|||||||
executionIdRef,
|
executionIdRef,
|
||||||
workflowEdges,
|
workflowEdges,
|
||||||
activeBlocksSet,
|
activeBlocksSet,
|
||||||
|
activeBlockRefCounts,
|
||||||
accumulatedBlockLogs,
|
accumulatedBlockLogs,
|
||||||
accumulatedBlockStates,
|
accumulatedBlockStates,
|
||||||
executedBlockIds,
|
executedBlockIds,
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
import type { ExecutionResult, StreamingExecution } from '@/executor/types'
|
||||||
import { useExecutionStore } from '@/stores/execution'
|
import { useExecutionStore } from '@/stores/execution'
|
||||||
import { useTerminalConsoleStore } from '@/stores/terminal'
|
import {
|
||||||
|
extractChildWorkflowEntries,
|
||||||
|
hasChildTraceSpans,
|
||||||
|
useTerminalConsoleStore,
|
||||||
|
} from '@/stores/terminal'
|
||||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||||
|
|
||||||
@@ -39,6 +43,7 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
const workflowEdges = useWorkflowStore.getState().edges
|
const workflowEdges = useWorkflowStore.getState().edges
|
||||||
|
|
||||||
const activeBlocksSet = new Set<string>()
|
const activeBlocksSet = new Set<string>()
|
||||||
|
const activeBlockRefCounts = new Map<string, number>()
|
||||||
|
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
input: options.workflowInput,
|
input: options.workflowInput,
|
||||||
@@ -103,6 +108,8 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'block:started': {
|
case 'block:started': {
|
||||||
|
const startCount = activeBlockRefCounts.get(event.data.blockId) ?? 0
|
||||||
|
activeBlockRefCounts.set(event.data.blockId, startCount + 1)
|
||||||
activeBlocksSet.add(event.data.blockId)
|
activeBlocksSet.add(event.data.blockId)
|
||||||
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
@@ -115,8 +122,14 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'block:completed':
|
case 'block:completed': {
|
||||||
|
const completeCount = activeBlockRefCounts.get(event.data.blockId) ?? 1
|
||||||
|
if (completeCount <= 1) {
|
||||||
|
activeBlockRefCounts.delete(event.data.blockId)
|
||||||
activeBlocksSet.delete(event.data.blockId)
|
activeBlocksSet.delete(event.data.blockId)
|
||||||
|
} else {
|
||||||
|
activeBlockRefCounts.set(event.data.blockId, completeCount - 1)
|
||||||
|
}
|
||||||
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
setBlockRunStatus(wfId, event.data.blockId, 'success')
|
setBlockRunStatus(wfId, event.data.blockId, 'success')
|
||||||
@@ -140,13 +153,34 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
iterationContainerId: event.data.iterationContainerId,
|
iterationContainerId: event.data.iterationContainerId,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extract child workflow trace spans into separate console entries
|
||||||
|
if (event.data.blockType === 'workflow' && hasChildTraceSpans(event.data.output)) {
|
||||||
|
const childEntries = extractChildWorkflowEntries({
|
||||||
|
parentBlockId: event.data.blockId,
|
||||||
|
executionId,
|
||||||
|
executionOrder: event.data.executionOrder,
|
||||||
|
workflowId: activeWorkflowId,
|
||||||
|
childTraceSpans: event.data.output.childTraceSpans,
|
||||||
|
})
|
||||||
|
for (const entry of childEntries) {
|
||||||
|
addConsole(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.onBlockComplete) {
|
if (options.onBlockComplete) {
|
||||||
options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {})
|
options.onBlockComplete(event.data.blockId, event.data.output).catch(() => {})
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'block:error':
|
case 'block:error': {
|
||||||
|
const errorCount = activeBlockRefCounts.get(event.data.blockId) ?? 1
|
||||||
|
if (errorCount <= 1) {
|
||||||
|
activeBlockRefCounts.delete(event.data.blockId)
|
||||||
activeBlocksSet.delete(event.data.blockId)
|
activeBlocksSet.delete(event.data.blockId)
|
||||||
|
} else {
|
||||||
|
activeBlockRefCounts.set(event.data.blockId, errorCount - 1)
|
||||||
|
}
|
||||||
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
setActiveBlocks(wfId, new Set(activeBlocksSet))
|
||||||
|
|
||||||
setBlockRunStatus(wfId, event.data.blockId, 'error')
|
setBlockRunStatus(wfId, event.data.blockId, 'error')
|
||||||
@@ -171,6 +205,7 @@ export async function executeWorkflowWithFullLogging(
|
|||||||
iterationContainerId: event.data.iterationContainerId,
|
iterationContainerId: event.data.iterationContainerId,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'execution:completed':
|
case 'execution:completed':
|
||||||
executionResult = {
|
executionResult = {
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { AgentIcon } from '@/components/icons'
|
import { AgentIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { AuthMode } from '@/blocks/types'
|
import { AuthMode } from '@/blocks/types'
|
||||||
import { getApiKeyCondition } from '@/blocks/utils'
|
import { getApiKeyCondition, getModelOptions } from '@/blocks/utils'
|
||||||
import {
|
import {
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
getMaxTemperature,
|
getMaxTemperature,
|
||||||
getProviderIcon,
|
|
||||||
getReasoningEffortValuesForModel,
|
getReasoningEffortValuesForModel,
|
||||||
getThinkingLevelsForModel,
|
getThinkingLevelsForModel,
|
||||||
getVerbosityValuesForModel,
|
getVerbosityValuesForModel,
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
providers,
|
providers,
|
||||||
supportsTemperature,
|
supportsTemperature,
|
||||||
} from '@/providers/utils'
|
} from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers'
|
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('AgentBlock')
|
const logger = createLogger('AgentBlock')
|
||||||
@@ -121,21 +119,7 @@ Return ONLY the JSON array.`,
|
|||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'claude-sonnet-4-5',
|
defaultValue: 'claude-sonnet-4-5',
|
||||||
options: () => {
|
options: getModelOptions,
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const vllmModels = providersState.providers.vllm.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(
|
|
||||||
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
|
||||||
)
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'vertexCredential',
|
id: 'vertexCredential',
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { ChartBarIcon } from '@/components/icons'
|
import { ChartBarIcon } from '@/components/icons'
|
||||||
import type { BlockConfig, ParamType } from '@/blocks/types'
|
import type { BlockConfig, ParamType } from '@/blocks/types'
|
||||||
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
import {
|
||||||
|
getModelOptions,
|
||||||
|
getProviderCredentialSubBlocks,
|
||||||
|
PROVIDER_CREDENTIAL_INPUTS,
|
||||||
|
} from '@/blocks/utils'
|
||||||
import type { ProviderId } from '@/providers/types'
|
import type { ProviderId } from '@/providers/types'
|
||||||
import { getBaseModelProviders, getProviderIcon } from '@/providers/utils'
|
import { getBaseModelProviders } from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
const logger = createLogger('EvaluatorBlock')
|
const logger = createLogger('EvaluatorBlock')
|
||||||
@@ -175,21 +178,7 @@ export const EvaluatorBlock: BlockConfig<EvaluatorResponse> = {
|
|||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
defaultValue: 'claude-sonnet-4-5',
|
defaultValue: 'claude-sonnet-4-5',
|
||||||
options: () => {
|
options: getModelOptions,
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const vllmModels = providersState.providers.vllm.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(
|
|
||||||
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
|
||||||
)
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
...getProviderCredentialSubBlocks(),
|
...getProviderCredentialSubBlocks(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { ShieldCheckIcon } from '@/components/icons'
|
import { ShieldCheckIcon } from '@/components/icons'
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
import {
|
||||||
import { getProviderIcon } from '@/providers/utils'
|
getModelOptions,
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
getProviderCredentialSubBlocks,
|
||||||
|
PROVIDER_CREDENTIAL_INPUTS,
|
||||||
|
} from '@/blocks/utils'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
export interface GuardrailsResponse extends ToolResponse {
|
export interface GuardrailsResponse extends ToolResponse {
|
||||||
@@ -111,21 +113,7 @@ Return ONLY the regex pattern - no explanations, no quotes, no forward slashes,
|
|||||||
type: 'combobox',
|
type: 'combobox',
|
||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
options: () => {
|
options: getModelOptions,
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const vllmModels = providersState.providers.vllm.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(
|
|
||||||
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
|
||||||
)
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
condition: {
|
condition: {
|
||||||
field: 'validationType',
|
field: 'validationType',
|
||||||
value: ['hallucination'],
|
value: ['hallucination'],
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ConnectIcon } from '@/components/icons'
|
import { ConnectIcon } from '@/components/icons'
|
||||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
import {
|
||||||
|
getModelOptions,
|
||||||
|
getProviderCredentialSubBlocks,
|
||||||
|
PROVIDER_CREDENTIAL_INPUTS,
|
||||||
|
} from '@/blocks/utils'
|
||||||
import type { ProviderId } from '@/providers/types'
|
import type { ProviderId } from '@/providers/types'
|
||||||
import { getBaseModelProviders, getProviderIcon } from '@/providers/utils'
|
import { getBaseModelProviders } from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers'
|
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
interface RouterResponse extends ToolResponse {
|
interface RouterResponse extends ToolResponse {
|
||||||
@@ -134,25 +137,6 @@ Respond with a JSON object containing:
|
|||||||
- reasoning: A brief explanation (1-2 sentences) of why you chose this route`
|
- reasoning: A brief explanation (1-2 sentences) of why you chose this route`
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper to get model options for both router versions.
|
|
||||||
*/
|
|
||||||
const getModelOptions = () => {
|
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const vllmModels = providersState.providers.vllm.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(
|
|
||||||
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
|
||||||
)
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legacy Router Block (block-based routing).
|
* Legacy Router Block (block-based routing).
|
||||||
* Hidden from toolbar but still supported for existing workflows.
|
* Hidden from toolbar but still supported for existing workflows.
|
||||||
|
|||||||
@@ -122,6 +122,25 @@ export const ScheduleBlock: BlockConfig = {
|
|||||||
required: true,
|
required: true,
|
||||||
mode: 'trigger',
|
mode: 'trigger',
|
||||||
condition: { field: 'scheduleType', value: 'custom' },
|
condition: { field: 'scheduleType', value: 'custom' },
|
||||||
|
wandConfig: {
|
||||||
|
enabled: true,
|
||||||
|
prompt: `You are an expert at writing cron expressions. Generate a valid cron expression based on the user's description.
|
||||||
|
|
||||||
|
Cron format: minute hour day-of-month month day-of-week
|
||||||
|
- minute: 0-59
|
||||||
|
- hour: 0-23
|
||||||
|
- day-of-month: 1-31
|
||||||
|
- month: 1-12
|
||||||
|
- day-of-week: 0-7 (0 and 7 are Sunday)
|
||||||
|
|
||||||
|
Special characters: * (any), , (list), - (range), / (step)
|
||||||
|
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Return ONLY the cron expression, nothing else. No explanation, no backticks, no quotes.`,
|
||||||
|
placeholder: 'Describe your schedule (e.g., "every weekday at 9am")',
|
||||||
|
generationType: 'cron-expression',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ Return ONLY the timestamp string - no explanations, no quotes, no extra text.`,
|
|||||||
case 'send': {
|
case 'send': {
|
||||||
baseParams.text = text
|
baseParams.text = text
|
||||||
if (threadTs) {
|
if (threadTs) {
|
||||||
baseParams.thread_ts = threadTs
|
baseParams.threadTs = threadTs
|
||||||
}
|
}
|
||||||
// files is the canonical param from attachmentFiles (basic) or files (advanced)
|
// files is the canonical param from attachmentFiles (basic) or files (advanced)
|
||||||
const normalizedFiles = normalizeFileInput(files)
|
const normalizedFiles = normalizeFileInput(files)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { TranslateIcon } from '@/components/icons'
|
import { TranslateIcon } from '@/components/icons'
|
||||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||||
import { getProviderCredentialSubBlocks, PROVIDER_CREDENTIAL_INPUTS } from '@/blocks/utils'
|
import {
|
||||||
import { getProviderIcon } from '@/providers/utils'
|
getModelOptions,
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
getProviderCredentialSubBlocks,
|
||||||
|
PROVIDER_CREDENTIAL_INPUTS,
|
||||||
|
} from '@/blocks/utils'
|
||||||
|
|
||||||
const getTranslationPrompt = (targetLanguage: string) =>
|
const getTranslationPrompt = (targetLanguage: string) =>
|
||||||
`Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.`
|
`Translate the following text into ${targetLanguage || 'English'}. Output ONLY the translated text with no additional commentary, explanations, or notes.`
|
||||||
@@ -38,18 +40,7 @@ export const TranslateBlock: BlockConfig = {
|
|||||||
type: 'combobox',
|
type: 'combobox',
|
||||||
placeholder: 'Type or select a model...',
|
placeholder: 'Type or select a model...',
|
||||||
required: true,
|
required: true,
|
||||||
options: () => {
|
options: getModelOptions,
|
||||||
const providersState = useProvidersStore.getState()
|
|
||||||
const baseModels = providersState.providers.base.models
|
|
||||||
const ollamaModels = providersState.providers.ollama.models
|
|
||||||
const openrouterModels = providersState.providers.openrouter.models
|
|
||||||
const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels]))
|
|
||||||
|
|
||||||
return allModels.map((model) => {
|
|
||||||
const icon = getProviderIcon(model)
|
|
||||||
return { label: model, id: model, ...(icon && { icon }) }
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
...getProviderCredentialSubBlocks(),
|
...getProviderCredentialSubBlocks(),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export type GenerationType =
|
|||||||
| 'neo4j-parameters'
|
| 'neo4j-parameters'
|
||||||
| 'timestamp'
|
| 'timestamp'
|
||||||
| 'timezone'
|
| 'timezone'
|
||||||
|
| 'cron-expression'
|
||||||
|
|
||||||
export type SubBlockType =
|
export type SubBlockType =
|
||||||
| 'short-input' // Single line input
|
| 'short-input' // Single line input
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
||||||
import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils'
|
import {
|
||||||
|
getHostedModels,
|
||||||
|
getProviderFromModel,
|
||||||
|
getProviderIcon,
|
||||||
|
providers,
|
||||||
|
} from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns model options for combobox subblocks, combining all provider sources.
|
||||||
|
*/
|
||||||
|
export function getModelOptions() {
|
||||||
|
const providersState = useProvidersStore.getState()
|
||||||
|
const baseModels = providersState.providers.base.models
|
||||||
|
const ollamaModels = providersState.providers.ollama.models
|
||||||
|
const vllmModels = providersState.providers.vllm.models
|
||||||
|
const openrouterModels = providersState.providers.openrouter.models
|
||||||
|
const allModels = Array.from(
|
||||||
|
new Set([...baseModels, ...ollamaModels, ...vllmModels, ...openrouterModels])
|
||||||
|
)
|
||||||
|
|
||||||
|
return allModels.map((model) => {
|
||||||
|
const icon = getProviderIcon(model)
|
||||||
|
return { label: model, id: model, ...(icon && { icon }) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a field is included in the dependsOn config.
|
* Checks if a field is included in the dependsOn config.
|
||||||
* Handles both simple array format and object format with all/any fields.
|
* Handles both simple array format and object format with all/any fields.
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ export class BlockExecutor {
|
|||||||
block: SerializedBlock,
|
block: SerializedBlock,
|
||||||
executionOrder: number
|
executionOrder: number
|
||||||
): void {
|
): void {
|
||||||
const blockId = node.id
|
const blockId = node.metadata?.originalBlockId ?? node.id
|
||||||
const blockName = block.metadata?.name ?? blockId
|
const blockName = block.metadata?.name ?? blockId
|
||||||
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ export class BlockExecutor {
|
|||||||
executionOrder: number,
|
executionOrder: number,
|
||||||
endedAt: string
|
endedAt: string
|
||||||
): void {
|
): void {
|
||||||
const blockId = node.id
|
const blockId = node.metadata?.originalBlockId ?? node.id
|
||||||
const blockName = block.metadata?.name ?? blockId
|
const blockName = block.metadata?.name ?? blockId
|
||||||
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
const blockType = block.metadata?.id ?? DEFAULTS.BLOCK_TYPE
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export const vertexProvider: ProviderConfig = {
|
|||||||
executeRequest: async (
|
executeRequest: async (
|
||||||
request: ProviderRequest
|
request: ProviderRequest
|
||||||
): Promise<ProviderResponse | StreamingExecution> => {
|
): Promise<ProviderResponse | StreamingExecution> => {
|
||||||
const vertexProject = env.VERTEX_PROJECT || request.vertexProject
|
const vertexProject = request.vertexProject || env.VERTEX_PROJECT
|
||||||
const vertexLocation = env.VERTEX_LOCATION || request.vertexLocation || 'us-central1'
|
const vertexLocation = request.vertexLocation || env.VERTEX_LOCATION || 'us-central1'
|
||||||
|
|
||||||
if (!vertexProject) {
|
if (!vertexProject) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export { indexedDBStorage } from './storage'
|
export { indexedDBStorage } from './storage'
|
||||||
export { useTerminalConsoleStore } from './store'
|
export { useTerminalConsoleStore } from './store'
|
||||||
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types'
|
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './types'
|
||||||
|
export { extractChildWorkflowEntries, hasChildTraceSpans } from './utils'
|
||||||
|
|||||||
@@ -224,7 +224,11 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
|
|
||||||
const newEntry = get().entries[0]
|
const newEntry = get().entries[0]
|
||||||
|
|
||||||
if (newEntry?.error && newEntry.blockType !== 'cancelled') {
|
if (
|
||||||
|
newEntry?.error &&
|
||||||
|
newEntry.blockType !== 'cancelled' &&
|
||||||
|
!newEntry.parentWorkflowBlockId
|
||||||
|
) {
|
||||||
notifyBlockError({
|
notifyBlockError({
|
||||||
error: newEntry.error,
|
error: newEntry.error,
|
||||||
blockName: newEntry.blockName || 'Unknown Block',
|
blockName: newEntry.blockName || 'Unknown Block',
|
||||||
@@ -249,7 +253,9 @@ export const useTerminalConsoleStore = create<ConsoleStore>()(
|
|||||||
})),
|
})),
|
||||||
|
|
||||||
exportConsoleCSV: (workflowId: string) => {
|
exportConsoleCSV: (workflowId: string) => {
|
||||||
const entries = get().entries.filter((entry) => entry.workflowId === workflowId)
|
const entries = get().entries.filter(
|
||||||
|
(entry) => entry.workflowId === workflowId && !entry.parentWorkflowBlockId
|
||||||
|
)
|
||||||
|
|
||||||
if (entries.length === 0) {
|
if (entries.length === 0) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface ConsoleEntry {
|
|||||||
iterationTotal?: number
|
iterationTotal?: number
|
||||||
iterationType?: SubflowType
|
iterationType?: SubflowType
|
||||||
iterationContainerId?: string
|
iterationContainerId?: string
|
||||||
|
parentWorkflowBlockId?: string
|
||||||
isRunning?: boolean
|
isRunning?: boolean
|
||||||
isCanceled?: boolean
|
isCanceled?: boolean
|
||||||
}
|
}
|
||||||
@@ -44,6 +45,7 @@ export interface ConsoleUpdate {
|
|||||||
iterationTotal?: number
|
iterationTotal?: number
|
||||||
iterationType?: SubflowType
|
iterationType?: SubflowType
|
||||||
iterationContainerId?: string
|
iterationContainerId?: string
|
||||||
|
parentWorkflowBlockId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConsoleStore {
|
export interface ConsoleStore {
|
||||||
|
|||||||
78
apps/sim/stores/terminal/console/utils.ts
Normal file
78
apps/sim/stores/terminal/console/utils.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { TraceSpan } from '@/lib/logs/types'
|
||||||
|
import type { ConsoleEntry } from '@/stores/terminal/console/types'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for extracting child workflow entries from trace spans
|
||||||
|
*/
|
||||||
|
interface ExtractChildWorkflowEntriesParams {
|
||||||
|
parentBlockId: string
|
||||||
|
executionId: string
|
||||||
|
executionOrder: number
|
||||||
|
workflowId: string
|
||||||
|
childTraceSpans: TraceSpan[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts child workflow trace spans into console entry payloads.
|
||||||
|
* Handles recursive nesting for multi-level child workflows by flattening
|
||||||
|
* nested children with a parent block ID chain.
|
||||||
|
*/
|
||||||
|
export function extractChildWorkflowEntries(
|
||||||
|
params: ExtractChildWorkflowEntriesParams
|
||||||
|
): Omit<ConsoleEntry, 'id' | 'timestamp'>[] {
|
||||||
|
const { parentBlockId, executionId, executionOrder, workflowId, childTraceSpans } = params
|
||||||
|
const entries: Omit<ConsoleEntry, 'id' | 'timestamp'>[] = []
|
||||||
|
|
||||||
|
for (const span of childTraceSpans) {
|
||||||
|
if (!span.blockId) continue
|
||||||
|
|
||||||
|
const childBlockId = `child-${parentBlockId}-${span.blockId}`
|
||||||
|
|
||||||
|
entries.push({
|
||||||
|
blockId: childBlockId,
|
||||||
|
blockName: span.name || 'Unknown Block',
|
||||||
|
blockType: span.type || 'unknown',
|
||||||
|
parentWorkflowBlockId: parentBlockId,
|
||||||
|
input: span.input || {},
|
||||||
|
output: (span.output || {}) as ConsoleEntry['output'],
|
||||||
|
durationMs: span.duration,
|
||||||
|
startedAt: span.startTime,
|
||||||
|
endedAt: span.endTime,
|
||||||
|
success: span.status !== 'error',
|
||||||
|
error:
|
||||||
|
span.status === 'error'
|
||||||
|
? (span.output?.error as string) || `${span.name || 'Block'} failed`
|
||||||
|
: undefined,
|
||||||
|
executionId,
|
||||||
|
executionOrder,
|
||||||
|
workflowId,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Recursively extract nested child workflow spans
|
||||||
|
if (span.children && span.children.length > 0 && span.type === 'workflow') {
|
||||||
|
const nestedEntries = extractChildWorkflowEntries({
|
||||||
|
parentBlockId: childBlockId,
|
||||||
|
executionId,
|
||||||
|
executionOrder,
|
||||||
|
workflowId,
|
||||||
|
childTraceSpans: span.children,
|
||||||
|
})
|
||||||
|
entries.push(...nestedEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a block completed event output contains child trace spans
|
||||||
|
*/
|
||||||
|
export function hasChildTraceSpans(output: unknown): output is Record<string, unknown> & {
|
||||||
|
childTraceSpans: TraceSpan[]
|
||||||
|
} {
|
||||||
|
return (
|
||||||
|
output !== null &&
|
||||||
|
typeof output === 'object' &&
|
||||||
|
Array.isArray((output as Record<string, unknown>).childTraceSpans)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
|
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
|
||||||
export { useTerminalConsoleStore } from './console'
|
export { extractChildWorkflowEntries, hasChildTraceSpans, useTerminalConsoleStore } from './console'
|
||||||
export { useTerminalStore } from './store'
|
export { useTerminalStore } from './store'
|
||||||
export type { TerminalState } from './types'
|
export type { TerminalState } from './types'
|
||||||
|
|||||||
@@ -827,11 +827,10 @@ export function formatParameterLabel(paramId: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SubBlock IDs that are "structural" — they control tool routing or auth,
|
* SubBlock IDs that control tool routing, not user-facing parameters.
|
||||||
* not user-facing parameters. These are excluded from tool-input rendering
|
* Excluded from tool-input rendering unless they have an explicit paramVisibility set.
|
||||||
* unless they have an explicit paramVisibility set.
|
|
||||||
*/
|
*/
|
||||||
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation', 'authMethod', 'destinationType'])
|
const STRUCTURAL_SUBBLOCK_IDS = new Set(['operation'])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SubBlock types that represent auth/credential inputs handled separately
|
* SubBlock types that represent auth/credential inputs handled separately
|
||||||
@@ -955,12 +954,8 @@ export function getSubBlocksForToolInput(
|
|||||||
} else if (sb.id in toolParamVisibility) {
|
} else if (sb.id in toolParamVisibility) {
|
||||||
visibility = toolParamVisibility[sb.id]
|
visibility = toolParamVisibility[sb.id]
|
||||||
} else if (sb.canonicalParamId) {
|
} else if (sb.canonicalParamId) {
|
||||||
// SubBlock has a canonicalParamId that doesn't directly match a tool param.
|
|
||||||
// This means the block's params() function transforms it before sending to the tool
|
|
||||||
// (e.g. listFolderId → folderId). These are user-facing inputs, default to user-or-llm.
|
|
||||||
visibility = 'user-or-llm'
|
visibility = 'user-or-llm'
|
||||||
} else {
|
} else {
|
||||||
// SubBlock has no corresponding tool param — skip it
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
|
|||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
description: 'Message text to send (supports Slack mrkdwn formatting)',
|
description: 'Message text to send (supports Slack mrkdwn formatting)',
|
||||||
},
|
},
|
||||||
thread_ts: {
|
threadTs: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: false,
|
required: false,
|
||||||
visibility: 'user-or-llm',
|
visibility: 'user-or-llm',
|
||||||
@@ -84,7 +84,7 @@ export const slackMessageTool: ToolConfig<SlackMessageParams, SlackMessageRespon
|
|||||||
channel: isDM ? undefined : params.channel,
|
channel: isDM ? undefined : params.channel,
|
||||||
userId: isDM ? params.dmUserId : params.userId,
|
userId: isDM ? params.dmUserId : params.userId,
|
||||||
text: params.text,
|
text: params.text,
|
||||||
thread_ts: params.thread_ts || undefined,
|
thread_ts: params.threadTs || undefined,
|
||||||
files: params.files || null,
|
files: params.files || null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -516,7 +516,7 @@ export interface SlackMessageParams extends SlackBaseParams {
|
|||||||
dmUserId?: string
|
dmUserId?: string
|
||||||
userId?: string
|
userId?: string
|
||||||
text: string
|
text: string
|
||||||
thread_ts?: string
|
threadTs?: string
|
||||||
files?: UserFile[]
|
files?: UserFile[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user