diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 40600da75b..0c61ea0b8a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -101,6 +101,9 @@ const ACTION_VERBS = [ 'Generated', 'Rendering', 'Rendered', + 'Sleeping', + 'Slept', + 'Resumed', ] as const /** @@ -580,6 +583,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: (toolCall.state === (ClientToolCallState.executing as any) || toolCall.state === ('executing' as any)) + const showWake = + toolCall.name === 'sleep' && + (toolCall.state === (ClientToolCallState.executing as any) || + toolCall.state === ('executing' as any)) + const handleStateChange = (state: any) => { forceUpdate({}) onStateChange?.(state) @@ -1102,6 +1110,37 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: Move to Background + ) : showWake ? ( +
+ +
) : null} ) diff --git a/apps/sim/lib/copilot/registry.ts b/apps/sim/lib/copilot/registry.ts index 67a253cc56..db013fc546 100644 --- a/apps/sim/lib/copilot/registry.ts +++ b/apps/sim/lib/copilot/registry.ts @@ -33,6 +33,7 @@ export const ToolIds = z.enum([ 'knowledge_base', 'manage_custom_tool', 'manage_mcp_tool', + 'sleep', ]) export type ToolId = z.infer @@ -252,6 +253,14 @@ export const ToolArgSchemas = { .optional() .describe('Required for add and edit operations. The MCP server configuration.'), }), + + sleep: z.object({ + seconds: z + .number() + .min(0) + .max(180) + .describe('The number of seconds to sleep (0-180, max 3 minutes)'), + }), } as const export type ToolArgSchemaMap = typeof ToolArgSchemas @@ -318,6 +327,7 @@ export const ToolSSESchemas = { knowledge_base: toolCallSSEFor('knowledge_base', ToolArgSchemas.knowledge_base), manage_custom_tool: toolCallSSEFor('manage_custom_tool', ToolArgSchemas.manage_custom_tool), manage_mcp_tool: toolCallSSEFor('manage_mcp_tool', ToolArgSchemas.manage_mcp_tool), + sleep: toolCallSSEFor('sleep', ToolArgSchemas.sleep), } as const export type ToolSSESchemaMap = typeof ToolSSESchemas @@ -552,6 +562,11 @@ export const ToolResultSchemas = { serverName: z.string().optional(), message: z.string().optional(), }), + sleep: z.object({ + success: z.boolean(), + seconds: z.number(), + message: z.string().optional(), + }), } as const export type ToolResultSchemaMap = typeof ToolResultSchemas diff --git a/apps/sim/lib/copilot/tools/client/other/sleep.ts b/apps/sim/lib/copilot/tools/client/other/sleep.ts new file mode 100644 index 0000000000..18ad084efa --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/other/sleep.ts @@ -0,0 +1,144 @@ +import { Loader2, MinusCircle, Moon, XCircle } from 'lucide-react' +import { + BaseClientTool, + type BaseClientToolMetadata, + ClientToolCallState, +} from '@/lib/copilot/tools/client/base-tool' +import { createLogger } from '@/lib/logs/console/logger' + +/** Maximum sleep duration in seconds (3 minutes) */ +const MAX_SLEEP_SECONDS = 180 + +/** Track sleep start times for calculating elapsed time on wake */ +const sleepStartTimes: Record = {} + +interface SleepArgs { + seconds?: number +} + +/** + * Format seconds into a human-readable duration string + */ +function formatDuration(seconds: number): string { + if (seconds >= 60) { + return `${Math.round(seconds / 60)} minute${seconds >= 120 ? 's' : ''}` + } + return `${seconds} second${seconds !== 1 ? 's' : ''}` +} + +export class SleepClientTool extends BaseClientTool { + static readonly id = 'sleep' + + constructor(toolCallId: string) { + super(toolCallId, SleepClientTool.id, SleepClientTool.metadata) + } + + static readonly metadata: BaseClientToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to sleep', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Sleeping', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Sleeping', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Finished sleeping', icon: Moon }, + [ClientToolCallState.error]: { text: 'Sleep interrupted', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Sleep skipped', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Sleep aborted', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Resumed', icon: Moon }, + }, + // No interrupt - auto-execute immediately + getDynamicText: (params, state) => { + const seconds = params?.seconds + if (typeof seconds === 'number' && seconds > 0) { + const displayTime = formatDuration(seconds) + switch (state) { + case ClientToolCallState.success: + return `Slept for ${displayTime}` + case ClientToolCallState.executing: + case ClientToolCallState.pending: + return `Sleeping for ${displayTime}` + case ClientToolCallState.generating: + return `Preparing to sleep for ${displayTime}` + case ClientToolCallState.error: + return `Failed to sleep for ${displayTime}` + case ClientToolCallState.rejected: + return `Skipped sleeping for ${displayTime}` + case ClientToolCallState.aborted: + return `Aborted sleeping for ${displayTime}` + case ClientToolCallState.background: { + // Calculate elapsed time from when sleep started + const elapsedSeconds = params?._elapsedSeconds + if (typeof elapsedSeconds === 'number' && elapsedSeconds > 0) { + return `Resumed after ${formatDuration(Math.round(elapsedSeconds))}` + } + return 'Resumed early' + } + } + } + return undefined + }, + } + + /** + * Get elapsed seconds since sleep started + */ + getElapsedSeconds(): number { + const startTime = sleepStartTimes[this.toolCallId] + if (!startTime) return 0 + return (Date.now() - startTime) / 1000 + } + + async handleReject(): Promise { + await super.handleReject() + this.setState(ClientToolCallState.rejected) + } + + async handleAccept(args?: SleepArgs): Promise { + const logger = createLogger('SleepClientTool') + + // Use a timeout slightly longer than max sleep (3 minutes + buffer) + const timeoutMs = (MAX_SLEEP_SECONDS + 30) * 1000 + + await this.executeWithTimeout(async () => { + const params = args || {} + logger.debug('handleAccept() called', { + toolCallId: this.toolCallId, + state: this.getState(), + hasArgs: !!args, + seconds: params.seconds, + }) + + // Validate and clamp seconds + let seconds = typeof params.seconds === 'number' ? params.seconds : 0 + if (seconds < 0) seconds = 0 + if (seconds > MAX_SLEEP_SECONDS) seconds = MAX_SLEEP_SECONDS + + logger.debug('Starting sleep', { seconds }) + + // Track start time for elapsed calculation + sleepStartTimes[this.toolCallId] = Date.now() + + this.setState(ClientToolCallState.executing) + + try { + // Sleep for the specified duration + await new Promise((resolve) => setTimeout(resolve, seconds * 1000)) + + logger.debug('Sleep completed successfully') + this.setState(ClientToolCallState.success) + await this.markToolComplete(200, `Slept for ${seconds} seconds`) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + logger.error('Sleep failed', { error: message }) + this.setState(ClientToolCallState.error) + await this.markToolComplete(500, message) + } finally { + // Clean up start time tracking + delete sleepStartTimes[this.toolCallId] + } + }, timeoutMs) + } + + async execute(args?: SleepArgs): Promise { + // Auto-execute without confirmation - go straight to executing + await this.handleAccept(args) + } +} diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index ae2f86093e..826f5531ec 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -8,6 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' +import { isValidKey } from '@/lib/workflows/sanitization/key-validation' import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { getAllBlocks, getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' @@ -850,13 +851,18 @@ function applyOperationsToWorkflowState( * Reorder operations to ensure correct execution sequence: * 1. delete - Remove blocks first to free up IDs and clean state * 2. extract_from_subflow - Extract blocks from subflows before modifications - * 3. add - Create new blocks so they exist before being referenced + * 3. add - Create new blocks (sorted by connection dependencies) * 4. insert_into_subflow - Insert blocks into subflows (sorted by parent dependency) * 5. edit - Edit existing blocks last, so connections to newly added blocks work * - * This ordering is CRITICAL: edit operations may reference blocks being added - * in the same batch (e.g., connecting block A to newly added block B). - * Without proper ordering, the target block wouldn't exist yet. + * This ordering is CRITICAL: operations may reference blocks being added/inserted + * in the same batch. Without proper ordering, target blocks wouldn't exist yet. + * + * For add operations, we use a two-pass approach: + * - Pass 1: Create all blocks (without connections) + * - Pass 2: Add all connections (now all blocks exist) + * This ensures that if block A connects to block B, and both are being added, + * B will exist when we try to create the edge from A to B. */ const deletes = operations.filter((op) => op.operation_type === 'delete') const extracts = operations.filter((op) => op.operation_type === 'extract_from_subflow') @@ -868,6 +874,8 @@ function applyOperationsToWorkflowState( // This handles cases where a loop/parallel is being added along with its children const sortedInserts = topologicalSortInserts(inserts, adds) + // We'll process add operations in two passes (handled in the switch statement below) + // This is tracked via a separate flag to know which pass we're in const orderedOperations: EditWorkflowOperation[] = [ ...deletes, ...extracts, @@ -877,15 +885,46 @@ function applyOperationsToWorkflowState( ] logger.info('Operations after reordering:', { - order: orderedOperations.map( + totalOperations: orderedOperations.length, + deleteCount: deletes.length, + extractCount: extracts.length, + addCount: adds.length, + insertCount: sortedInserts.length, + editCount: edits.length, + operationOrder: orderedOperations.map( (op) => `${op.operation_type}:${op.block_id}${op.params?.subflowId ? `(parent:${op.params.subflowId})` : ''}` ), }) + // Two-pass processing for add operations: + // Pass 1: Create all blocks (without connections) + // Pass 2: Add all connections (all blocks now exist) + const addOperationsWithConnections: Array<{ + blockId: string + connections: Record + }> = [] + for (const operation of orderedOperations) { const { operation_type, block_id, params } = operation + // CRITICAL: Validate block_id is a valid string and not "undefined" + // This prevents undefined keys from being set in the workflow state + if (!isValidKey(block_id)) { + logSkippedItem(skippedItems, { + type: 'missing_required_params', + operationType: operation_type, + blockId: String(block_id || 'invalid'), + reason: `Invalid block_id "${block_id}" (type: ${typeof block_id}) - operation skipped. Block IDs must be valid non-empty strings.`, + }) + logger.error('Invalid block_id detected in operation', { + operation_type, + block_id, + block_id_type: typeof block_id, + }) + continue + } + logger.debug(`Executing operation: ${operation_type} for block ${block_id}`, { params: params ? Object.keys(params) : [], currentBlockCount: Object.keys(modifiedState.blocks).length, @@ -1128,6 +1167,22 @@ function applyOperationsToWorkflowState( // Add new nested blocks Object.entries(params.nestedNodes).forEach(([childId, childBlock]: [string, any]) => { + // Validate childId is a valid string + if (!isValidKey(childId)) { + logSkippedItem(skippedItems, { + type: 'missing_required_params', + operationType: 'add_nested_node', + blockId: String(childId || 'invalid'), + reason: `Invalid childId "${childId}" in nestedNodes - child block skipped`, + }) + logger.error('Invalid childId detected in nestedNodes', { + parentBlockId: block_id, + childId, + childId_type: typeof childId, + }) + return + } + const childBlockState = createBlockFromParams( childId, childBlock, @@ -1360,6 +1415,22 @@ function applyOperationsToWorkflowState( // Handle nested nodes (for loops/parallels created from scratch) if (params.nestedNodes) { Object.entries(params.nestedNodes).forEach(([childId, childBlock]: [string, any]) => { + // Validate childId is a valid string + if (!isValidKey(childId)) { + logSkippedItem(skippedItems, { + type: 'missing_required_params', + operationType: 'add_nested_node', + blockId: String(childId || 'invalid'), + reason: `Invalid childId "${childId}" in nestedNodes - child block skipped`, + }) + logger.error('Invalid childId detected in nestedNodes', { + parentBlockId: block_id, + childId, + childId_type: typeof childId, + }) + return + } + const childBlockState = createBlockFromParams( childId, childBlock, @@ -1368,21 +1439,22 @@ function applyOperationsToWorkflowState( ) modifiedState.blocks[childId] = childBlockState + // Defer connection processing to ensure all blocks exist first if (childBlock.connections) { - addConnectionsAsEdges( - modifiedState, - childId, - childBlock.connections, - logger, - skippedItems - ) + addOperationsWithConnections.push({ + blockId: childId, + connections: childBlock.connections, + }) } }) } - // Add connections as edges + // Defer connection processing to ensure all blocks exist first (pass 2) if (params.connections) { - addConnectionsAsEdges(modifiedState, block_id, params.connections, logger, skippedItems) + addOperationsWithConnections.push({ + blockId: block_id, + connections: params.connections, + }) } break } @@ -1506,13 +1578,18 @@ function applyOperationsToWorkflowState( modifiedState.blocks[block_id] = newBlock } - // Add/update connections as edges + // Defer connection processing to ensure all blocks exist first + // This is particularly important when multiple blocks are being inserted + // and they have connections to each other if (params.connections) { - // Remove existing edges from this block + // Remove existing edges from this block first modifiedState.edges = modifiedState.edges.filter((edge: any) => edge.source !== block_id) - // Add new connections - addConnectionsAsEdges(modifiedState, block_id, params.connections, logger, skippedItems) + // Add to deferred connections list + addOperationsWithConnections.push({ + blockId: block_id, + connections: params.connections, + }) } break } @@ -1562,6 +1639,34 @@ function applyOperationsToWorkflowState( } } + // Pass 2: Add all deferred connections from add/insert operations + // Now all blocks exist (from add, insert, and edit operations), so connections can be safely created + // This ensures that if block A connects to block B, and both are being added/inserted, + // B will exist when we create the edge from A to B + if (addOperationsWithConnections.length > 0) { + logger.info('Processing deferred connections from add/insert operations', { + deferredConnectionCount: addOperationsWithConnections.length, + totalBlocks: Object.keys(modifiedState.blocks).length, + }) + + for (const { blockId, connections } of addOperationsWithConnections) { + // Verify the source block still exists (it might have been deleted by a later operation) + if (!modifiedState.blocks[blockId]) { + logger.warn('Source block no longer exists for deferred connection', { + blockId, + availableBlocks: Object.keys(modifiedState.blocks), + }) + continue + } + + addConnectionsAsEdges(modifiedState, blockId, connections, logger, skippedItems) + } + + logger.info('Finished processing deferred connections', { + totalEdges: modifiedState.edges.length, + }) + } + // Regenerate loops and parallels after modifications modifiedState.loops = generateLoopBlocks(modifiedState.blocks) modifiedState.parallels = generateParallelBlocks(modifiedState.blocks) diff --git a/apps/sim/lib/workflows/diff/diff-engine.ts b/apps/sim/lib/workflows/diff/diff-engine.ts index 292edbb628..e41652acdb 100644 --- a/apps/sim/lib/workflows/diff/diff-engine.ts +++ b/apps/sim/lib/workflows/diff/diff-engine.ts @@ -2,6 +2,7 @@ import type { Edge } from 'reactflow' import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' import type { BlockWithDiff } from '@/lib/workflows/diff/types' +import { isValidKey } from '@/lib/workflows/sanitization/key-validation' import { mergeSubblockState } from '@/stores/workflows/utils' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' @@ -537,6 +538,17 @@ export class WorkflowDiffEngine { // First pass: build ID mappings for (const [proposedId, proposedBlock] of Object.entries(proposedState.blocks)) { + // CRITICAL: Skip invalid block IDs to prevent "undefined" keys in workflow state + if (!isValidKey(proposedId)) { + logger.error('Invalid proposedId detected in proposed state', { + proposedId, + proposedId_type: typeof proposedId, + blockType: proposedBlock?.type, + blockName: proposedBlock?.name, + }) + continue + } + const key = `${proposedBlock.type}:${proposedBlock.name}` // Check if this block exists in current state by type:name @@ -552,7 +564,31 @@ export class WorkflowDiffEngine { // Second pass: build final blocks with mapped IDs for (const [proposedId, proposedBlock] of Object.entries(proposedState.blocks)) { + // CRITICAL: Skip invalid block IDs to prevent "undefined" keys in workflow state + if (!isValidKey(proposedId)) { + logger.error('Invalid proposedId detected in proposed state (second pass)', { + proposedId, + proposedId_type: typeof proposedId, + blockType: proposedBlock?.type, + blockName: proposedBlock?.name, + }) + continue + } + const finalId = idMap[proposedId] + + // CRITICAL: Validate finalId before using as key + if (!isValidKey(finalId)) { + logger.error('Invalid finalId generated from idMap', { + proposedId, + finalId, + finalId_type: typeof finalId, + blockType: proposedBlock?.type, + blockName: proposedBlock?.name, + }) + continue + } + const key = `${proposedBlock.type}:${proposedBlock.name}` const existingBlock = existingBlockMap[key]?.block @@ -617,6 +653,8 @@ export class WorkflowDiffEngine { const { generateLoopBlocks, generateParallelBlocks } = await import( '@/stores/workflows/workflow/utils' ) + + // Build the proposed state const finalProposedState: WorkflowState = { blocks: finalBlocks, edges: finalEdges, @@ -625,6 +663,9 @@ export class WorkflowDiffEngine { lastSaved: Date.now(), } + // Use the proposed state directly - validation happens at the source + const fullyCleanedState = finalProposedState + // Transfer block heights from baseline workflow for better measurements in diff view // If editing on top of diff, this transfers from the diff (which already has good heights) // Otherwise transfers from original workflow @@ -694,7 +735,7 @@ export class WorkflowDiffEngine { '@/lib/workflows/autolayout/constants' ) - const layoutedBlocks = applyTargetedLayout(finalBlocks, finalProposedState.edges, { + const layoutedBlocks = applyTargetedLayout(finalBlocks, fullyCleanedState.edges, { changedBlockIds: impactedBlockArray, horizontalSpacing: DEFAULT_HORIZONTAL_SPACING, verticalSpacing: DEFAULT_VERTICAL_SPACING, @@ -742,7 +783,7 @@ export class WorkflowDiffEngine { const layoutResult = applyNativeAutoLayout( finalBlocks, - finalProposedState.edges, + fullyCleanedState.edges, DEFAULT_LAYOUT_OPTIONS ) @@ -824,7 +865,7 @@ export class WorkflowDiffEngine { }) // Create edge identifiers for proposed state - finalEdges.forEach((edge) => { + fullyCleanedState.edges.forEach((edge) => { const edgeId = `${edge.source}-${edge.sourceHandle || 'source'}-${edge.target}-${edge.targetHandle || 'target'}` proposedEdgeSet.add(edgeId) }) @@ -863,21 +904,21 @@ export class WorkflowDiffEngine { } } - // Apply diff markers to blocks + // Apply diff markers to blocks in the fully cleaned state if (computed) { for (const id of computed.new_blocks || []) { - if (finalBlocks[id]) { - finalBlocks[id].is_diff = 'new' + if (fullyCleanedState.blocks[id]) { + ;(fullyCleanedState.blocks[id] as any).is_diff = 'new' } } for (const id of computed.edited_blocks || []) { - if (finalBlocks[id]) { - finalBlocks[id].is_diff = 'edited' + if (fullyCleanedState.blocks[id]) { + ;(fullyCleanedState.blocks[id] as any).is_diff = 'edited' // Also mark specific subblocks that changed if (computed.field_diffs?.[id]) { const fieldDiff = computed.field_diffs[id] - const block = finalBlocks[id] + const block = fullyCleanedState.blocks[id] // Apply diff markers to changed subblocks for (const changedField of fieldDiff.changed_fields) { @@ -889,12 +930,12 @@ export class WorkflowDiffEngine { } } } - // Note: We don't remove deleted blocks from finalBlocks, just mark them + // Note: We don't remove deleted blocks from fullyCleanedState, just mark them } - // Store the diff + // Store the diff with the fully sanitized state this.currentDiff = { - proposedState: finalProposedState, + proposedState: fullyCleanedState, diffAnalysis: computed, metadata: { source: 'workflow_state', @@ -903,10 +944,10 @@ export class WorkflowDiffEngine { } logger.info('Successfully created diff from workflow state', { - blockCount: Object.keys(finalProposedState.blocks).length, - edgeCount: finalProposedState.edges.length, - hasLoops: Object.keys(finalProposedState.loops || {}).length > 0, - hasParallels: Object.keys(finalProposedState.parallels || {}).length > 0, + blockCount: Object.keys(fullyCleanedState.blocks).length, + edgeCount: fullyCleanedState.edges.length, + hasLoops: Object.keys(fullyCleanedState.loops || {}).length > 0, + hasParallels: Object.keys(fullyCleanedState.parallels || {}).length > 0, newBlocks: computed?.new_blocks?.length || 0, editedBlocks: computed?.edited_blocks?.length || 0, deletedBlocks: computed?.deleted_blocks?.length || 0, @@ -1096,6 +1137,17 @@ export function stripWorkflowDiffMarkers(state: WorkflowState): WorkflowState { const cleanBlocks: Record = {} for (const [blockId, block] of Object.entries(state.blocks || {})) { + // Validate block ID at the source - skip invalid IDs + if (!isValidKey(blockId)) { + logger.error('Invalid blockId detected in stripWorkflowDiffMarkers', { + blockId, + blockId_type: typeof blockId, + blockType: block?.type, + blockName: block?.name, + }) + continue + } + const cleanBlock: BlockState = structuredClone(block) const blockWithDiff = cleanBlock as BlockState & BlockWithDiff blockWithDiff.is_diff = undefined diff --git a/apps/sim/lib/workflows/sanitization/key-validation.ts b/apps/sim/lib/workflows/sanitization/key-validation.ts new file mode 100644 index 0000000000..ff66166648 --- /dev/null +++ b/apps/sim/lib/workflows/sanitization/key-validation.ts @@ -0,0 +1,9 @@ +/** + * Checks if a key is valid (not undefined, null, empty, or literal "undefined"/"null") + * Use this to validate BEFORE setting a dynamic key on any object. + */ +export function isValidKey(key: unknown): key is string { + return ( + !!key && typeof key === 'string' && key !== 'undefined' && key !== 'null' && key.trim() !== '' + ) +} diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index 450844ea49..a350fb2d88 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -32,6 +32,7 @@ import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/ import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors' import { SearchOnlineClientTool } from '@/lib/copilot/tools/client/other/search-online' import { SearchPatternsClientTool } from '@/lib/copilot/tools/client/other/search-patterns' +import { SleepClientTool } from '@/lib/copilot/tools/client/other/sleep' import { createExecutionContext, getTool } from '@/lib/copilot/tools/client/registry' import { GetCredentialsClientTool } from '@/lib/copilot/tools/client/user/get-credentials' import { SetEnvironmentVariablesClientTool } from '@/lib/copilot/tools/client/user/set-environment-variables' @@ -104,6 +105,7 @@ const CLIENT_TOOL_INSTANTIATORS: Record any> = { navigate_ui: (id) => new NavigateUIClientTool(id), manage_custom_tool: (id) => new ManageCustomToolClientTool(id), manage_mcp_tool: (id) => new ManageMcpToolClientTool(id), + sleep: (id) => new SleepClientTool(id), } // Read-only static metadata for class-based tools (no instances) @@ -141,6 +143,7 @@ export const CLASS_TOOL_METADATA: Record()( set({ toolCallsById: map }) } catch {} }, + + updateToolCallParams: (toolCallId: string, params: Record) => { + try { + if (!toolCallId) return + const map = { ...get().toolCallsById } + const current = map[toolCallId] + if (!current) return + const updatedParams = { ...current.params, ...params } + map[toolCallId] = { + ...current, + params: updatedParams, + display: resolveToolDisplay(current.name, current.state, toolCallId, updatedParams), + } + set({ toolCallsById: map }) + } catch {} + }, updatePreviewToolCallState: ( toolCallState: 'accepted' | 'rejected' | 'error', toolCallId?: string diff --git a/apps/sim/stores/panel/copilot/types.ts b/apps/sim/stores/panel/copilot/types.ts index 89f27efa54..2380aca2b1 100644 --- a/apps/sim/stores/panel/copilot/types.ts +++ b/apps/sim/stores/panel/copilot/types.ts @@ -178,6 +178,7 @@ export interface CopilotActions { toolCallId?: string ) => void setToolCallState: (toolCall: any, newState: ClientToolCallState, options?: any) => void + updateToolCallParams: (toolCallId: string, params: Record) => void sendDocsMessage: (query: string, options?: { stream?: boolean; topK?: number }) => Promise saveChatMessages: (chatId: string) => Promise