diff --git a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts index 394c11f6d..d40513da7 100644 --- a/apps/sim/lib/copilot/client-sse/subagent-handlers.ts +++ b/apps/sim/lib/copilot/client-sse/subagent-handlers.ts @@ -250,10 +250,10 @@ export const subAgentSSEHandlers: Record = { sendAutoAcceptConfirmation(id) } - // Client-executable run tools: execute on the client for real-time feedback. - // The server defers execution in interactive mode; we execute here and - // report back via mark-complete. - if (CLIENT_EXECUTABLE_RUN_TOOLS.has(name)) { + // Client-executable run tools: if auto-allowed, execute immediately for + // real-time feedback. For non-auto-allowed, the user must click "Allow" + // first — handleRun in tool-call.tsx triggers executeRunToolOnClient. + if (CLIENT_EXECUTABLE_RUN_TOOLS.has(name) && isAutoAllowed) { executeRunToolOnClient(id, name, args || {}) } }, diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts index 6e0b28cfc..809eb6595 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/handlers.ts @@ -490,6 +490,23 @@ export const subAgentHandlers: Record = { options.timeout || STREAM_TIMEOUT_MS, options.abortSignal ) + if (completion?.status === 'rejected') { + toolCall.status = 'rejected' + toolCall.endTime = Date.now() + markToolComplete( + toolCall.id, + toolCall.name, + 400, + completion.message || 'Tool execution rejected' + ).catch((err) => { + logger.error('markToolComplete fire-and-forget failed (subagent run tool rejected)', { + toolCallId: toolCall.id, + error: err instanceof Error ? err.message : String(err), + }) + }) + markToolResultSeen(toolCallId) + return + } const success = completion?.status === 'success' toolCall.status = success ? 'success' : 'error' toolCall.endTime = Date.now() diff --git a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts index 8c48405ad..739b11c46 100644 --- a/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse-handlers/tool-execution.ts @@ -146,10 +146,12 @@ export async function waitForToolDecision( } /** - * Wait for a tool completion signal (success/error) from the client. - * Unlike waitForToolDecision which returns on any status, this ignores - * intermediate statuses like 'accepted'/'rejected'/'background' and only - * returns when the client reports final completion via success/error. + * Wait for a tool completion signal (success/error/rejected) from the client. + * Unlike waitForToolDecision which returns on any status, this ignores the + * initial 'accepted' status and only returns on terminal statuses: + * - success: client finished executing successfully + * - error: client execution failed + * - rejected: user clicked Skip (subagent run tools where user hasn't auto-allowed) * * Used for client-executable run tools: the client executes the workflow * and posts success/error to /api/copilot/confirm when done. The server @@ -166,8 +168,12 @@ export async function waitForToolCompletion( while (Date.now() - start < timeoutMs) { if (abortSignal?.aborted) return null const decision = await getToolConfirmation(toolCallId) - // Only return on completion statuses, not accept/reject decisions - if (decision?.status === 'success' || decision?.status === 'error') { + // Return on completion/terminal statuses, not intermediate 'accepted' + if ( + decision?.status === 'success' || + decision?.status === 'error' || + decision?.status === 'rejected' + ) { return decision } await new Promise((resolve) => setTimeout(resolve, interval)) diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index 9cfa68075..f3d908954 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -50,6 +50,18 @@ import { import { getLatestBlock } from '@/blocks/registry' import { getCustomTool } from '@/hooks/queries/custom-tools' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' + +/** Resolve a block ID to its human-readable name from the workflow store. */ +function resolveBlockName(blockId: string | undefined): string | undefined { + if (!blockId) return undefined + try { + const blocks = useWorkflowStore.getState().blocks + return blocks[blockId]?.name || undefined + } catch { + return undefined + } +} export enum ClientToolCallState { generating = 'generating', @@ -1742,12 +1754,12 @@ const META_generate_api_key: ToolMetadata = { const META_run_block: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Preparing to run block', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Run this block?', icon: Play }, + [ClientToolCallState.pending]: { text: 'Run block?', icon: Play }, [ClientToolCallState.executing]: { text: 'Running block', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Executed block', icon: Play }, + [ClientToolCallState.success]: { text: 'Ran block', icon: Play }, [ClientToolCallState.error]: { text: 'Failed to run block', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped block execution', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted block execution', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped running block', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted running block', icon: MinusCircle }, [ClientToolCallState.background]: { text: 'Running block in background', icon: Play }, }, interrupt: { @@ -1775,23 +1787,24 @@ const META_run_block: ToolMetadata = { getDynamicText: (params, state) => { const blockId = params?.blockId || params?.block_id if (blockId && typeof blockId === 'string') { + const name = resolveBlockName(blockId) || blockId switch (state) { case ClientToolCallState.success: - return `Executed block ${blockId}` + return `Ran ${name}` case ClientToolCallState.executing: - return `Running block ${blockId}` + return `Running ${name}` case ClientToolCallState.generating: - return `Preparing to run block ${blockId}` + return `Preparing to run ${name}` case ClientToolCallState.pending: - return `Run block ${blockId}?` + return `Run ${name}?` case ClientToolCallState.error: - return `Failed to run block ${blockId}` + return `Failed to run ${name}` case ClientToolCallState.rejected: - return `Skipped running block ${blockId}` + return `Skipped running ${name}` case ClientToolCallState.aborted: - return `Aborted running block ${blockId}` + return `Aborted running ${name}` case ClientToolCallState.background: - return `Running block ${blockId} in background` + return `Running ${name} in background` } } return undefined @@ -1801,12 +1814,12 @@ const META_run_block: ToolMetadata = { const META_run_from_block: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Preparing to run from block', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Run from this block?', icon: Play }, + [ClientToolCallState.pending]: { text: 'Run from block?', icon: Play }, [ClientToolCallState.executing]: { text: 'Running from block', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Executed from block', icon: Play }, + [ClientToolCallState.success]: { text: 'Ran from block', icon: Play }, [ClientToolCallState.error]: { text: 'Failed to run from block', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped run from block', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted run from block', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped running from block', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted running from block', icon: MinusCircle }, [ClientToolCallState.background]: { text: 'Running from block in background', icon: Play }, }, interrupt: { @@ -1834,23 +1847,24 @@ const META_run_from_block: ToolMetadata = { getDynamicText: (params, state) => { const blockId = params?.startBlockId || params?.start_block_id if (blockId && typeof blockId === 'string') { + const name = resolveBlockName(blockId) || blockId switch (state) { case ClientToolCallState.success: - return `Executed from block ${blockId}` + return `Ran from ${name}` case ClientToolCallState.executing: - return `Running from block ${blockId}` + return `Running from ${name}` case ClientToolCallState.generating: - return `Preparing to run from block ${blockId}` + return `Preparing to run from ${name}` case ClientToolCallState.pending: - return `Run from block ${blockId}?` + return `Run from ${name}?` case ClientToolCallState.error: - return `Failed to run from block ${blockId}` + return `Failed to run from ${name}` case ClientToolCallState.rejected: - return `Skipped running from block ${blockId}` + return `Skipped running from ${name}` case ClientToolCallState.aborted: - return `Aborted running from block ${blockId}` + return `Aborted running from ${name}` case ClientToolCallState.background: - return `Running from block ${blockId} in background` + return `Running from ${name} in background` } } return undefined @@ -1860,12 +1874,12 @@ const META_run_from_block: ToolMetadata = { const META_run_workflow_until_block: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Preparing to run until block', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Run until this block?', icon: Play }, + [ClientToolCallState.pending]: { text: 'Run until block?', icon: Play }, [ClientToolCallState.executing]: { text: 'Running until block', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Executed until block', icon: Play }, + [ClientToolCallState.success]: { text: 'Ran until block', icon: Play }, [ClientToolCallState.error]: { text: 'Failed to run until block', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped run until block', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted run until block', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped running until block', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted running until block', icon: MinusCircle }, [ClientToolCallState.background]: { text: 'Running until block in background', icon: Play }, }, interrupt: { @@ -1893,23 +1907,24 @@ const META_run_workflow_until_block: ToolMetadata = { getDynamicText: (params, state) => { const blockId = params?.stopAfterBlockId || params?.stop_after_block_id if (blockId && typeof blockId === 'string') { + const name = resolveBlockName(blockId) || blockId switch (state) { case ClientToolCallState.success: - return `Executed until block ${blockId}` + return `Ran until ${name}` case ClientToolCallState.executing: - return `Running until block ${blockId}` + return `Running until ${name}` case ClientToolCallState.generating: - return `Preparing to run until block ${blockId}` + return `Preparing to run until ${name}` case ClientToolCallState.pending: - return `Run until block ${blockId}?` + return `Run until ${name}?` case ClientToolCallState.error: - return `Failed to run until block ${blockId}` + return `Failed to run until ${name}` case ClientToolCallState.rejected: - return `Skipped running until block ${blockId}` + return `Skipped running until ${name}` case ClientToolCallState.aborted: - return `Aborted running until block ${blockId}` + return `Aborted running until ${name}` case ClientToolCallState.background: - return `Running until block ${blockId} in background` + return `Running until ${name} in background` } } return undefined