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 8d0e59eff..2d644c91e 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 @@ -6,16 +6,10 @@ import clsx from 'clsx' import { ChevronUp, LayoutList } from 'lucide-react' import Editor from 'react-simple-code-editor' import { Button, Code, getCodeEditorProps, highlight, languages } from '@/components/emcn' -import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' -import { getClientTool } from '@/lib/copilot/tools/client/manager' -import { getRegisteredTools } from '@/lib/copilot/tools/client/registry' -import '@/lib/copilot/tools/client/init-tool-configs' import { - getSubagentLabels as getSubagentLabelsFromConfig, - getToolUIConfig, - hasInterrupt as hasInterruptFromConfig, - isSpecialTool as isSpecialToolFromConfig, -} from '@/lib/copilot/tools/client/ui-config' + ClientToolCallState, + TOOL_DISPLAY_REGISTRY, +} from '@/lib/copilot/tools/client/tool-display-registry' import { formatDuration } from '@/lib/core/utils/formatting' import { CopilotMarkdownRenderer } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer' import { SmoothStreamingText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/smooth-streaming' @@ -26,7 +20,6 @@ import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/co import { getBlock } from '@/blocks/registry' import type { CopilotToolCall } from '@/stores/panel' import { useCopilotStore } from '@/stores/panel' -import { CLASS_TOOL_METADATA } from '@/stores/panel/copilot/store' import type { SubAgentContentBlock } from '@/stores/panel/copilot/types' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -711,8 +704,8 @@ const ShimmerOverlayText = memo(function ShimmerOverlayText({ * @returns The completion label from UI config, defaults to 'Thought' */ function getSubagentCompletionLabel(toolName: string): string { - const labels = getSubagentLabelsFromConfig(toolName, false) - return labels?.completed ?? 'Thought' + const labels = TOOL_DISPLAY_REGISTRY[toolName]?.uiConfig?.subagentLabels + return labels?.completed || 'Thought' } /** @@ -944,7 +937,7 @@ const SubagentContentRenderer = memo(function SubagentContentRenderer({ * Determines if a tool call should display with special gradient styling. */ function isSpecialToolCall(toolCall: CopilotToolCall): boolean { - return isSpecialToolFromConfig(toolCall.name) + return TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.isSpecial === true } /** @@ -1224,28 +1217,11 @@ const WorkflowEditSummary = memo(function WorkflowEditSummary({ /** Checks if a tool is server-side executed (not a client tool) */ function isIntegrationTool(toolName: string): boolean { - return !CLASS_TOOL_METADATA[toolName] + return !TOOL_DISPLAY_REGISTRY[toolName] } function shouldShowRunSkipButtons(toolCall: CopilotToolCall): boolean { - if (hasInterruptFromConfig(toolCall.name) && toolCall.state === 'pending') { - return true - } - - const instance = getClientTool(toolCall.id) - let hasInterrupt = !!instance?.getInterruptDisplays?.() - if (!hasInterrupt) { - try { - const def = getRegisteredTools()[toolCall.name] - if (def) { - hasInterrupt = - typeof def.hasInterrupt === 'function' - ? !!def.hasInterrupt(toolCall.params || {}) - : !!def.hasInterrupt - } - } catch {} - } - + const hasInterrupt = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.interrupt === true if (hasInterrupt && toolCall.state === 'pending') { return true } @@ -1299,11 +1275,9 @@ async function handleSkip(toolCall: CopilotToolCall, setToolCallState: any, onSt function getDisplayName(toolCall: CopilotToolCall): string { const fromStore = (toolCall as any).display?.text if (fromStore) return fromStore - try { - const def = getRegisteredTools()[toolCall.name] as any - const byState = def?.metadata?.displayNames?.[toolCall.state] - if (byState?.text) return byState.text - } catch {} + const registryEntry = TOOL_DISPLAY_REGISTRY[toolCall.name] + const byState = registryEntry?.displayNames?.[toolCall.state as ClientToolCallState] + if (byState?.text) return byState.text const stateVerb = getStateVerb(toolCall.state) const formattedName = formatToolName(toolCall.name) @@ -1481,23 +1455,7 @@ export function ToolCall({ return null // Special rendering for subagent tools - show as thinking text with tool calls at top level - const SUBAGENT_TOOLS = [ - 'plan', - 'edit', - 'debug', - 'test', - 'deploy', - 'evaluate', - 'auth', - 'research', - 'knowledge', - 'custom_tool', - 'tour', - 'info', - 'workflow', - 'superagent', - ] - const isSubagentTool = SUBAGENT_TOOLS.includes(toolCall.name) + const isSubagentTool = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig?.subagent === true // For ALL subagent tools, don't show anything until we have blocks with content if (isSubagentTool) { @@ -1537,17 +1495,18 @@ export function ToolCall({ stateStr === 'aborted' // Allow rendering if: - // 1. Tool is in CLASS_TOOL_METADATA (client tools), OR + // 1. Tool is in TOOL_DISPLAY_REGISTRY (client tools), OR // 2. We're in build mode (integration tools are executed server-side), OR // 3. Tool call is already completed (historical - should always render) - const isClientTool = !!CLASS_TOOL_METADATA[toolCall.name] + const isClientTool = !!TOOL_DISPLAY_REGISTRY[toolCall.name] const isIntegrationToolInBuildMode = mode === 'build' && !isClientTool if (!isClientTool && !isIntegrationToolInBuildMode && !isCompletedToolCall) { return null } + const toolUIConfig = TOOL_DISPLAY_REGISTRY[toolCall.name]?.uiConfig // Check if tool has params table config (meaning it's expandable) - const hasParamsTable = !!getToolUIConfig(toolCall.name)?.paramsTable + const hasParamsTable = !!toolUIConfig?.paramsTable const isRunWorkflow = toolCall.name === 'run_workflow' const isExpandableTool = hasParamsTable || @@ -1557,7 +1516,6 @@ export function ToolCall({ const showButtons = isCurrentMessage && shouldShowRunSkipButtons(toolCall) // Check UI config for secondary action - only show for current message tool calls - const toolUIConfig = getToolUIConfig(toolCall.name) const secondaryAction = toolUIConfig?.secondaryAction const showSecondaryAction = secondaryAction?.showInStates.includes( toolCall.state as ClientToolCallState diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index 82d05a587..11e3942e7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -18,7 +18,7 @@ import 'reactflow/dist/style.css' import { createLogger } from '@sim/logger' import { useShallow } from 'zustand/react/shallow' import { useSession } from '@/lib/auth/auth-client' -import type { OAuthConnectEventDetail } from '@/lib/copilot/tools/client/other/oauth-request-access' +import type { OAuthConnectEventDetail } from '@/lib/copilot/tools/client/base-tool' import type { OAuthProvider } from '@/lib/oauth' import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/block-dimensions' import { TriggerUtils } from '@/lib/workflows/triggers/triggers' diff --git a/apps/sim/lib/copilot/tools/client/base-subagent-tool.ts b/apps/sim/lib/copilot/tools/client/base-subagent-tool.ts deleted file mode 100644 index 7a843dd88..000000000 --- a/apps/sim/lib/copilot/tools/client/base-subagent-tool.ts +++ /dev/null @@ -1,120 +0,0 @@ -/** - * Base class for subagent tools. - * - * Subagent tools spawn a server-side subagent that does the actual work. - * The tool auto-executes and the subagent's output is streamed back - * as nested content under the tool call. - * - * Examples: edit, plan, debug, evaluate, research, etc. - */ -import type { LucideIcon } from 'lucide-react' -import { BaseClientTool, type BaseClientToolMetadata, ClientToolCallState } from './base-tool' -import type { SubagentConfig, ToolUIConfig } from './ui-config' -import { registerToolUIConfig } from './ui-config' - -/** - * Configuration for creating a subagent tool - */ -export interface SubagentToolConfig { - /** Unique tool ID */ - id: string - /** Display names per state */ - displayNames: { - streaming: { text: string; icon: LucideIcon } - success: { text: string; icon: LucideIcon } - error: { text: string; icon: LucideIcon } - } - /** Subagent UI configuration */ - subagent: SubagentConfig - /** - * Optional: Whether this is a "special" tool (gets gradient styling). - * Default: false - */ - isSpecial?: boolean -} - -/** - * Create metadata for a subagent tool from config - */ -function createSubagentMetadata(config: SubagentToolConfig): BaseClientToolMetadata { - const { displayNames, subagent, isSpecial } = config - const { streaming, success, error } = displayNames - - const uiConfig: ToolUIConfig = { - isSpecial: isSpecial ?? false, - subagent, - } - - return { - displayNames: { - [ClientToolCallState.generating]: streaming, - [ClientToolCallState.pending]: streaming, - [ClientToolCallState.executing]: streaming, - [ClientToolCallState.success]: success, - [ClientToolCallState.error]: error, - [ClientToolCallState.rejected]: { - text: `${config.id.charAt(0).toUpperCase() + config.id.slice(1)} skipped`, - icon: error.icon, - }, - [ClientToolCallState.aborted]: { - text: `${config.id.charAt(0).toUpperCase() + config.id.slice(1)} aborted`, - icon: error.icon, - }, - }, - uiConfig, - } -} - -/** - * Base class for subagent tools. - * Extends BaseClientTool with subagent-specific behavior. - */ -export abstract class BaseSubagentTool extends BaseClientTool { - /** - * Subagent configuration. - * Override in subclasses to customize behavior. - */ - static readonly subagentConfig: SubagentToolConfig - - constructor(toolCallId: string, config: SubagentToolConfig) { - super(toolCallId, config.id, createSubagentMetadata(config)) - // Register UI config for this tool - registerToolUIConfig(config.id, this.metadata.uiConfig!) - } - - /** - * Execute the subagent tool. - * Immediately transitions to executing state - the actual work - * is done server-side by the subagent. - */ - async execute(_args?: Record): Promise { - this.setState(ClientToolCallState.executing) - // The tool result will come from the server via tool_result event - // when the subagent completes its work - } -} - -/** - * Factory function to create a subagent tool class. - * Use this for simple subagent tools that don't need custom behavior. - */ -export function createSubagentToolClass(config: SubagentToolConfig) { - // Register UI config at class creation time - const uiConfig: ToolUIConfig = { - isSpecial: config.isSpecial ?? false, - subagent: config.subagent, - } - registerToolUIConfig(config.id, uiConfig) - - return class extends BaseClientTool { - static readonly id = config.id - - constructor(toolCallId: string) { - super(toolCallId, config.id, createSubagentMetadata(config)) - } - - async execute(_args?: Record): Promise { - this.setState(ClientToolCallState.executing) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/base-tool.ts b/apps/sim/lib/copilot/tools/client/base-tool.ts index d3640bea0..73a562aa1 100644 --- a/apps/sim/lib/copilot/tools/client/base-tool.ts +++ b/apps/sim/lib/copilot/tools/client/base-tool.ts @@ -1,15 +1,5 @@ -// Lazy require in setState to avoid circular init issues -import { createLogger } from '@sim/logger' import type { LucideIcon } from 'lucide-react' -import type { ToolUIConfig } from './ui-config' -const baseToolLogger = createLogger('BaseClientTool') - -const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000 - -export const WORKFLOW_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000 - -// Client tool call states used by the new runtime export enum ClientToolCallState { generating = 'generating', pending = 'pending', @@ -22,198 +12,29 @@ export enum ClientToolCallState { background = 'background', } -// Display configuration for a given state export interface ClientToolDisplay { text: string icon: LucideIcon } -/** - * Function to generate dynamic display text based on tool parameters and state - * @param params - The tool call parameters - * @param state - The current tool call state - * @returns The dynamic text to display, or undefined to use the default text - */ +export interface BaseClientToolMetadata { + displayNames: Partial> + uiConfig?: Record + getDynamicText?: (params: Record, state: ClientToolCallState) => string | undefined +} + export type DynamicTextFormatter = ( - params: Record, + params: Record, state: ClientToolCallState ) => string | undefined -export interface BaseClientToolMetadata { - displayNames: Partial> - interrupt?: { - accept: ClientToolDisplay - reject: ClientToolDisplay - } - /** - * Optional function to generate dynamic display text based on parameters - * If provided, this will override the default text in displayNames - */ - getDynamicText?: DynamicTextFormatter - /** - * UI configuration for how this tool renders in the tool-call component. - * This replaces hardcoded logic in tool-call.tsx with declarative config. - */ - uiConfig?: ToolUIConfig -} - -export class BaseClientTool { - readonly toolCallId: string - readonly name: string - protected state: ClientToolCallState - protected metadata: BaseClientToolMetadata - protected isMarkedComplete = false - protected timeoutMs: number = DEFAULT_TOOL_TIMEOUT_MS - - constructor(toolCallId: string, name: string, metadata: BaseClientToolMetadata) { - this.toolCallId = toolCallId - this.name = name - this.metadata = metadata - this.state = ClientToolCallState.generating - } - - /** - * Set a custom timeout for this tool (in milliseconds) - */ - setTimeoutMs(ms: number): void { - this.timeoutMs = ms - } - - /** - * Check if this tool has been marked complete - */ - hasBeenMarkedComplete(): boolean { - return this.isMarkedComplete - } - - /** - * Ensure the tool is marked complete. If not already marked, marks it with error. - * This should be called in finally blocks to prevent leaked tool calls. - */ - async ensureMarkedComplete( - fallbackMessage = 'Tool execution did not complete properly' - ): Promise { - if (!this.isMarkedComplete) { - baseToolLogger.warn('Tool was not marked complete, marking with error', { - toolCallId: this.toolCallId, - toolName: this.name, - state: this.state, - }) - await this.markToolComplete(500, fallbackMessage) - this.setState(ClientToolCallState.error) - } - } - - /** - * Execute with timeout protection. Wraps the execution in a timeout and ensures - * markToolComplete is always called. - */ - async executeWithTimeout(executeFn: () => Promise, timeoutMs?: number): Promise { - const timeout = timeoutMs ?? this.timeoutMs - let timeoutId: NodeJS.Timeout | null = null - - try { - await Promise.race([ - executeFn(), - new Promise((_, reject) => { - timeoutId = setTimeout(() => { - reject(new Error(`Tool execution timed out after ${timeout / 1000} seconds`)) - }, timeout) - }), - ]) - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - baseToolLogger.error('Tool execution failed or timed out', { - toolCallId: this.toolCallId, - toolName: this.name, - error: message, - }) - // Only mark complete if not already marked - if (!this.isMarkedComplete) { - await this.markToolComplete(500, message) - this.setState(ClientToolCallState.error) - } - } finally { - if (timeoutId) clearTimeout(timeoutId) - // Ensure tool is always marked complete - await this.ensureMarkedComplete() - } - } - - // Intentionally left empty - specific tools can override - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async execute(_args?: Record): Promise { - return - } - - /** - * Mark a tool as complete. Tool completion is now handled server-side by the - * orchestrator (which calls the Go backend directly). Client tools are retained - * for UI display only — this method just tracks local state. - */ - async markToolComplete(_status: number, _message?: unknown, _data?: unknown): Promise { - this.isMarkedComplete = true - return true - } - - // Accept (continue) for interrupt flows: move pending -> executing - async handleAccept(): Promise { - this.setState(ClientToolCallState.executing) - } - - // Reject (skip) for interrupt flows: mark complete with a standard skip message - async handleReject(): Promise { - await this.markToolComplete(200, 'Tool execution was skipped by the user') - this.setState(ClientToolCallState.rejected) - } - - // Return the display configuration for the current state - getDisplayState(): ClientToolDisplay | undefined { - return this.metadata.displayNames[this.state] - } - - // Return interrupt display config (labels/icons) if defined - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - return this.metadata.interrupt - } - - // Transition to a new state (also sync to Copilot store) - setState(next: ClientToolCallState, options?: { result?: any }): void { - const prev = this.state - this.state = next - - // Notify store via manager to avoid import cycles - try { - const { syncToolState } = require('@/lib/copilot/tools/client/manager') - syncToolState(this.toolCallId, next, options) - } catch {} - - // Log transition after syncing - try { - baseToolLogger.info('setState transition', { - toolCallId: this.toolCallId, - toolName: this.name, - prev, - next, - hasResult: options?.result !== undefined, - }) - } catch {} - } - - // Expose current state - getState(): ClientToolCallState { - return this.state - } - - hasInterrupt(): boolean { - return !!this.metadata.interrupt - } - - /** - * Get UI configuration for this tool. - * Used by tool-call component to determine rendering behavior. - */ - getUIConfig(): ToolUIConfig | undefined { - return this.metadata.uiConfig - } +export const WORKFLOW_EXECUTION_TIMEOUT_MS = 10 * 60 * 1000 + +/** Event detail for OAuth connect events dispatched by the copilot. */ +export interface OAuthConnectEventDetail { + providerName: string + serviceId: string + providerId: string + requiredScopes: string[] + newScopes?: string[] } diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts deleted file mode 100644 index 88e696397..000000000 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-config.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { FileCode, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { getLatestBlock } from '@/blocks/registry' - -export class GetBlockConfigClientTool extends BaseClientTool { - static readonly id = 'get_block_config' - - constructor(toolCallId: string) { - super(toolCallId, GetBlockConfigClientTool.id, GetBlockConfigClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting block config', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved block config', icon: FileCode }, - [ClientToolCallState.error]: { text: 'Failed to get block config', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting block config', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped getting block config', - icon: MinusCircle, - }, - }, - getDynamicText: (params, state) => { - if (params?.blockType && typeof params.blockType === 'string') { - const blockConfig = getLatestBlock(params.blockType) - const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase() - const opSuffix = params.operation ? ` (${params.operation})` : '' - - switch (state) { - case ClientToolCallState.success: - return `Retrieved ${blockName}${opSuffix} config` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Retrieving ${blockName}${opSuffix} config` - case ClientToolCallState.error: - return `Failed to retrieve ${blockName}${opSuffix} config` - case ClientToolCallState.aborted: - return `Aborted retrieving ${blockName}${opSuffix} config` - case ClientToolCallState.rejected: - return `Skipped retrieving ${blockName}${opSuffix} config` - } - } - return undefined - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts deleted file mode 100644 index 993773f0e..000000000 --- a/apps/sim/lib/copilot/tools/client/blocks/get-block-options.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { getLatestBlock } from '@/blocks/registry' - -export class GetBlockOptionsClientTool extends BaseClientTool { - static readonly id = 'get_block_options' - - constructor(toolCallId: string) { - super(toolCallId, GetBlockOptionsClientTool.id, GetBlockOptionsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting block operations', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved block operations', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to get block operations', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting block operations', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped getting block operations', - icon: MinusCircle, - }, - }, - getDynamicText: (params, state) => { - const blockId = - (params as any)?.blockId || - (params as any)?.blockType || - (params as any)?.block_id || - (params as any)?.block_type - if (typeof blockId === 'string') { - const blockConfig = getLatestBlock(blockId) - const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase() - - switch (state) { - case ClientToolCallState.success: - return `Retrieved ${blockName} operations` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Retrieving ${blockName} operations` - case ClientToolCallState.error: - return `Failed to retrieve ${blockName} operations` - case ClientToolCallState.aborted: - return `Aborted retrieving ${blockName} operations` - case ClientToolCallState.rejected: - return `Skipped retrieving ${blockName} operations` - } - } - return undefined - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts deleted file mode 100644 index 17108c6db..000000000 --- a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-and-tools.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Blocks, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetBlocksAndToolsClientTool extends BaseClientTool { - static readonly id = 'get_blocks_and_tools' - - constructor(toolCallId: string) { - super(toolCallId, GetBlocksAndToolsClientTool.id, GetBlocksAndToolsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Exploring available options', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Explored available options', icon: Blocks }, - [ClientToolCallState.error]: { text: 'Failed to explore options', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted exploring options', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped exploring options', icon: MinusCircle }, - }, - interrupt: undefined, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts b/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts deleted file mode 100644 index fd547fa0c..000000000 --- a/apps/sim/lib/copilot/tools/client/blocks/get-blocks-metadata.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetBlocksMetadataClientTool extends BaseClientTool { - static readonly id = 'get_blocks_metadata' - - constructor(toolCallId: string) { - super(toolCallId, GetBlocksMetadataClientTool.id, GetBlocksMetadataClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching block choices', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Searched block choices', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to search block choices', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted searching block choices', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped searching block choices', - icon: MinusCircle, - }, - }, - getDynamicText: (params, state) => { - if (params?.blockIds && Array.isArray(params.blockIds) && params.blockIds.length > 0) { - const blockList = params.blockIds - .slice(0, 3) - .map((blockId) => blockId.replace(/_/g, ' ')) - .join(', ') - const more = params.blockIds.length > 3 ? '...' : '' - const blocks = `${blockList}${more}` - - switch (state) { - case ClientToolCallState.success: - return `Searched ${blocks}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching ${blocks}` - case ClientToolCallState.error: - return `Failed to search ${blocks}` - case ClientToolCallState.aborted: - return `Aborted searching ${blocks}` - case ClientToolCallState.rejected: - return `Skipped searching ${blocks}` - } - } - return undefined - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts deleted file mode 100644 index 2d8bda809..000000000 --- a/apps/sim/lib/copilot/tools/client/blocks/get-trigger-blocks.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ListFilter, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetTriggerBlocksClientTool extends BaseClientTool { - static readonly id = 'get_trigger_blocks' - - constructor(toolCallId: string) { - super(toolCallId, GetTriggerBlocksClientTool.id, GetTriggerBlocksClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Finding trigger blocks', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Found trigger blocks', icon: ListFilter }, - [ClientToolCallState.error]: { text: 'Failed to find trigger blocks', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted finding trigger blocks', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped finding trigger blocks', icon: MinusCircle }, - }, - interrupt: undefined, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts b/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts deleted file mode 100644 index 258330e0e..000000000 --- a/apps/sim/lib/copilot/tools/client/examples/get-examples-rag.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Loader2, MinusCircle, Search, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetExamplesRagClientTool extends BaseClientTool { - static readonly id = 'get_examples_rag' - - constructor(toolCallId: string) { - super(toolCallId, GetExamplesRagClientTool.id, GetExamplesRagClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching examples', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched examples', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to fetch examples', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Found examples for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching examples for ${query}` - case ClientToolCallState.error: - return `Failed to find examples for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching examples for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching examples for ${query}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/examples/get-operations-examples.ts b/apps/sim/lib/copilot/tools/client/examples/get-operations-examples.ts deleted file mode 100644 index 4a14b71ef..000000000 --- a/apps/sim/lib/copilot/tools/client/examples/get-operations-examples.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { Loader2, MinusCircle, XCircle, Zap } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetOperationsExamplesClientTool extends BaseClientTool { - static readonly id = 'get_operations_examples' - - constructor(toolCallId: string) { - super(toolCallId, GetOperationsExamplesClientTool.id, GetOperationsExamplesClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Designing workflow component', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Designed workflow component', icon: Zap }, - [ClientToolCallState.error]: { text: 'Failed to design workflow component', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted designing workflow component', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped designing workflow component', - icon: MinusCircle, - }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Designed ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Designing ${query}` - case ClientToolCallState.error: - return `Failed to design ${query}` - case ClientToolCallState.aborted: - return `Aborted designing ${query}` - case ClientToolCallState.rejected: - return `Skipped designing ${query}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts b/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts deleted file mode 100644 index f24ea4801..000000000 --- a/apps/sim/lib/copilot/tools/client/examples/get-trigger-examples.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Loader2, MinusCircle, XCircle, Zap } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetTriggerExamplesClientTool extends BaseClientTool { - static readonly id = 'get_trigger_examples' - - constructor(toolCallId: string) { - super(toolCallId, GetTriggerExamplesClientTool.id, GetTriggerExamplesClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Selecting a trigger', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Selected a trigger', icon: Zap }, - [ClientToolCallState.error]: { text: 'Failed to select a trigger', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted selecting a trigger', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped selecting a trigger', icon: MinusCircle }, - }, - interrupt: undefined, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/examples/summarize.ts b/apps/sim/lib/copilot/tools/client/examples/summarize.ts deleted file mode 100644 index 240be300b..000000000 --- a/apps/sim/lib/copilot/tools/client/examples/summarize.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Loader2, MinusCircle, PencilLine, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SummarizeClientTool extends BaseClientTool { - static readonly id = 'summarize_conversation' - - constructor(toolCallId: string) { - super(toolCallId, SummarizeClientTool.id, SummarizeClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Summarizing conversation', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Summarized conversation', icon: PencilLine }, - [ClientToolCallState.error]: { text: 'Failed to summarize conversation', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted summarizing conversation', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped summarizing conversation', - icon: MinusCircle, - }, - }, - interrupt: undefined, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/init-tool-configs.ts b/apps/sim/lib/copilot/tools/client/init-tool-configs.ts deleted file mode 100644 index 336fdbb0c..000000000 --- a/apps/sim/lib/copilot/tools/client/init-tool-configs.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Initialize all tool UI configurations. - * - * This module imports all client tools to trigger their UI config registration. - * Import this module early in the app to ensure all tool configs are available. - */ - -// Other tools (subagents) -import './other/auth' -import './other/custom-tool' -import './other/debug' -import './other/deploy' -import './other/edit' -import './other/evaluate' -import './other/info' -import './other/knowledge' -import './other/make-api-request' -import './other/plan' -import './other/research' -import './other/sleep' -import './other/superagent' -import './other/test' -import './other/tour' -import './other/workflow' - -// Workflow tools -import './workflow/deploy-api' -import './workflow/deploy-chat' -import './workflow/deploy-mcp' -import './workflow/edit-workflow' -import './workflow/redeploy' -import './workflow/run-workflow' -import './workflow/set-global-workflow-variables' - -// User tools -import './user/set-environment-variables' diff --git a/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts deleted file mode 100644 index 0245c7dee..000000000 --- a/apps/sim/lib/copilot/tools/client/knowledge/knowledge-base.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Database, Loader2, MinusCircle, PlusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { type KnowledgeBaseArgs } from '@/lib/copilot/tools/shared/schemas' -import { useCopilotStore } from '@/stores/panel/copilot/store' - -/** - * Client tool for knowledge base operations - */ -export class KnowledgeBaseClientTool extends BaseClientTool { - static readonly id = 'knowledge_base' - - constructor(toolCallId: string) { - super(toolCallId, KnowledgeBaseClientTool.id, KnowledgeBaseClientTool.metadata) - } - - /** - * Only show interrupt for create operation - */ - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as KnowledgeBaseArgs | undefined - - // Only require confirmation for create operation - if (params?.operation === 'create') { - const name = params?.args?.name || 'new knowledge base' - return { - accept: { text: `Create "${name}"`, icon: PlusCircle }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - // No interrupt for list, get, query - auto-execute - return undefined - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Accessing knowledge base', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Accessed knowledge base', icon: Database }, - [ClientToolCallState.error]: { text: 'Failed to access knowledge base', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted knowledge base access', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped knowledge base access', icon: MinusCircle }, - }, - getDynamicText: (params: Record, state: ClientToolCallState) => { - const operation = params?.operation as string | undefined - const name = params?.args?.name as string | undefined - - const opVerbs: Record = { - create: { - active: 'Creating knowledge base', - past: 'Created knowledge base', - pending: name ? `Create knowledge base "${name}"?` : 'Create knowledge base?', - }, - list: { active: 'Listing knowledge bases', past: 'Listed knowledge bases' }, - get: { active: 'Getting knowledge base', past: 'Retrieved knowledge base' }, - query: { active: 'Querying knowledge base', past: 'Queried knowledge base' }, - } - const defaultVerb: { active: string; past: string; pending?: string } = { - active: 'Accessing knowledge base', - past: 'Accessed knowledge base', - } - const verb = operation ? opVerbs[operation] || defaultVerb : defaultVerb - - if (state === ClientToolCallState.success) { - return verb.past - } - if (state === ClientToolCallState.pending && verb.pending) { - return verb.pending - } - if ( - state === ClientToolCallState.generating || - state === ClientToolCallState.pending || - state === ClientToolCallState.executing - ) { - return verb.active - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(): Promise { - await this.execute() - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/manager.ts b/apps/sim/lib/copilot/tools/client/manager.ts deleted file mode 100644 index bb83771d1..000000000 --- a/apps/sim/lib/copilot/tools/client/manager.ts +++ /dev/null @@ -1,24 +0,0 @@ -const instances: Record = {} - -let syncStateFn: ((toolCallId: string, nextState: any, options?: { result?: any }) => void) | null = - null - -export function registerClientTool(toolCallId: string, instance: any) { - instances[toolCallId] = instance -} - -export function getClientTool(toolCallId: string): any | undefined { - return instances[toolCallId] -} - -export function registerToolStateSync( - fn: (toolCallId: string, nextState: any, options?: { result?: any }) => void -) { - syncStateFn = fn -} - -export function syncToolState(toolCallId: string, nextState: any, options?: { result?: any }) { - try { - syncStateFn?.(toolCallId, nextState, options) - } catch {} -} diff --git a/apps/sim/lib/copilot/tools/client/navigation/navigate-ui.ts b/apps/sim/lib/copilot/tools/client/navigation/navigate-ui.ts deleted file mode 100644 index 5b9d30c06..000000000 --- a/apps/sim/lib/copilot/tools/client/navigation/navigate-ui.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Navigation, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -type NavigationDestination = 'workflow' | 'logs' | 'templates' | 'vector_db' | 'settings' - -interface NavigateUIArgs { - destination: NavigationDestination - workflowName?: string -} - -export class NavigateUIClientTool extends BaseClientTool { - static readonly id = 'navigate_ui' - - constructor(toolCallId: string) { - super(toolCallId, NavigateUIClientTool.id, NavigateUIClientTool.metadata) - } - - /** - * Override to provide dynamic button text based on destination - */ - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as NavigateUIArgs | undefined - - const destination = params?.destination - const workflowName = params?.workflowName - - let buttonText = 'Navigate' - - if (destination === 'workflow' && workflowName) { - buttonText = 'Open workflow' - } else if (destination === 'logs') { - buttonText = 'Open logs' - } else if (destination === 'templates') { - buttonText = 'Open templates' - } else if (destination === 'vector_db') { - buttonText = 'Open vector DB' - } else if (destination === 'settings') { - buttonText = 'Open settings' - } - - return { - accept: { text: buttonText, icon: Navigation }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to open', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Open?', icon: Navigation }, - [ClientToolCallState.executing]: { text: 'Opening', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Opened', icon: Navigation }, - [ClientToolCallState.error]: { text: 'Failed to open', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted opening', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped opening', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Open', icon: Navigation }, - reject: { text: 'Skip', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const destination = params?.destination as NavigationDestination | undefined - const workflowName = params?.workflowName - - const action = 'open' - const actionCapitalized = 'Open' - const actionPast = 'opened' - const actionIng = 'opening' - let target = '' - - if (destination === 'workflow' && workflowName) { - target = ` workflow "${workflowName}"` - } else if (destination === 'workflow') { - target = ' workflows' - } else if (destination === 'logs') { - target = ' logs' - } else if (destination === 'templates') { - target = ' templates' - } else if (destination === 'vector_db') { - target = ' vector database' - } else if (destination === 'settings') { - target = ' settings' - } - - const fullAction = `${action}${target}` - const fullActionCapitalized = `${actionCapitalized}${target}` - const fullActionPast = `${actionPast}${target}` - const fullActionIng = `${actionIng}${target}` - - switch (state) { - case ClientToolCallState.success: - return fullActionPast.charAt(0).toUpperCase() + fullActionPast.slice(1) - case ClientToolCallState.executing: - return fullActionIng.charAt(0).toUpperCase() + fullActionIng.slice(1) - case ClientToolCallState.generating: - return `Preparing to ${fullAction}` - case ClientToolCallState.pending: - return `${fullActionCapitalized}?` - case ClientToolCallState.error: - return `Failed to ${fullAction}` - case ClientToolCallState.aborted: - return `Aborted ${fullAction}` - case ClientToolCallState.rejected: - return `Skipped ${fullAction}` - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: NavigateUIArgs): Promise { - const logger = createLogger('NavigateUIClientTool') - try { - this.setState(ClientToolCallState.executing) - - // Get params from copilot store if not provided directly - let destination = args?.destination - let workflowName = args?.workflowName - - if (!destination) { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as NavigateUIArgs | undefined - destination = params?.destination - workflowName = params?.workflowName - } - - if (!destination) { - throw new Error('No destination provided') - } - - let navigationUrl = '' - let successMessage = '' - - // Get current workspace ID from URL - const workspaceId = window.location.pathname.split('/')[2] - - switch (destination) { - case 'workflow': - if (workflowName) { - // Find workflow by name - const { workflows } = useWorkflowRegistry.getState() - const workflow = Object.values(workflows).find( - (w) => w.name.toLowerCase() === workflowName.toLowerCase() - ) - - if (!workflow) { - throw new Error(`Workflow "${workflowName}" not found`) - } - - navigationUrl = `/workspace/${workspaceId}/w/${workflow.id}` - successMessage = `Navigated to workflow "${workflowName}"` - } else { - navigationUrl = `/workspace/${workspaceId}/w` - successMessage = 'Navigated to workflows' - } - break - - case 'logs': - navigationUrl = `/workspace/${workspaceId}/logs` - successMessage = 'Navigated to logs' - break - - case 'templates': - navigationUrl = `/workspace/${workspaceId}/templates` - successMessage = 'Navigated to templates' - break - - case 'vector_db': - navigationUrl = `/workspace/${workspaceId}/vector-db` - successMessage = 'Navigated to vector database' - break - - case 'settings': - window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'general' } })) - successMessage = 'Opened settings' - break - - default: - throw new Error(`Unknown destination: ${destination}`) - } - - // Navigate if URL was set - if (navigationUrl) { - window.location.href = navigationUrl - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, successMessage, { - destination, - workflowName, - navigated: true, - }) - } catch (e: any) { - logger.error('Navigation failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - - // Get destination info for better error message - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as NavigateUIArgs | undefined - const dest = params?.destination - const wfName = params?.workflowName - - let errorMessage = e?.message || 'Failed to navigate' - if (dest === 'workflow' && wfName) { - errorMessage = `Failed to navigate to workflow "${wfName}": ${e?.message || 'Unknown error'}` - } else if (dest) { - errorMessage = `Failed to navigate to ${dest}: ${e?.message || 'Unknown error'}` - } - - await this.markToolComplete(500, errorMessage) - } - } - - async execute(args?: NavigateUIArgs): Promise { - await this.handleAccept(args) - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/auth.ts b/apps/sim/lib/copilot/tools/client/other/auth.ts deleted file mode 100644 index b73a3f003..000000000 --- a/apps/sim/lib/copilot/tools/client/other/auth.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { KeyRound, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface AuthArgs { - instruction: string -} - -/** - * Auth tool that spawns a subagent to handle authentication setup. - * This tool auto-executes and the actual work is done by the auth subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class AuthClientTool extends BaseClientTool { - static readonly id = 'auth' - - constructor(toolCallId: string) { - super(toolCallId, AuthClientTool.id, AuthClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Authenticating', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Authenticated', icon: KeyRound }, - [ClientToolCallState.error]: { text: 'Failed to authenticate', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped auth', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted auth', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Authenticating', - completedLabel: 'Authenticated', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the auth tool. - * This just marks the tool as executing - the actual auth work is done server-side - * by the auth subagent, and its output is streamed as subagent events. - */ - async execute(_args?: AuthArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(AuthClientTool.id, AuthClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/checkoff-todo.ts b/apps/sim/lib/copilot/tools/client/other/checkoff-todo.ts deleted file mode 100644 index 2a925d82d..000000000 --- a/apps/sim/lib/copilot/tools/client/other/checkoff-todo.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Check, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -interface CheckoffTodoArgs { - id?: string - todoId?: string -} - -export class CheckoffTodoClientTool extends BaseClientTool { - static readonly id = 'checkoff_todo' - - constructor(toolCallId: string) { - super(toolCallId, CheckoffTodoClientTool.id, CheckoffTodoClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Marking todo', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Marking todo', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Marked todo complete', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to mark todo', icon: XCircle }, - }, - } - - async execute(args?: CheckoffTodoArgs): Promise { - const logger = createLogger('CheckoffTodoClientTool') - try { - this.setState(ClientToolCallState.executing) - - const todoId = args?.id || args?.todoId - if (!todoId) { - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'Missing todo id') - return - } - - try { - const { useCopilotStore } = await import('@/stores/panel/copilot/store') - const store = useCopilotStore.getState() - if (store.updatePlanTodoStatus) { - store.updatePlanTodoStatus(todoId, 'completed') - } - } catch (e) { - logger.warn('Failed to update todo status in store', { message: (e as any)?.message }) - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Todo checked off', { todoId }) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to check off todo') - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/crawl-website.ts b/apps/sim/lib/copilot/tools/client/other/crawl-website.ts deleted file mode 100644 index 37c220d36..000000000 --- a/apps/sim/lib/copilot/tools/client/other/crawl-website.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class CrawlWebsiteClientTool extends BaseClientTool { - static readonly id = 'crawl_website' - - constructor(toolCallId: string) { - super(toolCallId, CrawlWebsiteClientTool.id, CrawlWebsiteClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Crawling website', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Crawled website', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to crawl website', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted crawling website', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped crawling website', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const url = params.url - - switch (state) { - case ClientToolCallState.success: - return `Crawled ${url}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Crawling ${url}` - case ClientToolCallState.error: - return `Failed to crawl ${url}` - case ClientToolCallState.aborted: - return `Aborted crawling ${url}` - case ClientToolCallState.rejected: - return `Skipped crawling ${url}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/custom-tool.ts b/apps/sim/lib/copilot/tools/client/other/custom-tool.ts deleted file mode 100644 index eab2818a8..000000000 --- a/apps/sim/lib/copilot/tools/client/other/custom-tool.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Loader2, Wrench, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface CustomToolArgs { - instruction: string -} - -/** - * Custom tool that spawns a subagent to manage custom tools. - * This tool auto-executes and the actual work is done by the custom_tool subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class CustomToolClientTool extends BaseClientTool { - static readonly id = 'custom_tool' - - constructor(toolCallId: string) { - super(toolCallId, CustomToolClientTool.id, CustomToolClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Wrench }, - [ClientToolCallState.error]: { text: 'Failed custom tool', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped custom tool', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted custom tool', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Managing custom tool', - completedLabel: 'Custom tool managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the custom_tool tool. - * This just marks the tool as executing - the actual custom tool work is done server-side - * by the custom_tool subagent, and its output is streamed as subagent events. - */ - async execute(_args?: CustomToolArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(CustomToolClientTool.id, CustomToolClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/debug.ts b/apps/sim/lib/copilot/tools/client/other/debug.ts deleted file mode 100644 index 6be16d886..000000000 --- a/apps/sim/lib/copilot/tools/client/other/debug.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Bug, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface DebugArgs { - error_description: string - context?: string -} - -/** - * Debug tool that spawns a subagent to diagnose workflow issues. - * This tool auto-executes and the actual work is done by the debug subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class DebugClientTool extends BaseClientTool { - static readonly id = 'debug' - - constructor(toolCallId: string) { - super(toolCallId, DebugClientTool.id, DebugClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, - [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped debug', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted debug', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Debugging', - completedLabel: 'Debugged', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the debug tool. - * This just marks the tool as executing - the actual debug work is done server-side - * by the debug subagent, and its output is streamed as subagent events. - */ - async execute(_args?: DebugArgs): Promise { - // Immediately transition to executing state - no user confirmation needed - this.setState(ClientToolCallState.executing) - // The tool result will come from the server via tool_result event - // when the debug subagent completes its work - } -} - -// Register UI config at module load -registerToolUIConfig(DebugClientTool.id, DebugClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/deploy.ts b/apps/sim/lib/copilot/tools/client/other/deploy.ts deleted file mode 100644 index 80e8f8bc6..000000000 --- a/apps/sim/lib/copilot/tools/client/other/deploy.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Loader2, Rocket, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface DeployArgs { - instruction: string -} - -/** - * Deploy tool that spawns a subagent to handle deployment. - * This tool auto-executes and the actual work is done by the deploy subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class DeployClientTool extends BaseClientTool { - static readonly id = 'deploy' - - constructor(toolCallId: string) { - super(toolCallId, DeployClientTool.id, DeployClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Deploying', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to deploy', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped deploy', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted deploy', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Deploying', - completedLabel: 'Deployed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the deploy tool. - * This just marks the tool as executing - the actual deploy work is done server-side - * by the deploy subagent, and its output is streamed as subagent events. - */ - async execute(_args?: DeployArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(DeployClientTool.id, DeployClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/edit.ts b/apps/sim/lib/copilot/tools/client/other/edit.ts deleted file mode 100644 index 85e67a927..000000000 --- a/apps/sim/lib/copilot/tools/client/other/edit.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Loader2, Pencil, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface EditArgs { - instruction: string -} - -/** - * Edit tool that spawns a subagent to apply code/workflow edits. - * This tool auto-executes and the actual work is done by the edit subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class EditClientTool extends BaseClientTool { - static readonly id = 'edit' - - constructor(toolCallId: string) { - super(toolCallId, EditClientTool.id, EditClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Editing', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Edited', icon: Pencil }, - [ClientToolCallState.error]: { text: 'Failed to apply edit', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped edit', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted edit', icon: XCircle }, - }, - uiConfig: { - isSpecial: true, - subagent: { - streamingLabel: 'Editing', - completedLabel: 'Edited', - shouldCollapse: false, // Edit subagent stays expanded - outputArtifacts: ['edit_summary'], - hideThinkingText: true, // We show WorkflowEditSummary instead - }, - }, - } - - /** - * Execute the edit tool. - * This just marks the tool as executing - the actual edit work is done server-side - * by the edit subagent, and its output is streamed as subagent events. - */ - async execute(_args?: EditArgs): Promise { - // Immediately transition to executing state - no user confirmation needed - this.setState(ClientToolCallState.executing) - // The tool result will come from the server via tool_result event - // when the edit subagent completes its work - } -} - -// Register UI config at module load -registerToolUIConfig(EditClientTool.id, EditClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/evaluate.ts b/apps/sim/lib/copilot/tools/client/other/evaluate.ts deleted file mode 100644 index eaf7f542a..000000000 --- a/apps/sim/lib/copilot/tools/client/other/evaluate.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ClipboardCheck, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface EvaluateArgs { - instruction: string -} - -/** - * Evaluate tool that spawns a subagent to evaluate workflows or outputs. - * This tool auto-executes and the actual work is done by the evaluate subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class EvaluateClientTool extends BaseClientTool { - static readonly id = 'evaluate' - - constructor(toolCallId: string) { - super(toolCallId, EvaluateClientTool.id, EvaluateClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Evaluating', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Evaluated', icon: ClipboardCheck }, - [ClientToolCallState.error]: { text: 'Failed to evaluate', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped evaluation', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted evaluation', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Evaluating', - completedLabel: 'Evaluated', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the evaluate tool. - * This just marks the tool as executing - the actual evaluation work is done server-side - * by the evaluate subagent, and its output is streamed as subagent events. - */ - async execute(_args?: EvaluateArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(EvaluateClientTool.id, EvaluateClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts b/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts deleted file mode 100644 index 5b30c9111..000000000 --- a/apps/sim/lib/copilot/tools/client/other/get-page-contents.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { FileText, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetPageContentsClientTool extends BaseClientTool { - static readonly id = 'get_page_contents' - - constructor(toolCallId: string) { - super(toolCallId, GetPageContentsClientTool.id, GetPageContentsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting page contents', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved page contents', icon: FileText }, - [ClientToolCallState.error]: { text: 'Failed to get page contents', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting page contents', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting page contents', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.urls && Array.isArray(params.urls) && params.urls.length > 0) { - const firstUrl = String(params.urls[0]) - const count = params.urls.length - - switch (state) { - case ClientToolCallState.success: - return count > 1 ? `Retrieved ${count} pages` : `Retrieved ${firstUrl}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return count > 1 ? `Getting ${count} pages` : `Getting ${firstUrl}` - case ClientToolCallState.error: - return count > 1 ? `Failed to get ${count} pages` : `Failed to get ${firstUrl}` - case ClientToolCallState.aborted: - return count > 1 ? `Aborted getting ${count} pages` : `Aborted getting ${firstUrl}` - case ClientToolCallState.rejected: - return count > 1 ? `Skipped getting ${count} pages` : `Skipped getting ${firstUrl}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/info.ts b/apps/sim/lib/copilot/tools/client/other/info.ts deleted file mode 100644 index e4253a22c..000000000 --- a/apps/sim/lib/copilot/tools/client/other/info.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Info, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface InfoArgs { - instruction: string -} - -/** - * Info tool that spawns a subagent to retrieve information. - * This tool auto-executes and the actual work is done by the info subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class InfoClientTool extends BaseClientTool { - static readonly id = 'info' - - constructor(toolCallId: string) { - super(toolCallId, InfoClientTool.id, InfoClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting info', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved info', icon: Info }, - [ClientToolCallState.error]: { text: 'Failed to get info', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped info', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted info', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Getting info', - completedLabel: 'Info retrieved', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the info tool. - * This just marks the tool as executing - the actual info work is done server-side - * by the info subagent, and its output is streamed as subagent events. - */ - async execute(_args?: InfoArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(InfoClientTool.id, InfoClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/knowledge.ts b/apps/sim/lib/copilot/tools/client/other/knowledge.ts deleted file mode 100644 index 25c853c71..000000000 --- a/apps/sim/lib/copilot/tools/client/other/knowledge.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { BookOpen, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface KnowledgeArgs { - instruction: string -} - -/** - * Knowledge tool that spawns a subagent to manage knowledge bases. - * This tool auto-executes and the actual work is done by the knowledge subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class KnowledgeClientTool extends BaseClientTool { - static readonly id = 'knowledge' - - constructor(toolCallId: string) { - super(toolCallId, KnowledgeClientTool.id, KnowledgeClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing knowledge', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed knowledge', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to manage knowledge', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped knowledge', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted knowledge', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Managing knowledge', - completedLabel: 'Knowledge managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the knowledge tool. - * This just marks the tool as executing - the actual knowledge search work is done server-side - * by the knowledge subagent, and its output is streamed as subagent events. - */ - async execute(_args?: KnowledgeArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(KnowledgeClientTool.id, KnowledgeClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/make-api-request.ts b/apps/sim/lib/copilot/tools/client/other/make-api-request.ts deleted file mode 100644 index 37d78b17c..000000000 --- a/apps/sim/lib/copilot/tools/client/other/make-api-request.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Globe2, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' - -interface MakeApiRequestArgs { - url: string - method: 'GET' | 'POST' | 'PUT' - queryParams?: Record - headers?: Record - body?: any -} - -export class MakeApiRequestClientTool extends BaseClientTool { - static readonly id = 'make_api_request' - - constructor(toolCallId: string) { - super(toolCallId, MakeApiRequestClientTool.id, MakeApiRequestClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Preparing API request', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Review API request', icon: Globe2 }, - [ClientToolCallState.executing]: { text: 'Executing API request', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed API request', icon: Globe2 }, - [ClientToolCallState.error]: { text: 'Failed to execute API request', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped API request', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted API request', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Execute', icon: Globe2 }, - reject: { text: 'Skip', icon: MinusCircle }, - }, - uiConfig: { - interrupt: { - accept: { text: 'Execute', icon: Globe2 }, - reject: { text: 'Skip', icon: MinusCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'method', label: 'Method', width: '26%', editable: true, mono: true }, - { key: 'url', label: 'Endpoint', width: '74%', editable: true, mono: true }, - ], - extractRows: (params) => { - return [['request', (params.method || 'GET').toUpperCase(), params.url || '']] - }, - }, - }, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const method = params.method || 'GET' - let url = params.url - - // Extract domain from URL for cleaner display - try { - const urlObj = new URL(url) - url = urlObj.hostname + urlObj.pathname - } catch { - // Use URL as-is if parsing fails - } - - switch (state) { - case ClientToolCallState.success: - return `${method} ${url} complete` - case ClientToolCallState.executing: - return `${method} ${url}` - case ClientToolCallState.generating: - return `Preparing ${method} ${url}` - case ClientToolCallState.pending: - return `Review ${method} ${url}` - case ClientToolCallState.error: - return `Failed ${method} ${url}` - case ClientToolCallState.rejected: - return `Skipped ${method} ${url}` - case ClientToolCallState.aborted: - return `Aborted ${method} ${url}` - } - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(_args?: MakeApiRequestArgs): Promise { - // Tool execution is handled server-side by the orchestrator. - this.setState(ClientToolCallState.executing) - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} - -// Register UI config at module load -registerToolUIConfig(MakeApiRequestClientTool.id, MakeApiRequestClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/mark-todo-in-progress.ts b/apps/sim/lib/copilot/tools/client/other/mark-todo-in-progress.ts deleted file mode 100644 index fbed86ea8..000000000 --- a/apps/sim/lib/copilot/tools/client/other/mark-todo-in-progress.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -interface MarkTodoInProgressArgs { - id?: string - todoId?: string -} - -export class MarkTodoInProgressClientTool extends BaseClientTool { - static readonly id = 'mark_todo_in_progress' - - constructor(toolCallId: string) { - super(toolCallId, MarkTodoInProgressClientTool.id, MarkTodoInProgressClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Marking todo in progress', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Marked todo in progress', icon: Loader2 }, - [ClientToolCallState.error]: { text: 'Failed to mark in progress', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted marking in progress', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped marking in progress', icon: MinusCircle }, - }, - } - - async execute(args?: MarkTodoInProgressArgs): Promise { - const logger = createLogger('MarkTodoInProgressClientTool') - try { - this.setState(ClientToolCallState.executing) - - const todoId = args?.id || args?.todoId - if (!todoId) { - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'Missing todo id') - return - } - - try { - const { useCopilotStore } = await import('@/stores/panel/copilot/store') - const store = useCopilotStore.getState() - if (store.updatePlanTodoStatus) { - store.updatePlanTodoStatus(todoId, 'executing') - } - } catch (e) { - logger.warn('Failed to update todo status in store', { message: (e as any)?.message }) - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Todo marked in progress', { todoId }) - this.setState(ClientToolCallState.success) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to mark todo in progress') - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/oauth-request-access.ts b/apps/sim/lib/copilot/tools/client/other/oauth-request-access.ts deleted file mode 100644 index 725f73bc7..000000000 --- a/apps/sim/lib/copilot/tools/client/other/oauth-request-access.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { createLogger } from '@sim/logger' -import { CheckCircle, Loader2, MinusCircle, PlugZap, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { OAUTH_PROVIDERS, type OAuthServiceConfig } from '@/lib/oauth' - -const logger = createLogger('OAuthRequestAccessClientTool') - -interface OAuthRequestAccessArgs { - providerName?: string -} - -interface ResolvedServiceInfo { - serviceId: string - providerId: string - service: OAuthServiceConfig -} - -/** - * Finds the service configuration from a provider name. - * The providerName should match the exact `name` field returned by get_credentials tool's notConnected services. - */ -function findServiceByName(providerName: string): ResolvedServiceInfo | null { - const normalizedName = providerName.toLowerCase().trim() - - // First pass: exact match (case-insensitive) - for (const [, providerConfig] of Object.entries(OAUTH_PROVIDERS)) { - for (const [serviceId, service] of Object.entries(providerConfig.services)) { - if (service.name.toLowerCase() === normalizedName) { - return { serviceId, providerId: service.providerId, service } - } - } - } - - // Second pass: partial match as fallback for flexibility - for (const [, providerConfig] of Object.entries(OAUTH_PROVIDERS)) { - for (const [serviceId, service] of Object.entries(providerConfig.services)) { - if ( - service.name.toLowerCase().includes(normalizedName) || - normalizedName.includes(service.name.toLowerCase()) - ) { - return { serviceId, providerId: service.providerId, service } - } - } - } - - return null -} - -export interface OAuthConnectEventDetail { - providerName: string - serviceId: string - providerId: string - requiredScopes: string[] - newScopes?: string[] -} - -export class OAuthRequestAccessClientTool extends BaseClientTool { - static readonly id = 'oauth_request_access' - - private providerName?: string - - constructor(toolCallId: string) { - super(toolCallId, OAuthRequestAccessClientTool.id, OAuthRequestAccessClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Requesting integration access', icon: Loader2 }, - [ClientToolCallState.rejected]: { text: 'Skipped integration access', icon: MinusCircle }, - [ClientToolCallState.success]: { text: 'Requested integration access', icon: CheckCircle }, - [ClientToolCallState.error]: { text: 'Failed to request integration access', icon: X }, - [ClientToolCallState.aborted]: { text: 'Aborted integration access request', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Connect', icon: PlugZap }, - reject: { text: 'Skip', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - if (params.providerName) { - const name = params.providerName - switch (state) { - case ClientToolCallState.generating: - case ClientToolCallState.pending: - case ClientToolCallState.executing: - return `Requesting ${name} access` - case ClientToolCallState.rejected: - return `Skipped ${name} access` - case ClientToolCallState.success: - return `Requested ${name} access` - case ClientToolCallState.error: - return `Failed to request ${name} access` - case ClientToolCallState.aborted: - return `Aborted ${name} access request` - } - } - return undefined - }, - } - - async handleAccept(args?: OAuthRequestAccessArgs): Promise { - try { - if (args?.providerName) { - this.providerName = args.providerName - } - - if (!this.providerName) { - logger.error('No provider name provided') - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'No provider name specified') - return - } - - // Find the service by name - const serviceInfo = findServiceByName(this.providerName) - if (!serviceInfo) { - logger.error('Could not find OAuth service for provider', { - providerName: this.providerName, - }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, `Unknown provider: ${this.providerName}`) - return - } - - const { serviceId, providerId, service } = serviceInfo - - logger.info('Opening OAuth connect modal', { - providerName: this.providerName, - serviceId, - providerId, - }) - - // Move to executing state - this.setState(ClientToolCallState.executing) - - // Dispatch event to open the OAuth modal (same pattern as open-settings) - window.dispatchEvent( - new CustomEvent('open-oauth-connect', { - detail: { - providerName: this.providerName, - serviceId, - providerId, - requiredScopes: service.scopes || [], - }, - }) - ) - - // Mark as success - the user opened the prompt, but connection is not guaranteed - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `The user opened the ${this.providerName} connection prompt and may have connected. Check the connected integrations to verify the connection status.` - ) - } catch (e) { - logger.error('Failed to open OAuth connect modal', { error: e }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, 'Failed to open OAuth connection dialog') - } - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async execute(args?: OAuthRequestAccessArgs): Promise { - await this.handleAccept(args) - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/plan.ts b/apps/sim/lib/copilot/tools/client/other/plan.ts deleted file mode 100644 index 63eaad7b4..000000000 --- a/apps/sim/lib/copilot/tools/client/other/plan.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ListTodo, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface PlanArgs { - request: string -} - -/** - * Plan tool that spawns a subagent to plan an approach. - * This tool auto-executes and the actual work is done by the plan subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class PlanClientTool extends BaseClientTool { - static readonly id = 'plan' - - constructor(toolCallId: string) { - super(toolCallId, PlanClientTool.id, PlanClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Planning', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Planned', icon: ListTodo }, - [ClientToolCallState.error]: { text: 'Failed to plan', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped plan', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted plan', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Planning', - completedLabel: 'Planned', - shouldCollapse: true, - outputArtifacts: ['plan'], - }, - }, - } - - /** - * Execute the plan tool. - * This just marks the tool as executing - the actual planning work is done server-side - * by the plan subagent, and its output is streamed as subagent events. - */ - async execute(_args?: PlanArgs): Promise { - // Immediately transition to executing state - no user confirmation needed - this.setState(ClientToolCallState.executing) - // The tool result will come from the server via tool_result event - // when the plan subagent completes its work - } -} - -// Register UI config at module load -registerToolUIConfig(PlanClientTool.id, PlanClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/remember-debug.ts b/apps/sim/lib/copilot/tools/client/other/remember-debug.ts deleted file mode 100644 index 822ceda07..000000000 --- a/apps/sim/lib/copilot/tools/client/other/remember-debug.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { CheckCircle2, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class RememberDebugClientTool extends BaseClientTool { - static readonly id = 'remember_debug' - - constructor(toolCallId: string) { - super(toolCallId, RememberDebugClientTool.id, RememberDebugClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Validating fix', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Validated fix', icon: CheckCircle2 }, - [ClientToolCallState.error]: { text: 'Failed to validate', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted validation', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped validation', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - const operation = params?.operation - - if (operation === 'add' || operation === 'edit') { - // For add/edit, show from problem or solution - const text = params?.problem || params?.solution - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Validated fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Validating fix ${text}` - case ClientToolCallState.error: - return `Failed to validate fix ${text}` - case ClientToolCallState.aborted: - return `Aborted validating fix ${text}` - case ClientToolCallState.rejected: - return `Skipped validating fix ${text}` - } - } - } else if (operation === 'delete') { - // For delete, show from problem or solution (or id as fallback) - const text = params?.problem || params?.solution || params?.id - if (text && typeof text === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Adjusted fix ${text}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Adjusting fix ${text}` - case ClientToolCallState.error: - return `Failed to adjust fix ${text}` - case ClientToolCallState.aborted: - return `Aborted adjusting fix ${text}` - case ClientToolCallState.rejected: - return `Skipped adjusting fix ${text}` - } - } - } - - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/research.ts b/apps/sim/lib/copilot/tools/client/other/research.ts deleted file mode 100644 index 0a10e8989..000000000 --- a/apps/sim/lib/copilot/tools/client/other/research.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Loader2, Search, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface ResearchArgs { - instruction: string -} - -/** - * Research tool that spawns a subagent to research information. - * This tool auto-executes and the actual work is done by the research subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class ResearchClientTool extends BaseClientTool { - static readonly id = 'research' - - constructor(toolCallId: string) { - super(toolCallId, ResearchClientTool.id, ResearchClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Researching', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Researched', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to research', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped research', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted research', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Researching', - completedLabel: 'Researched', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the research tool. - * This just marks the tool as executing - the actual research work is done server-side - * by the research subagent, and its output is streamed as subagent events. - */ - async execute(_args?: ResearchArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(ResearchClientTool.id, ResearchClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/scrape-page.ts b/apps/sim/lib/copilot/tools/client/other/scrape-page.ts deleted file mode 100644 index 5979c9f0c..000000000 --- a/apps/sim/lib/copilot/tools/client/other/scrape-page.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class ScrapePageClientTool extends BaseClientTool { - static readonly id = 'scrape_page' - - constructor(toolCallId: string) { - super(toolCallId, ScrapePageClientTool.id, ScrapePageClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Scraping page', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Scraped page', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to scrape page', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted scraping page', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped scraping page', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.url && typeof params.url === 'string') { - const url = params.url - - switch (state) { - case ClientToolCallState.success: - return `Scraped ${url}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Scraping ${url}` - case ClientToolCallState.error: - return `Failed to scrape ${url}` - case ClientToolCallState.aborted: - return `Aborted scraping ${url}` - case ClientToolCallState.rejected: - return `Skipped scraping ${url}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/search-documentation.ts b/apps/sim/lib/copilot/tools/client/other/search-documentation.ts deleted file mode 100644 index 07fa971bb..000000000 --- a/apps/sim/lib/copilot/tools/client/other/search-documentation.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { BookOpen, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SearchDocumentationClientTool extends BaseClientTool { - static readonly id = 'search_documentation' - - constructor(toolCallId: string) { - super(toolCallId, SearchDocumentationClientTool.id, SearchDocumentationClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching documentation', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed documentation search', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to search docs', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted documentation search', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped documentation search', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Searched docs for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching docs for ${query}` - case ClientToolCallState.error: - return `Failed to search docs for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching docs for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching docs for ${query}` - } - } - return undefined - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/search-errors.ts b/apps/sim/lib/copilot/tools/client/other/search-errors.ts deleted file mode 100644 index d0eb6cc35..000000000 --- a/apps/sim/lib/copilot/tools/client/other/search-errors.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Bug, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SearchErrorsClientTool extends BaseClientTool { - static readonly id = 'search_errors' - - constructor(toolCallId: string) { - super(toolCallId, SearchErrorsClientTool.id, SearchErrorsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, - [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted debugging', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped debugging', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Debugged ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Debugging ${query}` - case ClientToolCallState.error: - return `Failed to debug ${query}` - case ClientToolCallState.aborted: - return `Aborted debugging ${query}` - case ClientToolCallState.rejected: - return `Skipped debugging ${query}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/search-library-docs.ts b/apps/sim/lib/copilot/tools/client/other/search-library-docs.ts deleted file mode 100644 index 7dcff295b..000000000 --- a/apps/sim/lib/copilot/tools/client/other/search-library-docs.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BookOpen, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SearchLibraryDocsClientTool extends BaseClientTool { - static readonly id = 'search_library_docs' - - constructor(toolCallId: string) { - super(toolCallId, SearchLibraryDocsClientTool.id, SearchLibraryDocsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Reading docs', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Read docs', icon: BookOpen }, - [ClientToolCallState.error]: { text: 'Failed to read docs', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted reading docs', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped reading docs', icon: MinusCircle }, - }, - getDynamicText: (params, state) => { - const libraryName = params?.library_name - if (libraryName && typeof libraryName === 'string') { - switch (state) { - case ClientToolCallState.success: - return `Read ${libraryName} docs` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Reading ${libraryName} docs` - case ClientToolCallState.error: - return `Failed to read ${libraryName} docs` - case ClientToolCallState.aborted: - return `Aborted reading ${libraryName} docs` - case ClientToolCallState.rejected: - return `Skipped reading ${libraryName} docs` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/search-online.ts b/apps/sim/lib/copilot/tools/client/other/search-online.ts deleted file mode 100644 index 083658468..000000000 --- a/apps/sim/lib/copilot/tools/client/other/search-online.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Globe, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SearchOnlineClientTool extends BaseClientTool { - static readonly id = 'search_online' - - constructor(toolCallId: string) { - super(toolCallId, SearchOnlineClientTool.id, SearchOnlineClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching online', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed online search', icon: Globe }, - [ClientToolCallState.error]: { text: 'Failed to search online', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped online search', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted online search', icon: XCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.query && typeof params.query === 'string') { - const query = params.query - - switch (state) { - case ClientToolCallState.success: - return `Searched online for ${query}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching online for ${query}` - case ClientToolCallState.error: - return `Failed to search online for ${query}` - case ClientToolCallState.aborted: - return `Aborted searching online for ${query}` - case ClientToolCallState.rejected: - return `Skipped searching online for ${query}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/search-patterns.ts b/apps/sim/lib/copilot/tools/client/other/search-patterns.ts deleted file mode 100644 index e16785a70..000000000 --- a/apps/sim/lib/copilot/tools/client/other/search-patterns.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Loader2, MinusCircle, Search, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class SearchPatternsClientTool extends BaseClientTool { - static readonly id = 'search_patterns' - - constructor(toolCallId: string) { - super(toolCallId, SearchPatternsClientTool.id, SearchPatternsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Searching workflow patterns', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Found workflow patterns', icon: Search }, - [ClientToolCallState.error]: { text: 'Failed to search patterns', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted pattern search', icon: MinusCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped pattern search', icon: MinusCircle }, - }, - interrupt: undefined, - getDynamicText: (params, state) => { - if (params?.queries && Array.isArray(params.queries) && params.queries.length > 0) { - const firstQuery = String(params.queries[0]) - - switch (state) { - case ClientToolCallState.success: - return `Searched ${firstQuery}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Searching ${firstQuery}` - case ClientToolCallState.error: - return `Failed to search ${firstQuery}` - case ClientToolCallState.aborted: - return `Aborted searching ${firstQuery}` - case ClientToolCallState.rejected: - return `Skipped searching ${firstQuery}` - } - } - return undefined - }, - } - - async execute(): Promise { - return - } -} diff --git a/apps/sim/lib/copilot/tools/client/other/sleep.ts b/apps/sim/lib/copilot/tools/client/other/sleep.ts deleted file mode 100644 index 91949ea81..000000000 --- a/apps/sim/lib/copilot/tools/client/other/sleep.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, MinusCircle, Moon, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -/** 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: 'Interrupted sleep', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped sleep', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted sleep', icon: MinusCircle }, - [ClientToolCallState.background]: { text: 'Resumed', icon: Moon }, - }, - uiConfig: { - secondaryAction: { - text: 'Wake', - title: 'Wake', - variant: 'tertiary', - showInStates: [ClientToolCallState.executing], - targetState: ClientToolCallState.background, - }, - }, - // 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) - } -} - -// Register UI config at module load -registerToolUIConfig(SleepClientTool.id, SleepClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/superagent.ts b/apps/sim/lib/copilot/tools/client/other/superagent.ts deleted file mode 100644 index 99ec1fbfe..000000000 --- a/apps/sim/lib/copilot/tools/client/other/superagent.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Loader2, Sparkles, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface SuperagentArgs { - instruction: string -} - -/** - * Superagent tool that spawns a powerful subagent for complex tasks. - * This tool auto-executes and the actual work is done by the superagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class SuperagentClientTool extends BaseClientTool { - static readonly id = 'superagent' - - constructor(toolCallId: string) { - super(toolCallId, SuperagentClientTool.id, SuperagentClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles }, - [ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Superagent working', - completedLabel: 'Superagent completed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the superagent tool. - * This just marks the tool as executing - the actual work is done server-side - * by the superagent, and its output is streamed as subagent events. - */ - async execute(_args?: SuperagentArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(SuperagentClientTool.id, SuperagentClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/test.ts b/apps/sim/lib/copilot/tools/client/other/test.ts deleted file mode 100644 index 3aa698aad..000000000 --- a/apps/sim/lib/copilot/tools/client/other/test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { FlaskConical, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface TestArgs { - instruction: string -} - -/** - * Test tool that spawns a subagent to run tests. - * This tool auto-executes and the actual work is done by the test subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class TestClientTool extends BaseClientTool { - static readonly id = 'test' - - constructor(toolCallId: string) { - super(toolCallId, TestClientTool.id, TestClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Testing', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Tested', icon: FlaskConical }, - [ClientToolCallState.error]: { text: 'Failed to test', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped test', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted test', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Testing', - completedLabel: 'Tested', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the test tool. - * This just marks the tool as executing - the actual test work is done server-side - * by the test subagent, and its output is streamed as subagent events. - */ - async execute(_args?: TestArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(TestClientTool.id, TestClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/tour.ts b/apps/sim/lib/copilot/tools/client/other/tour.ts deleted file mode 100644 index 8faca5587..000000000 --- a/apps/sim/lib/copilot/tools/client/other/tour.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Compass, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface TourArgs { - instruction: string -} - -/** - * Tour tool that spawns a subagent to guide the user. - * This tool auto-executes and the actual work is done by the tour subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class TourClientTool extends BaseClientTool { - static readonly id = 'tour' - - constructor(toolCallId: string) { - super(toolCallId, TourClientTool.id, TourClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Touring', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Completed tour', icon: Compass }, - [ClientToolCallState.error]: { text: 'Failed tour', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped tour', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted tour', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Touring', - completedLabel: 'Tour complete', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the tour tool. - * This just marks the tool as executing - the actual tour work is done server-side - * by the tour subagent, and its output is streamed as subagent events. - */ - async execute(_args?: TourArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(TourClientTool.id, TourClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/other/workflow.ts b/apps/sim/lib/copilot/tools/client/other/workflow.ts deleted file mode 100644 index 5b99e73e9..000000000 --- a/apps/sim/lib/copilot/tools/client/other/workflow.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { GitBranch, Loader2, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -interface WorkflowArgs { - instruction: string -} - -/** - * Workflow tool that spawns a subagent to manage workflows. - * This tool auto-executes and the actual work is done by the workflow subagent. - * The subagent's output is streamed as nested content under this tool call. - */ -export class WorkflowClientTool extends BaseClientTool { - static readonly id = 'workflow' - - constructor(toolCallId: string) { - super(toolCallId, WorkflowClientTool.id, WorkflowClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed workflow', icon: GitBranch }, - [ClientToolCallState.error]: { text: 'Failed to manage workflow', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped workflow', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted workflow', icon: XCircle }, - }, - uiConfig: { - subagent: { - streamingLabel: 'Managing workflow', - completedLabel: 'Workflow managed', - shouldCollapse: true, - outputArtifacts: [], - }, - }, - } - - /** - * Execute the workflow tool. - * This just marks the tool as executing - the actual workflow work is done server-side - * by the workflow subagent, and its output is streamed as subagent events. - */ - async execute(_args?: WorkflowArgs): Promise { - this.setState(ClientToolCallState.executing) - } -} - -// Register UI config at module load -registerToolUIConfig(WorkflowClientTool.id, WorkflowClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/registry.ts b/apps/sim/lib/copilot/tools/client/registry.ts deleted file mode 100644 index 7dfb757aa..000000000 --- a/apps/sim/lib/copilot/tools/client/registry.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { createLogger } from '@sim/logger' -import type { ClientToolDefinition, ToolExecutionContext } from '@/lib/copilot/tools/client/types' - -const logger = createLogger('ClientToolRegistry') - -const tools: Record> = {} - -export function registerTool(def: ClientToolDefinition) { - tools[def.name] = def -} - -export function getTool(name: string): ClientToolDefinition | undefined { - return tools[name] -} - -export function createExecutionContext(params: { - toolCallId: string - toolName: string -}): ToolExecutionContext { - const { toolCallId, toolName } = params - return { - toolCallId, - toolName, - log: (level, message, extra) => { - try { - logger[level](message, { toolCallId, toolName, ...(extra || {}) }) - } catch {} - }, - } -} - -export function getRegisteredTools(): Record> { - return { ...tools } -} diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts new file mode 100644 index 000000000..f7242d12c --- /dev/null +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -0,0 +1,2240 @@ +import type { LucideIcon } from 'lucide-react' +import { Blocks, BookOpen, Bug, Check, CheckCircle, CheckCircle2, ClipboardCheck, Compass, Database, FileCode, FileText, FlaskConical, GitBranch, Globe, Globe2, Grid2x2, Grid2x2Check, Grid2x2X, Info, Key, KeyRound, ListChecks, ListFilter, ListTodo, Loader2, MessageSquare, MinusCircle, Moon, Navigation, Pencil, PencilLine, Play, PlugZap, Plus, Rocket, Search, Server, Settings2, Sparkles, Tag, TerminalSquare, WorkflowIcon, Wrench, X, XCircle, Zap } from 'lucide-react' +import { getLatestBlock } from '@/blocks/registry' +import { getCustomTool } from '@/hooks/queries/custom-tools' +import { useWorkflowRegistry } from '@/stores/workflows/registry/store' + +export enum ClientToolCallState { + generating = 'generating', + pending = 'pending', + executing = 'executing', + aborted = 'aborted', + rejected = 'rejected', + success = 'success', + error = 'error', + review = 'review', + background = 'background', +} + +export interface ClientToolDisplay { + text: string + icon: LucideIcon +} + +export type DynamicTextFormatter = ( + params: Record, + state: ClientToolCallState +) => string | undefined + +export interface ToolUIConfig { + isSpecial?: boolean + subagent?: boolean + interrupt?: boolean + customRenderer?: string + paramsTable?: any + dynamicText?: DynamicTextFormatter + secondaryAction?: any + alwaysExpanded?: boolean + subagentLabels?: { + streaming: string + completed: string + } +} + +interface ToolMetadata { + displayNames: Partial> + interrupt?: { + accept: ClientToolDisplay + reject: ClientToolDisplay + } + getDynamicText?: DynamicTextFormatter + uiConfig?: { + isSpecial?: boolean + subagent?: { + streamingLabel?: string + completedLabel?: string + } + interrupt?: any + customRenderer?: string + paramsTable?: any + secondaryAction?: any + alwaysExpanded?: boolean + } +} + +interface ToolDisplayEntry { + displayNames: Partial> + uiConfig?: ToolUIConfig +} + +function toUiConfig(metadata?: ToolMetadata): ToolUIConfig | undefined { + const legacy = metadata?.uiConfig + const subagent = legacy?.subagent + const dynamicText = metadata?.getDynamicText + if (!legacy && !dynamicText) return undefined + + const config: ToolUIConfig = { + isSpecial: legacy?.isSpecial === true, + subagent: !!legacy?.subagent, + interrupt: !!legacy?.interrupt, + customRenderer: legacy?.customRenderer, + paramsTable: legacy?.paramsTable, + dynamicText, + secondaryAction: legacy?.secondaryAction, + alwaysExpanded: legacy?.alwaysExpanded, + } + + if (subagent?.streamingLabel || subagent?.completedLabel) { + config.subagentLabels = { + streaming: subagent.streamingLabel || '', + completed: subagent.completedLabel || '', + } + } + + return config +} + +function toToolDisplayEntry(metadata?: ToolMetadata): ToolDisplayEntry { + return { + displayNames: metadata?.displayNames || {}, + uiConfig: toUiConfig(metadata), + } +} + +const META_auth: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Authenticating', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Authenticated', icon: KeyRound }, + [ClientToolCallState.error]: { text: 'Failed to authenticate', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped auth', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted auth', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Authenticating', + completedLabel: 'Authenticated', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_check_deployment_status: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Checking deployment status', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Checking deployment status', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Checking deployment status', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Checked deployment status', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to check deployment status', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted checking deployment status', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped checking deployment status', + icon: XCircle, + }, + }, + interrupt: undefined, + } + +const META_checkoff_todo: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Marking todo', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Marking todo', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Marked todo complete', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to mark todo', icon: XCircle }, + }, + } + +const META_crawl_website: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Crawling website', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Crawled website', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to crawl website', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted crawling website', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped crawling website', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url + + switch (state) { + case ClientToolCallState.success: + return `Crawled ${url}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Crawling ${url}` + case ClientToolCallState.error: + return `Failed to crawl ${url}` + case ClientToolCallState.aborted: + return `Aborted crawling ${url}` + case ClientToolCallState.rejected: + return `Skipped crawling ${url}` + } + } + return undefined + }, + } + +const META_create_workspace_mcp_server: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to create MCP server', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Create MCP server?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Creating MCP server', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Created MCP server', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to create MCP server', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted creating MCP server', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped creating MCP server', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Create', icon: Plus }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const name = params?.name || 'MCP server' + switch (state) { + case ClientToolCallState.success: + return `Created MCP server "${name}"` + case ClientToolCallState.executing: + return `Creating MCP server "${name}"` + case ClientToolCallState.generating: + return `Preparing to create "${name}"` + case ClientToolCallState.pending: + return `Create MCP server "${name}"?` + case ClientToolCallState.error: + return `Failed to create "${name}"` + } + return undefined + }, + } + +const META_custom_tool: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Wrench }, + [ClientToolCallState.error]: { text: 'Failed custom tool', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped custom tool', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted custom tool', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing custom tool', + completedLabel: 'Custom tool managed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_debug: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, + [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped debug', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted debug', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Debugging', + completedLabel: 'Debugged', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_deploy: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Deploying', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to deploy', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped deploy', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted deploy', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Deploying', + completedLabel: 'Deployed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_deploy_api: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy API', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Deploy as API?', icon: Rocket }, + [ClientToolCallState.executing]: { text: 'Deploying API', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed API', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to deploy API', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted deploying API', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped deploying API', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Deploy', icon: Rocket }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, + interrupt: { + accept: { text: 'Deploy', icon: Rocket }, + reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + }, + getDynamicText: (params, state) => { + const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' + + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + const isAlreadyDeployed = workflowId + ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed + : false + + let actionText = action + let actionTextIng = action === 'undeploy' ? 'undeploying' : 'deploying' + const actionTextPast = action === 'undeploy' ? 'undeployed' : 'deployed' + + if (action === 'deploy' && isAlreadyDeployed) { + actionText = 'redeploy' + actionTextIng = 'redeploying' + } + + const actionCapitalized = actionText.charAt(0).toUpperCase() + actionText.slice(1) + + switch (state) { + case ClientToolCallState.success: + return `API ${actionTextPast}` + case ClientToolCallState.executing: + return `${actionCapitalized}ing API` + case ClientToolCallState.generating: + return `Preparing to ${actionText} API` + case ClientToolCallState.pending: + return `${actionCapitalized} API?` + case ClientToolCallState.error: + return `Failed to ${actionText} API` + case ClientToolCallState.aborted: + return `Aborted ${actionTextIng} API` + case ClientToolCallState.rejected: + return `Skipped ${actionTextIng} API` + } + return undefined + }, + } + +const META_deploy_chat: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy chat', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Deploy as chat?', icon: MessageSquare }, + [ClientToolCallState.executing]: { text: 'Deploying chat', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed chat', icon: MessageSquare }, + [ClientToolCallState.error]: { text: 'Failed to deploy chat', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted deploying chat', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped deploying chat', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Deploy Chat', icon: MessageSquare }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, + interrupt: { + accept: { text: 'Deploy Chat', icon: MessageSquare }, + reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + }, + getDynamicText: (params, state) => { + const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' + + switch (state) { + case ClientToolCallState.success: + return action === 'undeploy' ? 'Chat undeployed' : 'Chat deployed' + case ClientToolCallState.executing: + return action === 'undeploy' ? 'Undeploying chat' : 'Deploying chat' + case ClientToolCallState.generating: + return `Preparing to ${action} chat` + case ClientToolCallState.pending: + return action === 'undeploy' ? 'Undeploy chat?' : 'Deploy as chat?' + case ClientToolCallState.error: + return `Failed to ${action} chat` + case ClientToolCallState.aborted: + return action === 'undeploy' ? 'Aborted undeploying chat' : 'Aborted deploying chat' + case ClientToolCallState.rejected: + return action === 'undeploy' ? 'Skipped undeploying chat' : 'Skipped deploying chat' + } + return undefined + }, + } + +const META_deploy_mcp: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to deploy to MCP', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Deploy to MCP server?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Deploying to MCP', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Deployed to MCP', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to deploy to MCP', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted MCP deployment', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped MCP deployment', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Deploy', icon: Server }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, + interrupt: { + accept: { text: 'Deploy', icon: Server }, + reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + }, + getDynamicText: (params, state) => { + const toolName = params?.toolName || 'workflow' + switch (state) { + case ClientToolCallState.success: + return `Deployed "${toolName}" to MCP` + case ClientToolCallState.executing: + return `Deploying "${toolName}" to MCP` + case ClientToolCallState.generating: + return `Preparing to deploy to MCP` + case ClientToolCallState.pending: + return `Deploy "${toolName}" to MCP?` + case ClientToolCallState.error: + return `Failed to deploy to MCP` + } + return undefined + }, + } + +const META_edit: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Editing', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Edited', icon: Pencil }, + [ClientToolCallState.error]: { text: 'Failed to apply edit', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped edit', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted edit', icon: XCircle }, + }, + uiConfig: { + isSpecial: true, + subagent: { + streamingLabel: 'Editing', + completedLabel: 'Edited', + shouldCollapse: false, // Edit subagent stays expanded + outputArtifacts: ['edit_summary'], + hideThinkingText: true, // We show WorkflowEditSummary instead + }, + }, + } + +const META_edit_workflow: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Editing your workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Edited your workflow', icon: Grid2x2Check }, + [ClientToolCallState.error]: { text: 'Failed to edit your workflow', icon: XCircle }, + [ClientToolCallState.review]: { text: 'Review your workflow changes', icon: Grid2x2 }, + [ClientToolCallState.rejected]: { text: 'Rejected workflow changes', icon: Grid2x2X }, + [ClientToolCallState.aborted]: { text: 'Aborted editing your workflow', icon: MinusCircle }, + [ClientToolCallState.pending]: { text: 'Editing your workflow', icon: Loader2 }, + }, + uiConfig: { + isSpecial: true, + customRenderer: 'edit_summary', + }, + } + +const META_evaluate: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Evaluating', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Evaluated', icon: ClipboardCheck }, + [ClientToolCallState.error]: { text: 'Failed to evaluate', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped evaluation', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted evaluation', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Evaluating', + completedLabel: 'Evaluated', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_get_block_config: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting block config', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved block config', icon: FileCode }, + [ClientToolCallState.error]: { text: 'Failed to get block config', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting block config', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped getting block config', + icon: MinusCircle, + }, + }, + getDynamicText: (params, state) => { + if (params?.blockType && typeof params.blockType === 'string') { + const blockConfig = getLatestBlock(params.blockType) + const blockName = (blockConfig?.name ?? params.blockType.replace(/_/g, ' ')).toLowerCase() + const opSuffix = params.operation ? ` (${params.operation})` : '' + + switch (state) { + case ClientToolCallState.success: + return `Retrieved ${blockName}${opSuffix} config` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Retrieving ${blockName}${opSuffix} config` + case ClientToolCallState.error: + return `Failed to retrieve ${blockName}${opSuffix} config` + case ClientToolCallState.aborted: + return `Aborted retrieving ${blockName}${opSuffix} config` + case ClientToolCallState.rejected: + return `Skipped retrieving ${blockName}${opSuffix} config` + } + } + return undefined + }, + } + +const META_get_block_options: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting block operations', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved block operations', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to get block operations', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting block operations', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped getting block operations', + icon: MinusCircle, + }, + }, + getDynamicText: (params, state) => { + const blockId = + (params as any)?.blockId || + (params as any)?.blockType || + (params as any)?.block_id || + (params as any)?.block_type + if (typeof blockId === 'string') { + const blockConfig = getLatestBlock(blockId) + const blockName = (blockConfig?.name ?? blockId.replace(/_/g, ' ')).toLowerCase() + + switch (state) { + case ClientToolCallState.success: + return `Retrieved ${blockName} operations` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Retrieving ${blockName} operations` + case ClientToolCallState.error: + return `Failed to retrieve ${blockName} operations` + case ClientToolCallState.aborted: + return `Aborted retrieving ${blockName} operations` + case ClientToolCallState.rejected: + return `Skipped retrieving ${blockName} operations` + } + } + return undefined + }, + } + +const META_get_block_outputs: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag }, + [ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag }, + [ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const blockIds = params?.blockIds + if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { + const count = blockIds.length + switch (state) { + case ClientToolCallState.success: + return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Getting outputs for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.error: + return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}` + } + } + return undefined + }, + } + +const META_get_block_upstream_references: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch }, + [ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch }, + [ClientToolCallState.error]: { text: 'Failed to get references', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const blockIds = params?.blockIds + if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { + const count = blockIds.length + switch (state) { + case ClientToolCallState.success: + return `Retrieved references for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Getting references for ${count} block${count > 1 ? 's' : ''}` + case ClientToolCallState.error: + return `Failed to get references for ${count} block${count > 1 ? 's' : ''}` + } + } + return undefined + }, + } + +const META_get_blocks_and_tools: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Exploring available options', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Explored available options', icon: Blocks }, + [ClientToolCallState.error]: { text: 'Failed to explore options', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted exploring options', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped exploring options', icon: MinusCircle }, + }, + interrupt: undefined, + } + +const META_get_blocks_metadata: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching block choices', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Searched block choices', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to search block choices', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted searching block choices', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped searching block choices', + icon: MinusCircle, + }, + }, + getDynamicText: (params, state) => { + if (params?.blockIds && Array.isArray(params.blockIds) && params.blockIds.length > 0) { + const blockList = params.blockIds + .slice(0, 3) + .map((blockId) => blockId.replace(/_/g, ' ')) + .join(', ') + const more = params.blockIds.length > 3 ? '...' : '' + const blocks = `${blockList}${more}` + + switch (state) { + case ClientToolCallState.success: + return `Searched ${blocks}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching ${blocks}` + case ClientToolCallState.error: + return `Failed to search ${blocks}` + case ClientToolCallState.aborted: + return `Aborted searching ${blocks}` + case ClientToolCallState.rejected: + return `Skipped searching ${blocks}` + } + } + return undefined + }, + } + +const META_get_credentials: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching connected integrations', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched connected integrations', icon: Key }, + [ClientToolCallState.error]: { + text: 'Failed to fetch connected integrations', + icon: XCircle, + }, + [ClientToolCallState.aborted]: { + text: 'Aborted fetching connected integrations', + icon: MinusCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped fetching connected integrations', + icon: MinusCircle, + }, + }, + } + +const META_get_examples_rag: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching examples', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched examples', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to fetch examples', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting examples', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting examples', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Found examples for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching examples for ${query}` + case ClientToolCallState.error: + return `Failed to find examples for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching examples for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching examples for ${query}` + } + } + return undefined + }, + } + +const META_get_operations_examples: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Designing workflow component', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Designed workflow component', icon: Zap }, + [ClientToolCallState.error]: { text: 'Failed to design workflow component', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted designing workflow component', + icon: MinusCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped designing workflow component', + icon: MinusCircle, + }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Designed ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Designing ${query}` + case ClientToolCallState.error: + return `Failed to design ${query}` + case ClientToolCallState.aborted: + return `Aborted designing ${query}` + case ClientToolCallState.rejected: + return `Skipped designing ${query}` + } + } + return undefined + }, + } + +const META_get_page_contents: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting page contents', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved page contents', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to get page contents', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting page contents', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting page contents', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.urls && Array.isArray(params.urls) && params.urls.length > 0) { + const firstUrl = String(params.urls[0]) + const count = params.urls.length + + switch (state) { + case ClientToolCallState.success: + return count > 1 ? `Retrieved ${count} pages` : `Retrieved ${firstUrl}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return count > 1 ? `Getting ${count} pages` : `Getting ${firstUrl}` + case ClientToolCallState.error: + return count > 1 ? `Failed to get ${count} pages` : `Failed to get ${firstUrl}` + case ClientToolCallState.aborted: + return count > 1 ? `Aborted getting ${count} pages` : `Aborted getting ${firstUrl}` + case ClientToolCallState.rejected: + return count > 1 ? `Skipped getting ${count} pages` : `Skipped getting ${firstUrl}` + } + } + return undefined + }, + } + +const META_get_trigger_blocks: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Finding trigger blocks', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Found trigger blocks', icon: ListFilter }, + [ClientToolCallState.error]: { text: 'Failed to find trigger blocks', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted finding trigger blocks', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped finding trigger blocks', icon: MinusCircle }, + }, + interrupt: undefined, + } + +const META_get_trigger_examples: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Selecting a trigger', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Selected a trigger', icon: Zap }, + [ClientToolCallState.error]: { text: 'Failed to select a trigger', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted selecting a trigger', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped selecting a trigger', icon: MinusCircle }, + }, + interrupt: undefined, + } + +const META_get_user_workflow: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading your workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading your workflow', icon: WorkflowIcon }, + [ClientToolCallState.executing]: { text: 'Reading your workflow', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted reading your workflow', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Read your workflow', icon: WorkflowIcon }, + [ClientToolCallState.error]: { text: 'Failed to read your workflow', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped reading your workflow', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + if (workflowId) { + const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name + if (workflowName) { + switch (state) { + case ClientToolCallState.success: + return `Read ${workflowName}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Reading ${workflowName}` + case ClientToolCallState.error: + return `Failed to read ${workflowName}` + case ClientToolCallState.aborted: + return `Aborted reading ${workflowName}` + case ClientToolCallState.rejected: + return `Skipped reading ${workflowName}` + } + } + } + return undefined + }, + } + +const META_get_workflow_console: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching execution logs', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Fetching execution logs', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Fetched execution logs', icon: TerminalSquare }, + [ClientToolCallState.error]: { text: 'Failed to fetch execution logs', icon: XCircle }, + [ClientToolCallState.rejected]: { + text: 'Skipped fetching execution logs', + icon: MinusCircle, + }, + [ClientToolCallState.aborted]: { + text: 'Aborted fetching execution logs', + icon: MinusCircle, + }, + [ClientToolCallState.pending]: { text: 'Fetching execution logs', icon: Loader2 }, + }, + getDynamicText: (params, state) => { + const limit = params?.limit + if (limit && typeof limit === 'number') { + const logText = limit === 1 ? 'execution log' : 'execution logs' + + switch (state) { + case ClientToolCallState.success: + return `Fetched last ${limit} ${logText}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Fetching last ${limit} ${logText}` + case ClientToolCallState.error: + return `Failed to fetch last ${limit} ${logText}` + case ClientToolCallState.rejected: + return `Skipped fetching last ${limit} ${logText}` + case ClientToolCallState.aborted: + return `Aborted fetching last ${limit} ${logText}` + } + } + return undefined + }, + } + +const META_get_workflow_data: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Fetching workflow data', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Fetching workflow data', icon: Database }, + [ClientToolCallState.executing]: { text: 'Fetching workflow data', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted fetching data', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Retrieved workflow data', icon: Database }, + [ClientToolCallState.error]: { text: 'Failed to fetch data', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped fetching data', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const dataType = params?.data_type as WorkflowDataType | undefined + if (!dataType) return undefined + + const typeLabels: Record = { + global_variables: 'variables', + custom_tools: 'custom tools', + mcp_tools: 'MCP tools', + files: 'files', + } + + const label = typeLabels[dataType] || dataType + + switch (state) { + case ClientToolCallState.success: + return `Retrieved ${label}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + return `Fetching ${label}` + case ClientToolCallState.pending: + return `Fetch ${label}?` + case ClientToolCallState.error: + return `Failed to fetch ${label}` + case ClientToolCallState.aborted: + return `Aborted fetching ${label}` + case ClientToolCallState.rejected: + return `Skipped fetching ${label}` + } + return undefined + }, + } + +const META_get_workflow_from_name: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading workflow', icon: FileText }, + [ClientToolCallState.executing]: { text: 'Reading workflow', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted reading workflow', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Read workflow', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to read workflow', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped reading workflow', icon: XCircle }, + }, + getDynamicText: (params, state) => { + if (params?.workflow_name && typeof params.workflow_name === 'string') { + const workflowName = params.workflow_name + + switch (state) { + case ClientToolCallState.success: + return `Read ${workflowName}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Reading ${workflowName}` + case ClientToolCallState.error: + return `Failed to read ${workflowName}` + case ClientToolCallState.aborted: + return `Aborted reading ${workflowName}` + case ClientToolCallState.rejected: + return `Skipped reading ${workflowName}` + } + } + return undefined + }, + } + +const META_info: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting info', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved info', icon: Info }, + [ClientToolCallState.error]: { text: 'Failed to get info', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped info', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted info', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Getting info', + completedLabel: 'Info retrieved', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_knowledge: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing knowledge', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed knowledge', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to manage knowledge', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped knowledge', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted knowledge', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing knowledge', + completedLabel: 'Knowledge managed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_knowledge_base: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Accessing knowledge base', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Accessed knowledge base', icon: Database }, + [ClientToolCallState.error]: { text: 'Failed to access knowledge base', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted knowledge base access', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped knowledge base access', icon: MinusCircle }, + }, + getDynamicText: (params: Record, state: ClientToolCallState) => { + const operation = params?.operation as string | undefined + const name = params?.args?.name as string | undefined + + const opVerbs: Record = { + create: { + active: 'Creating knowledge base', + past: 'Created knowledge base', + pending: name ? `Create knowledge base "${name}"?` : 'Create knowledge base?', + }, + list: { active: 'Listing knowledge bases', past: 'Listed knowledge bases' }, + get: { active: 'Getting knowledge base', past: 'Retrieved knowledge base' }, + query: { active: 'Querying knowledge base', past: 'Queried knowledge base' }, + } + const defaultVerb: { active: string; past: string; pending?: string } = { + active: 'Accessing knowledge base', + past: 'Accessed knowledge base', + } + const verb = operation ? opVerbs[operation] || defaultVerb : defaultVerb + + if (state === ClientToolCallState.success) { + return verb.past + } + if (state === ClientToolCallState.pending && verb.pending) { + return verb.pending + } + if ( + state === ClientToolCallState.generating || + state === ClientToolCallState.pending || + state === ClientToolCallState.executing + ) { + return verb.active + } + return undefined + }, + } + +const META_list_user_workflows: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Listing your workflows', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Listing your workflows', icon: ListChecks }, + [ClientToolCallState.executing]: { text: 'Listing your workflows', icon: Loader2 }, + [ClientToolCallState.aborted]: { text: 'Aborted listing workflows', icon: XCircle }, + [ClientToolCallState.success]: { text: 'Listed your workflows', icon: ListChecks }, + [ClientToolCallState.error]: { text: 'Failed to list workflows', icon: X }, + [ClientToolCallState.rejected]: { text: 'Skipped listing workflows', icon: XCircle }, + }, + } + +const META_list_workspace_mcp_servers: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Getting MCP servers', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Getting MCP servers', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Getting MCP servers', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Retrieved MCP servers', icon: Server }, + [ClientToolCallState.error]: { text: 'Failed to get MCP servers', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted getting MCP servers', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped getting MCP servers', icon: XCircle }, + }, + interrupt: undefined, + } + +const META_make_api_request: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing API request', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Review API request', icon: Globe2 }, + [ClientToolCallState.executing]: { text: 'Executing API request', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed API request', icon: Globe2 }, + [ClientToolCallState.error]: { text: 'Failed to execute API request', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped API request', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted API request', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Execute', icon: Globe2 }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + uiConfig: { + interrupt: { + accept: { text: 'Execute', icon: Globe2 }, + reject: { text: 'Skip', icon: MinusCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + paramsTable: { + columns: [ + { key: 'method', label: 'Method', width: '26%', editable: true, mono: true }, + { key: 'url', label: 'Endpoint', width: '74%', editable: true, mono: true }, + ], + extractRows: (params) => { + return [['request', (params.method || 'GET').toUpperCase(), params.url || '']] + }, + }, + }, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const method = params.method || 'GET' + let url = params.url + + // Extract domain from URL for cleaner display + try { + const urlObj = new URL(url) + url = urlObj.hostname + urlObj.pathname + } catch { + // Use URL as-is if parsing fails + } + + switch (state) { + case ClientToolCallState.success: + return `${method} ${url} complete` + case ClientToolCallState.executing: + return `${method} ${url}` + case ClientToolCallState.generating: + return `Preparing ${method} ${url}` + case ClientToolCallState.pending: + return `Review ${method} ${url}` + case ClientToolCallState.error: + return `Failed ${method} ${url}` + case ClientToolCallState.rejected: + return `Skipped ${method} ${url}` + case ClientToolCallState.aborted: + return `Aborted ${method} ${url}` + } + } + return undefined + }, + } + +const META_manage_custom_tool: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Managing custom tool', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Manage custom tool?', icon: Plus }, + [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to manage custom tool', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted managing custom tool', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped managing custom tool', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Allow', icon: Check }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined + + if (!operation) return undefined + + let toolName = params?.schema?.function?.name + if (!toolName && params?.toolId) { + try { + const tool = getCustomTool(params.toolId) + toolName = tool?.schema?.function?.name + } catch { + // Ignore errors accessing cache + } + } + + const getActionText = (verb: 'present' | 'past' | 'gerund') => { + switch (operation) { + case 'add': + return verb === 'present' ? 'Create' : verb === 'past' ? 'Created' : 'Creating' + case 'edit': + return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' + case 'delete': + return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + case 'list': + return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' + default: + return verb === 'present' ? 'Manage' : verb === 'past' ? 'Managed' : 'Managing' + } + } + + // For add: only show tool name in past tense (success) + // For edit/delete: always show tool name + // For list: never show individual tool name, use plural + const shouldShowToolName = (currentState: ClientToolCallState) => { + if (operation === 'list') return false + if (operation === 'add') { + return currentState === ClientToolCallState.success + } + return true // edit and delete always show tool name + } + + const nameText = + operation === 'list' + ? ' custom tools' + : shouldShowToolName(state) && toolName + ? ` ${toolName}` + : ' custom tool' + + switch (state) { + case ClientToolCallState.success: + return `${getActionText('past')}${nameText}` + case ClientToolCallState.executing: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.generating: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.pending: + return `${getActionText('present')}${nameText}?` + case ClientToolCallState.error: + return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` + case ClientToolCallState.aborted: + return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` + case ClientToolCallState.rejected: + return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` + } + return undefined + }, + } + +const META_manage_mcp_tool: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Managing MCP tool', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Manage MCP tool?', icon: Server }, + [ClientToolCallState.executing]: { text: 'Managing MCP tool', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed MCP tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed to manage MCP tool', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted managing MCP tool', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped managing MCP tool', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Allow', icon: Check }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined + + if (!operation) return undefined + + const serverName = params?.config?.name || params?.serverName + + const getActionText = (verb: 'present' | 'past' | 'gerund') => { + switch (operation) { + case 'add': + return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' + case 'edit': + return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' + case 'delete': + return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + } + } + + const shouldShowServerName = (currentState: ClientToolCallState) => { + if (operation === 'add') { + return currentState === ClientToolCallState.success + } + return true + } + + const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' + + switch (state) { + case ClientToolCallState.success: + return `${getActionText('past')}${nameText}` + case ClientToolCallState.executing: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.generating: + return `${getActionText('gerund')}${nameText}` + case ClientToolCallState.pending: + return `${getActionText('present')}${nameText}?` + case ClientToolCallState.error: + return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` + case ClientToolCallState.aborted: + return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` + case ClientToolCallState.rejected: + return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` + } + return undefined + }, + } + +const META_mark_todo_in_progress: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Marking todo in progress', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Marked todo in progress', icon: Loader2 }, + [ClientToolCallState.error]: { text: 'Failed to mark in progress', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted marking in progress', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped marking in progress', icon: MinusCircle }, + }, + } + +const META_navigate_ui: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to open', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Open?', icon: Navigation }, + [ClientToolCallState.executing]: { text: 'Opening', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Opened', icon: Navigation }, + [ClientToolCallState.error]: { text: 'Failed to open', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted opening', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped opening', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Open', icon: Navigation }, + reject: { text: 'Skip', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const destination = params?.destination as NavigationDestination | undefined + const workflowName = params?.workflowName + + const action = 'open' + const actionCapitalized = 'Open' + const actionPast = 'opened' + const actionIng = 'opening' + let target = '' + + if (destination === 'workflow' && workflowName) { + target = ` workflow "${workflowName}"` + } else if (destination === 'workflow') { + target = ' workflows' + } else if (destination === 'logs') { + target = ' logs' + } else if (destination === 'templates') { + target = ' templates' + } else if (destination === 'vector_db') { + target = ' vector database' + } else if (destination === 'settings') { + target = ' settings' + } + + const fullAction = `${action}${target}` + const fullActionCapitalized = `${actionCapitalized}${target}` + const fullActionPast = `${actionPast}${target}` + const fullActionIng = `${actionIng}${target}` + + switch (state) { + case ClientToolCallState.success: + return fullActionPast.charAt(0).toUpperCase() + fullActionPast.slice(1) + case ClientToolCallState.executing: + return fullActionIng.charAt(0).toUpperCase() + fullActionIng.slice(1) + case ClientToolCallState.generating: + return `Preparing to ${fullAction}` + case ClientToolCallState.pending: + return `${fullActionCapitalized}?` + case ClientToolCallState.error: + return `Failed to ${fullAction}` + case ClientToolCallState.aborted: + return `Aborted ${fullAction}` + case ClientToolCallState.rejected: + return `Skipped ${fullAction}` + } + return undefined + }, + } + +const META_oauth_request_access: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Requesting integration access', icon: Loader2 }, + [ClientToolCallState.rejected]: { text: 'Skipped integration access', icon: MinusCircle }, + [ClientToolCallState.success]: { text: 'Requested integration access', icon: CheckCircle }, + [ClientToolCallState.error]: { text: 'Failed to request integration access', icon: X }, + [ClientToolCallState.aborted]: { text: 'Aborted integration access request', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Connect', icon: PlugZap }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + if (params.providerName) { + const name = params.providerName + switch (state) { + case ClientToolCallState.generating: + case ClientToolCallState.pending: + case ClientToolCallState.executing: + return `Requesting ${name} access` + case ClientToolCallState.rejected: + return `Skipped ${name} access` + case ClientToolCallState.success: + return `Requested ${name} access` + case ClientToolCallState.error: + return `Failed to request ${name} access` + case ClientToolCallState.aborted: + return `Aborted ${name} access request` + } + } + return undefined + }, + } + +const META_plan: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Planning', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Planned', icon: ListTodo }, + [ClientToolCallState.error]: { text: 'Failed to plan', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped plan', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted plan', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Planning', + completedLabel: 'Planned', + shouldCollapse: true, + outputArtifacts: ['plan'], + }, + }, + } + +const META_redeploy: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Redeploying workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Redeploy workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Redeploying workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Redeployed workflow', icon: Rocket }, + [ClientToolCallState.error]: { text: 'Failed to redeploy workflow', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted redeploy', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped redeploy', icon: XCircle }, + }, + interrupt: undefined, + } + +const META_remember_debug: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Validating fix', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Validated fix', icon: CheckCircle2 }, + [ClientToolCallState.error]: { text: 'Failed to validate', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted validation', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped validation', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + const operation = params?.operation + + if (operation === 'add' || operation === 'edit') { + // For add/edit, show from problem or solution + const text = params?.problem || params?.solution + if (text && typeof text === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Validated fix ${text}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Validating fix ${text}` + case ClientToolCallState.error: + return `Failed to validate fix ${text}` + case ClientToolCallState.aborted: + return `Aborted validating fix ${text}` + case ClientToolCallState.rejected: + return `Skipped validating fix ${text}` + } + } + } else if (operation === 'delete') { + // For delete, show from problem or solution (or id as fallback) + const text = params?.problem || params?.solution || params?.id + if (text && typeof text === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Adjusted fix ${text}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Adjusting fix ${text}` + case ClientToolCallState.error: + return `Failed to adjust fix ${text}` + case ClientToolCallState.aborted: + return `Aborted adjusting fix ${text}` + case ClientToolCallState.rejected: + return `Skipped adjusting fix ${text}` + } + } + } + + return undefined + }, + } + +const META_research: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Researching', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Researched', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to research', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped research', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted research', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Researching', + completedLabel: 'Researched', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_run_workflow: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Run this workflow?', icon: Play }, + [ClientToolCallState.executing]: { text: 'Running your workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Executed workflow', icon: Play }, + [ClientToolCallState.error]: { text: 'Errored running workflow', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped workflow execution', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted workflow execution', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Running in background', icon: Play }, + }, + interrupt: { + accept: { text: 'Run', icon: Play }, + reject: { text: 'Skip', icon: MinusCircle }, + }, + uiConfig: { + isSpecial: true, + interrupt: { + accept: { text: 'Run', icon: Play }, + reject: { text: 'Skip', icon: MinusCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + secondaryAction: { + text: 'Move to Background', + title: 'Move to Background', + variant: 'tertiary', + showInStates: [ClientToolCallState.executing], + completionMessage: + 'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete', + targetState: ClientToolCallState.background, + }, + paramsTable: { + columns: [ + { key: 'input', label: 'Input', width: '36%' }, + { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, + ], + extractRows: (params) => { + let inputs = params.input || params.inputs || params.workflow_input + if (typeof inputs === 'string') { + try { + inputs = JSON.parse(inputs) + } catch { + inputs = {} + } + } + if (params.workflow_input && typeof params.workflow_input === 'object') { + inputs = params.workflow_input + } + if (!inputs || typeof inputs !== 'object') { + const { workflowId, workflow_input, ...rest } = params + inputs = rest + } + const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} + return Object.entries(safeInputs).map(([key, value]) => [key, key, String(value)]) + }, + }, + }, + getDynamicText: (params, state) => { + const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId + if (workflowId) { + const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name + if (workflowName) { + switch (state) { + case ClientToolCallState.success: + return `Ran ${workflowName}` + case ClientToolCallState.executing: + return `Running ${workflowName}` + case ClientToolCallState.generating: + return `Preparing to run ${workflowName}` + case ClientToolCallState.pending: + return `Run ${workflowName}?` + case ClientToolCallState.error: + return `Failed to run ${workflowName}` + case ClientToolCallState.rejected: + return `Skipped running ${workflowName}` + case ClientToolCallState.aborted: + return `Aborted running ${workflowName}` + case ClientToolCallState.background: + return `Running ${workflowName} in background` + } + } + } + return undefined + }, + } + +const META_scrape_page: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Scraping page', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Scraped page', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to scrape page', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted scraping page', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped scraping page', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.url && typeof params.url === 'string') { + const url = params.url + + switch (state) { + case ClientToolCallState.success: + return `Scraped ${url}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Scraping ${url}` + case ClientToolCallState.error: + return `Failed to scrape ${url}` + case ClientToolCallState.aborted: + return `Aborted scraping ${url}` + case ClientToolCallState.rejected: + return `Skipped scraping ${url}` + } + } + return undefined + }, + } + +const META_search_documentation: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching documentation', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed documentation search', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to search docs', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted documentation search', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped documentation search', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Searched docs for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching docs for ${query}` + case ClientToolCallState.error: + return `Failed to search docs for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching docs for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching docs for ${query}` + } + } + return undefined + }, + } + +const META_search_errors: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Debugging', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Debugged', icon: Bug }, + [ClientToolCallState.error]: { text: 'Failed to debug', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted debugging', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped debugging', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Debugged ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Debugging ${query}` + case ClientToolCallState.error: + return `Failed to debug ${query}` + case ClientToolCallState.aborted: + return `Aborted debugging ${query}` + case ClientToolCallState.rejected: + return `Skipped debugging ${query}` + } + } + return undefined + }, + } + +const META_search_library_docs: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Reading docs', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Read docs', icon: BookOpen }, + [ClientToolCallState.error]: { text: 'Failed to read docs', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted reading docs', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped reading docs', icon: MinusCircle }, + }, + getDynamicText: (params, state) => { + const libraryName = params?.library_name + if (libraryName && typeof libraryName === 'string') { + switch (state) { + case ClientToolCallState.success: + return `Read ${libraryName} docs` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Reading ${libraryName} docs` + case ClientToolCallState.error: + return `Failed to read ${libraryName} docs` + case ClientToolCallState.aborted: + return `Aborted reading ${libraryName} docs` + case ClientToolCallState.rejected: + return `Skipped reading ${libraryName} docs` + } + } + return undefined + }, + } + +const META_search_online: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching online', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed online search', icon: Globe }, + [ClientToolCallState.error]: { text: 'Failed to search online', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped online search', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted online search', icon: XCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.query && typeof params.query === 'string') { + const query = params.query + + switch (state) { + case ClientToolCallState.success: + return `Searched online for ${query}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching online for ${query}` + case ClientToolCallState.error: + return `Failed to search online for ${query}` + case ClientToolCallState.aborted: + return `Aborted searching online for ${query}` + case ClientToolCallState.rejected: + return `Skipped searching online for ${query}` + } + } + return undefined + }, + } + +const META_search_patterns: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Searching workflow patterns', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Found workflow patterns', icon: Search }, + [ClientToolCallState.error]: { text: 'Failed to search patterns', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted pattern search', icon: MinusCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped pattern search', icon: MinusCircle }, + }, + interrupt: undefined, + getDynamicText: (params, state) => { + if (params?.queries && Array.isArray(params.queries) && params.queries.length > 0) { + const firstQuery = String(params.queries[0]) + + switch (state) { + case ClientToolCallState.success: + return `Searched ${firstQuery}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + case ClientToolCallState.pending: + return `Searching ${firstQuery}` + case ClientToolCallState.error: + return `Failed to search ${firstQuery}` + case ClientToolCallState.aborted: + return `Aborted searching ${firstQuery}` + case ClientToolCallState.rejected: + return `Skipped searching ${firstQuery}` + } + } + return undefined + }, + } + +const META_set_environment_variables: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to set environment variables', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Set environment variables?', icon: Settings2 }, + [ClientToolCallState.executing]: { text: 'Setting environment variables', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Set environment variables', icon: Settings2 }, + [ClientToolCallState.error]: { text: 'Failed to set environment variables', icon: X }, + [ClientToolCallState.aborted]: { + text: 'Aborted setting environment variables', + icon: XCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped setting environment variables', + icon: XCircle, + }, + }, + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + alwaysExpanded: true, + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + paramsTable: { + columns: [ + { key: 'name', label: 'Variable', width: '36%', editable: true }, + { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, + ], + extractRows: (params) => { + const variables = params.variables || {} + const entries = Array.isArray(variables) + ? variables.map((v: any, i: number) => [String(i), v.name || `var_${i}`, v.value || '']) + : Object.entries(variables).map(([key, val]) => { + if (typeof val === 'object' && val !== null && 'value' in (val as any)) { + return [key, key, (val as any).value] + } + return [key, key, val] + }) + return entries as Array<[string, ...any[]]> + }, + }, + }, + getDynamicText: (params, state) => { + if (params?.variables && typeof params.variables === 'object') { + const count = Object.keys(params.variables).length + const varText = count === 1 ? 'variable' : 'variables' + + switch (state) { + case ClientToolCallState.success: + return `Set ${count} ${varText}` + case ClientToolCallState.executing: + return `Setting ${count} ${varText}` + case ClientToolCallState.generating: + return `Preparing to set ${count} ${varText}` + case ClientToolCallState.pending: + return `Set ${count} ${varText}?` + case ClientToolCallState.error: + return `Failed to set ${count} ${varText}` + case ClientToolCallState.aborted: + return `Aborted setting ${count} ${varText}` + case ClientToolCallState.rejected: + return `Skipped setting ${count} ${varText}` + } + } + return undefined + }, + } + +const META_set_global_workflow_variables: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { + text: 'Preparing to set workflow variables', + icon: Loader2, + }, + [ClientToolCallState.pending]: { text: 'Set workflow variables?', icon: Settings2 }, + [ClientToolCallState.executing]: { text: 'Setting workflow variables', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Updated workflow variables', icon: Settings2 }, + [ClientToolCallState.error]: { text: 'Failed to set workflow variables', icon: X }, + [ClientToolCallState.aborted]: { text: 'Aborted setting variables', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped setting variables', icon: XCircle }, + }, + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + }, + uiConfig: { + interrupt: { + accept: { text: 'Apply', icon: Settings2 }, + reject: { text: 'Skip', icon: XCircle }, + showAllowOnce: true, + showAllowAlways: true, + }, + paramsTable: { + columns: [ + { key: 'name', label: 'Name', width: '40%', editable: true, mono: true }, + { key: 'value', label: 'Value', width: '60%', editable: true, mono: true }, + ], + extractRows: (params) => { + const operations = params.operations || [] + return operations.map((op: any, idx: number) => [ + String(idx), + op.name || '', + String(op.value ?? ''), + ]) + }, + }, + }, + getDynamicText: (params, state) => { + if (params?.operations && Array.isArray(params.operations)) { + const varNames = params.operations + .slice(0, 2) + .map((op: any) => op.name) + .filter(Boolean) + + if (varNames.length > 0) { + const varList = varNames.join(', ') + const more = params.operations.length > 2 ? '...' : '' + const displayText = `${varList}${more}` + + switch (state) { + case ClientToolCallState.success: + return `Set ${displayText}` + case ClientToolCallState.executing: + return `Setting ${displayText}` + case ClientToolCallState.generating: + return `Preparing to set ${displayText}` + case ClientToolCallState.pending: + return `Set ${displayText}?` + case ClientToolCallState.error: + return `Failed to set ${displayText}` + case ClientToolCallState.aborted: + return `Aborted setting ${displayText}` + case ClientToolCallState.rejected: + return `Skipped setting ${displayText}` + } + } + } + return undefined + }, + } + +const META_sleep: ToolMetadata = { + 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: 'Interrupted sleep', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped sleep', icon: MinusCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted sleep', icon: MinusCircle }, + [ClientToolCallState.background]: { text: 'Resumed', icon: Moon }, + }, + uiConfig: { + secondaryAction: { + text: 'Wake', + title: 'Wake', + variant: 'tertiary', + showInStates: [ClientToolCallState.executing], + targetState: ClientToolCallState.background, + }, + }, + // 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 + }, + } + +const META_summarize_conversation: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Summarizing conversation', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Summarized conversation', icon: PencilLine }, + [ClientToolCallState.error]: { text: 'Failed to summarize conversation', icon: XCircle }, + [ClientToolCallState.aborted]: { + text: 'Aborted summarizing conversation', + icon: MinusCircle, + }, + [ClientToolCallState.rejected]: { + text: 'Skipped summarizing conversation', + icon: MinusCircle, + }, + }, + interrupt: undefined, + } + +const META_superagent: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Superagent working', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Superagent completed', icon: Sparkles }, + [ClientToolCallState.error]: { text: 'Superagent failed', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Superagent skipped', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Superagent aborted', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Superagent working', + completedLabel: 'Superagent completed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_test: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Testing', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Tested', icon: FlaskConical }, + [ClientToolCallState.error]: { text: 'Failed to test', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped test', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted test', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Testing', + completedLabel: 'Tested', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_tour: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Touring', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed tour', icon: Compass }, + [ClientToolCallState.error]: { text: 'Failed tour', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped tour', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted tour', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Touring', + completedLabel: 'Tour complete', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const META_workflow: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Managed workflow', icon: GitBranch }, + [ClientToolCallState.error]: { text: 'Failed to manage workflow', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped workflow', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted workflow', icon: XCircle }, + }, + uiConfig: { + subagent: { + streamingLabel: 'Managing workflow', + completedLabel: 'Workflow managed', + shouldCollapse: true, + outputArtifacts: [], + }, + }, + } + +const TOOL_METADATA_BY_ID: Record = { + 'auth': META_auth, + 'check_deployment_status': META_check_deployment_status, + 'checkoff_todo': META_checkoff_todo, + 'crawl_website': META_crawl_website, + 'create_workspace_mcp_server': META_create_workspace_mcp_server, + 'custom_tool': META_custom_tool, + 'debug': META_debug, + 'deploy': META_deploy, + 'deploy_api': META_deploy_api, + 'deploy_chat': META_deploy_chat, + 'deploy_mcp': META_deploy_mcp, + 'edit': META_edit, + 'edit_workflow': META_edit_workflow, + 'evaluate': META_evaluate, + 'get_block_config': META_get_block_config, + 'get_block_options': META_get_block_options, + 'get_block_outputs': META_get_block_outputs, + 'get_block_upstream_references': META_get_block_upstream_references, + 'get_blocks_and_tools': META_get_blocks_and_tools, + 'get_blocks_metadata': META_get_blocks_metadata, + 'get_credentials': META_get_credentials, + 'get_examples_rag': META_get_examples_rag, + 'get_operations_examples': META_get_operations_examples, + 'get_page_contents': META_get_page_contents, + 'get_trigger_blocks': META_get_trigger_blocks, + 'get_trigger_examples': META_get_trigger_examples, + 'get_user_workflow': META_get_user_workflow, + 'get_workflow_console': META_get_workflow_console, + 'get_workflow_data': META_get_workflow_data, + 'get_workflow_from_name': META_get_workflow_from_name, + 'info': META_info, + 'knowledge': META_knowledge, + 'knowledge_base': META_knowledge_base, + 'list_user_workflows': META_list_user_workflows, + 'list_workspace_mcp_servers': META_list_workspace_mcp_servers, + 'make_api_request': META_make_api_request, + 'manage_custom_tool': META_manage_custom_tool, + 'manage_mcp_tool': META_manage_mcp_tool, + 'mark_todo_in_progress': META_mark_todo_in_progress, + 'navigate_ui': META_navigate_ui, + 'oauth_request_access': META_oauth_request_access, + 'plan': META_plan, + 'redeploy': META_redeploy, + 'remember_debug': META_remember_debug, + 'research': META_research, + 'run_workflow': META_run_workflow, + 'scrape_page': META_scrape_page, + 'search_documentation': META_search_documentation, + 'search_errors': META_search_errors, + 'search_library_docs': META_search_library_docs, + 'search_online': META_search_online, + 'search_patterns': META_search_patterns, + 'set_environment_variables': META_set_environment_variables, + 'set_global_workflow_variables': META_set_global_workflow_variables, + 'sleep': META_sleep, + 'summarize_conversation': META_summarize_conversation, + 'superagent': META_superagent, + 'test': META_test, + 'tour': META_tour, + 'workflow': META_workflow, +} + +export const TOOL_DISPLAY_REGISTRY: Record = Object.fromEntries( + Object.entries(TOOL_METADATA_BY_ID).map(([toolName, metadata]) => [ + toolName, + toToolDisplayEntry(metadata), + ]) +) diff --git a/apps/sim/lib/copilot/tools/client/types.ts b/apps/sim/lib/copilot/tools/client/types.ts deleted file mode 100644 index 0f8ded86d..000000000 --- a/apps/sim/lib/copilot/tools/client/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { BaseClientToolMetadata } from '@/lib/copilot/tools/client/base-tool' -import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' - -export interface ToolExecutionContext { - toolCallId: string - toolName: string - // Logging only; tools must not mutate store state directly - log: ( - level: 'debug' | 'info' | 'warn' | 'error', - message: string, - extra?: Record - ) => void -} - -export interface ToolRunResult { - status: number - message?: any - data?: any -} - -export interface ClientToolDefinition { - name: string - metadata?: BaseClientToolMetadata - // Return true if this tool requires user confirmation before execution - hasInterrupt?: boolean | ((args?: Args) => boolean) - // Main execution entry point. Returns a result for the store to handle. - execute: (ctx: ToolExecutionContext, args?: Args) => Promise - // Optional accept/reject handlers for interrupt flows - accept?: (ctx: ToolExecutionContext, args?: Args) => Promise - reject?: (ctx: ToolExecutionContext, args?: Args) => Promise -} - -export { ClientToolCallState } diff --git a/apps/sim/lib/copilot/tools/client/ui-config.ts b/apps/sim/lib/copilot/tools/client/ui-config.ts deleted file mode 100644 index 6fac1645c..000000000 --- a/apps/sim/lib/copilot/tools/client/ui-config.ts +++ /dev/null @@ -1,238 +0,0 @@ -/** - * UI Configuration Types for Copilot Tools - * - * This module defines the configuration interfaces that control how tools - * are rendered in the tool-call component. All UI behavior should be defined - * here rather than hardcoded in the rendering component. - */ -import type { LucideIcon } from 'lucide-react' -import type { ClientToolCallState } from './base-tool' - -/** - * Configuration for a params table column - */ -export interface ParamsTableColumn { - /** Key to extract from params */ - key: string - /** Display label for the column header */ - label: string - /** Width as percentage or CSS value */ - width?: string - /** Whether values in this column are editable */ - editable?: boolean - /** Whether to use monospace font */ - mono?: boolean - /** Whether to mask the value (for passwords) */ - masked?: boolean -} - -/** - * Configuration for params table rendering - */ -export interface ParamsTableConfig { - /** Column definitions */ - columns: ParamsTableColumn[] - /** - * Extract rows from tool params. - * Returns array of [key, ...cellValues] for each row. - */ - extractRows: (params: Record) => Array<[string, ...any[]]> - /** - * Optional: Update params when a cell is edited. - * Returns the updated params object. - */ - updateCell?: ( - params: Record, - rowKey: string, - columnKey: string, - newValue: any - ) => Record -} - -/** - * Configuration for secondary action button (like "Move to Background") - */ -export interface SecondaryActionConfig { - /** Button text */ - text: string - /** Button title/tooltip */ - title?: string - /** Button variant */ - variant?: 'tertiary' | 'default' | 'outline' - /** States in which to show this button */ - showInStates: ClientToolCallState[] - /** - * Message to send when the action is triggered. - * Used by markToolComplete. - */ - completionMessage?: string - /** - * Target state after action. - * If not provided, defaults to 'background'. - */ - targetState?: ClientToolCallState -} - -/** - * Configuration for subagent tools (tools that spawn subagents) - */ -export interface SubagentConfig { - /** Label shown while streaming (e.g., "Planning", "Editing") */ - streamingLabel: string - /** Label shown when complete (e.g., "Planned", "Edited") */ - completedLabel: string - /** - * Whether the content should collapse when streaming ends. - * Default: true - */ - shouldCollapse?: boolean - /** - * Output artifacts that should NOT be collapsed. - * These are rendered outside the collapsible content. - * Examples: 'plan' for PlanSteps, 'options' for OptionsSelector - */ - outputArtifacts?: Array<'plan' | 'options' | 'edit_summary'> - /** - * Whether this subagent renders its own specialized content - * and the thinking text should be minimal or hidden. - * Used for tools like 'edit' where we show WorkflowEditSummary instead. - */ - hideThinkingText?: boolean -} - -/** - * Interrupt button configuration - */ -export interface InterruptButtonConfig { - text: string - icon: LucideIcon -} - -/** - * Configuration for interrupt behavior (Run/Skip buttons) - */ -export interface InterruptConfig { - /** Accept button config */ - accept: InterruptButtonConfig - /** Reject button config */ - reject: InterruptButtonConfig - /** - * Whether to show "Allow Once" button (default accept behavior). - * Default: true - */ - showAllowOnce?: boolean - /** - * Whether to show "Allow Always" button (auto-approve this tool in future). - * Default: true for most tools - */ - showAllowAlways?: boolean -} - -/** - * Complete UI configuration for a tool - */ -export interface ToolUIConfig { - /** - * Whether this is a "special" tool that gets gradient styling. - * Used for workflow operation tools like edit_workflow, build_workflow, etc. - */ - isSpecial?: boolean - - /** - * Interrupt configuration for tools that require user confirmation. - * If not provided, tool auto-executes. - */ - interrupt?: InterruptConfig - - /** - * Secondary action button (like "Move to Background" for run_workflow) - */ - secondaryAction?: SecondaryActionConfig - - /** - * Configuration for rendering params as a table. - * If provided, tool will show an expandable/inline table. - */ - paramsTable?: ParamsTableConfig - - /** - * Subagent configuration for tools that spawn subagents. - * If provided, tool is treated as a subagent tool. - */ - subagent?: SubagentConfig - - /** - * Whether this tool should always show params expanded (not collapsible). - * Used for tools like set_environment_variables that always show their table. - */ - alwaysExpanded?: boolean - - /** - * Custom component type for special rendering. - * The tool-call component will use this to render specialized content. - */ - customRenderer?: 'code' | 'edit_summary' | 'none' -} - -/** - * Registry of tool UI configurations. - * Tools can register their UI config here for the tool-call component to use. - */ -const toolUIConfigs: Record = {} - -/** - * Register a tool's UI configuration - */ -export function registerToolUIConfig(toolName: string, config: ToolUIConfig): void { - toolUIConfigs[toolName] = config -} - -/** - * Get a tool's UI configuration - */ -export function getToolUIConfig(toolName: string): ToolUIConfig | undefined { - return toolUIConfigs[toolName] -} - -/** - * Check if a tool is a subagent tool - */ -export function isSubagentTool(toolName: string): boolean { - return !!toolUIConfigs[toolName]?.subagent -} - -/** - * Check if a tool is a "special" tool (gets gradient styling) - */ -export function isSpecialTool(toolName: string): boolean { - return !!toolUIConfigs[toolName]?.isSpecial -} - -/** - * Check if a tool has interrupt (requires user confirmation) - */ -export function hasInterrupt(toolName: string): boolean { - return !!toolUIConfigs[toolName]?.interrupt -} - -/** - * Get subagent labels for a tool - */ -export function getSubagentLabels( - toolName: string, - isStreaming: boolean -): { streaming: string; completed: string } | undefined { - const config = toolUIConfigs[toolName]?.subagent - if (!config) return undefined - return { - streaming: config.streamingLabel, - completed: config.completedLabel, - } -} - -/** - * Get all registered tool UI configs (for debugging) - */ -export function getAllToolUIConfigs(): Record { - return { ...toolUIConfigs } -} diff --git a/apps/sim/lib/copilot/tools/client/user/get-credentials.ts b/apps/sim/lib/copilot/tools/client/user/get-credentials.ts deleted file mode 100644 index 0623693c4..000000000 --- a/apps/sim/lib/copilot/tools/client/user/get-credentials.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Key, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetCredentialsClientTool extends BaseClientTool { - static readonly id = 'get_credentials' - - constructor(toolCallId: string) { - super(toolCallId, GetCredentialsClientTool.id, GetCredentialsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching connected integrations', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched connected integrations', icon: Key }, - [ClientToolCallState.error]: { - text: 'Failed to fetch connected integrations', - icon: XCircle, - }, - [ClientToolCallState.aborted]: { - text: 'Aborted fetching connected integrations', - icon: MinusCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped fetching connected integrations', - icon: MinusCircle, - }, - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts b/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts deleted file mode 100644 index 415987c8e..000000000 --- a/apps/sim/lib/copilot/tools/client/user/set-environment-variables.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Settings2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { ExecuteResponseSuccessSchema } from '@/lib/copilot/tools/shared/schemas' -import { useEnvironmentStore } from '@/stores/settings/environment' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface SetEnvArgs { - variables: Record - workflowId?: string -} - -export class SetEnvironmentVariablesClientTool extends BaseClientTool { - static readonly id = 'set_environment_variables' - - constructor(toolCallId: string) { - super( - toolCallId, - SetEnvironmentVariablesClientTool.id, - SetEnvironmentVariablesClientTool.metadata - ) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to set environment variables', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Set environment variables?', icon: Settings2 }, - [ClientToolCallState.executing]: { text: 'Setting environment variables', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Set environment variables', icon: Settings2 }, - [ClientToolCallState.error]: { text: 'Failed to set environment variables', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted setting environment variables', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped setting environment variables', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - }, - uiConfig: { - alwaysExpanded: true, - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'name', label: 'Variable', width: '36%', editable: true }, - { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, - ], - extractRows: (params) => { - const variables = params.variables || {} - const entries = Array.isArray(variables) - ? variables.map((v: any, i: number) => [String(i), v.name || `var_${i}`, v.value || '']) - : Object.entries(variables).map(([key, val]) => { - if (typeof val === 'object' && val !== null && 'value' in (val as any)) { - return [key, key, (val as any).value] - } - return [key, key, val] - }) - return entries as Array<[string, ...any[]]> - }, - }, - }, - getDynamicText: (params, state) => { - if (params?.variables && typeof params.variables === 'object') { - const count = Object.keys(params.variables).length - const varText = count === 1 ? 'variable' : 'variables' - - switch (state) { - case ClientToolCallState.success: - return `Set ${count} ${varText}` - case ClientToolCallState.executing: - return `Setting ${count} ${varText}` - case ClientToolCallState.generating: - return `Preparing to set ${count} ${varText}` - case ClientToolCallState.pending: - return `Set ${count} ${varText}?` - case ClientToolCallState.error: - return `Failed to set ${count} ${varText}` - case ClientToolCallState.aborted: - return `Aborted setting ${count} ${varText}` - case ClientToolCallState.rejected: - return `Skipped setting ${count} ${varText}` - } - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(_args?: SetEnvArgs): Promise { - // Tool execution is handled server-side by the orchestrator. - this.setState(ClientToolCallState.executing) - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} - -// Register UI config at module load -registerToolUIConfig( - SetEnvironmentVariablesClientTool.id, - SetEnvironmentVariablesClientTool.metadata.uiConfig! -) diff --git a/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts b/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts deleted file mode 100644 index 4916cb770..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/block-output-utils.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { - extractFieldsFromSchema, - parseResponseFormatSafely, -} from '@/lib/core/utils/response-format' -import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs' -import { getBlock } from '@/blocks' -import { normalizeName } from '@/executor/constants' -import { useVariablesStore } from '@/stores/panel/variables/store' -import type { Variable } from '@/stores/panel/variables/types' -import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types' - -export interface WorkflowContext { - workflowId: string - blocks: Record - loops: Record - parallels: Record - subBlockValues: Record> -} - -export interface VariableOutput { - id: string - name: string - type: string - tag: string -} - -export function getWorkflowSubBlockValues(workflowId: string): Record> { - const subBlockStore = useSubBlockStore.getState() - return subBlockStore.workflowValues[workflowId] ?? {} -} - -export function getMergedSubBlocks( - blocks: Record, - subBlockValues: Record>, - targetBlockId: string -): Record { - const base = blocks[targetBlockId]?.subBlocks || {} - const live = subBlockValues?.[targetBlockId] || {} - const merged: Record = { ...base } - for (const [subId, liveVal] of Object.entries(live)) { - merged[subId] = { ...(base[subId] || {}), value: liveVal } - } - return merged -} - -export function getSubBlockValue( - blocks: Record, - subBlockValues: Record>, - targetBlockId: string, - subBlockId: string -): any { - const live = subBlockValues?.[targetBlockId]?.[subBlockId] - if (live !== undefined) return live - return blocks[targetBlockId]?.subBlocks?.[subBlockId]?.value -} - -export function getWorkflowVariables(workflowId: string): VariableOutput[] { - const getVariablesByWorkflowId = useVariablesStore.getState().getVariablesByWorkflowId - const workflowVariables = getVariablesByWorkflowId(workflowId) - const validVariables = workflowVariables.filter( - (variable: Variable) => variable.name.trim() !== '' - ) - return validVariables.map((variable: Variable) => ({ - id: variable.id, - name: variable.name, - type: variable.type, - tag: `variable.${normalizeName(variable.name)}`, - })) -} - -export function getSubflowInsidePaths( - blockType: 'loop' | 'parallel', - blockId: string, - loops: Record, - parallels: Record -): string[] { - const paths = ['index'] - if (blockType === 'loop') { - const loopType = loops[blockId]?.loopType || 'for' - if (loopType === 'forEach') { - paths.push('currentItem', 'items') - } - } else { - const parallelType = parallels[blockId]?.parallelType || 'count' - if (parallelType === 'collection') { - paths.push('currentItem', 'items') - } - } - return paths -} - -export function computeBlockOutputPaths(block: BlockState, ctx: WorkflowContext): string[] { - const { blocks, loops, parallels, subBlockValues } = ctx - const blockConfig = getBlock(block.type) - const mergedSubBlocks = getMergedSubBlocks(blocks, subBlockValues, block.id) - - if (block.type === 'loop' || block.type === 'parallel') { - const insidePaths = getSubflowInsidePaths(block.type, block.id, loops, parallels) - return ['results', ...insidePaths] - } - - if (block.type === 'evaluator') { - const metricsValue = getSubBlockValue(blocks, subBlockValues, block.id, 'metrics') - if (metricsValue && Array.isArray(metricsValue) && metricsValue.length > 0) { - const validMetrics = metricsValue.filter((metric: { name?: string }) => metric?.name) - return validMetrics.map((metric: { name: string }) => metric.name.toLowerCase()) - } - return getBlockOutputPaths(block.type, mergedSubBlocks) - } - - if (block.type === 'variables') { - const variablesValue = getSubBlockValue(blocks, subBlockValues, block.id, 'variables') - if (variablesValue && Array.isArray(variablesValue) && variablesValue.length > 0) { - const validAssignments = variablesValue.filter((assignment: { variableName?: string }) => - assignment?.variableName?.trim() - ) - return validAssignments.map((assignment: { variableName: string }) => - assignment.variableName.trim() - ) - } - return [] - } - - if (blockConfig) { - const responseFormatValue = mergedSubBlocks?.responseFormat?.value - const responseFormat = parseResponseFormatSafely(responseFormatValue, block.id) - if (responseFormat) { - const schemaFields = extractFieldsFromSchema(responseFormat) - if (schemaFields.length > 0) { - return schemaFields.map((field) => field.name) - } - } - } - - return getBlockOutputPaths(block.type, mergedSubBlocks, block.triggerMode) -} - -export function formatOutputsWithPrefix(paths: string[], blockName: string): string[] { - const normalizedName = normalizeName(blockName) - return paths.map((path) => `${normalizedName}.${path}`) -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/check-deployment-status.ts b/apps/sim/lib/copilot/tools/client/workflow/check-deployment-status.ts deleted file mode 100644 index a0d3de72e..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/check-deployment-status.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Rocket, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface CheckDeploymentStatusArgs { - workflowId?: string -} - -interface ApiDeploymentDetails { - isDeployed: boolean - deployedAt: string | null - endpoint: string | null - apiKey: string | null - needsRedeployment: boolean -} - -interface ChatDeploymentDetails { - isDeployed: boolean - chatId: string | null - identifier: string | null - chatUrl: string | null - title: string | null - description: string | null - authType: string | null - allowedEmails: string[] | null - outputConfigs: Array<{ blockId: string; path: string }> | null - welcomeMessage: string | null - primaryColor: string | null - hasPassword: boolean -} - -interface McpDeploymentDetails { - isDeployed: boolean - servers: Array<{ - serverId: string - serverName: string - toolName: string - toolDescription: string | null - parameterSchema?: Record | null - toolId?: string | null - }> -} - -export class CheckDeploymentStatusClientTool extends BaseClientTool { - static readonly id = 'check_deployment_status' - - constructor(toolCallId: string) { - super(toolCallId, CheckDeploymentStatusClientTool.id, CheckDeploymentStatusClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Checking deployment status', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Checking deployment status', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Checking deployment status', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Checked deployment status', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to check deployment status', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted checking deployment status', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped checking deployment status', - icon: XCircle, - }, - }, - interrupt: undefined, - } - - async execute(args?: CheckDeploymentStatusArgs): Promise { - const logger = createLogger('CheckDeploymentStatusClientTool') - try { - this.setState(ClientToolCallState.executing) - - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - const workflowId = args?.workflowId || activeWorkflowId - - if (!workflowId) { - throw new Error('No workflow ID provided') - } - - const workflow = workflows[workflowId] - const workspaceId = workflow?.workspaceId - - // Fetch deployment status from all sources - const [apiDeployRes, chatDeployRes, mcpServersRes] = await Promise.all([ - fetch(`/api/workflows/${workflowId}/deploy`), - fetch(`/api/workflows/${workflowId}/chat/status`), - workspaceId ? fetch(`/api/mcp/workflow-servers?workspaceId=${workspaceId}`) : null, - ]) - - const apiDeploy = apiDeployRes.ok ? await apiDeployRes.json() : null - const chatDeploy = chatDeployRes.ok ? await chatDeployRes.json() : null - const mcpServers = mcpServersRes?.ok ? await mcpServersRes.json() : null - - // API deployment details - const isApiDeployed = apiDeploy?.isDeployed || false - const appUrl = typeof window !== 'undefined' ? window.location.origin : '' - const apiDetails: ApiDeploymentDetails = { - isDeployed: isApiDeployed, - deployedAt: apiDeploy?.deployedAt || null, - endpoint: isApiDeployed ? `${appUrl}/api/workflows/${workflowId}/execute` : null, - apiKey: apiDeploy?.apiKey || null, - needsRedeployment: apiDeploy?.needsRedeployment === true, - } - - // Chat deployment details - const isChatDeployed = !!(chatDeploy?.isDeployed && chatDeploy?.deployment) - const chatDetails: ChatDeploymentDetails = { - isDeployed: isChatDeployed, - chatId: chatDeploy?.deployment?.id || null, - identifier: chatDeploy?.deployment?.identifier || null, - chatUrl: isChatDeployed ? `${appUrl}/chat/${chatDeploy?.deployment?.identifier}` : null, - title: chatDeploy?.deployment?.title || null, - description: chatDeploy?.deployment?.description || null, - authType: chatDeploy?.deployment?.authType || null, - allowedEmails: Array.isArray(chatDeploy?.deployment?.allowedEmails) - ? chatDeploy?.deployment?.allowedEmails - : null, - outputConfigs: Array.isArray(chatDeploy?.deployment?.outputConfigs) - ? chatDeploy?.deployment?.outputConfigs - : null, - welcomeMessage: chatDeploy?.deployment?.customizations?.welcomeMessage || null, - primaryColor: chatDeploy?.deployment?.customizations?.primaryColor || null, - hasPassword: chatDeploy?.deployment?.hasPassword === true, - } - - // MCP deployment details - find servers that have this workflow as a tool - const mcpServerList = mcpServers?.data?.servers || [] - const mcpToolDeployments: McpDeploymentDetails['servers'] = [] - - for (const server of mcpServerList) { - // Check if this workflow is deployed as a tool on this server - if (server.toolNames && Array.isArray(server.toolNames)) { - // We need to fetch the actual tools to check if this workflow is there - try { - const toolsRes = await fetch( - `/api/mcp/workflow-servers/${server.id}/tools?workspaceId=${workspaceId}` - ) - if (toolsRes.ok) { - const toolsData = await toolsRes.json() - const tools = toolsData.data?.tools || [] - for (const tool of tools) { - if (tool.workflowId === workflowId) { - mcpToolDeployments.push({ - serverId: server.id, - serverName: server.name, - toolName: tool.toolName, - toolDescription: tool.toolDescription, - parameterSchema: tool.parameterSchema ?? null, - toolId: tool.id ?? null, - }) - } - } - } - } catch { - // Skip this server if we can't fetch tools - } - } - } - - const isMcpDeployed = mcpToolDeployments.length > 0 - const mcpDetails: McpDeploymentDetails = { - isDeployed: isMcpDeployed, - servers: mcpToolDeployments, - } - - // Build deployment types list - const deploymentTypes: string[] = [] - if (isApiDeployed) deploymentTypes.push('api') - if (isChatDeployed) deploymentTypes.push('chat') - if (isMcpDeployed) deploymentTypes.push('mcp') - - const isDeployed = isApiDeployed || isChatDeployed || isMcpDeployed - - // Build summary message - let message = '' - if (!isDeployed) { - message = 'Workflow is not deployed' - } else { - const parts: string[] = [] - if (isApiDeployed) parts.push('API') - if (isChatDeployed) parts.push(`Chat (${chatDetails.identifier})`) - if (isMcpDeployed) { - const serverNames = mcpToolDeployments.map((d) => d.serverName).join(', ') - parts.push(`MCP (${serverNames})`) - } - message = `Workflow is deployed as: ${parts.join(', ')}` - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, message, { - isDeployed, - deploymentTypes, - api: apiDetails, - chat: chatDetails, - mcp: mcpDetails, - }) - - logger.info('Checked deployment status', { isDeployed, deploymentTypes }) - } catch (e: any) { - logger.error('Check deployment status failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to check deployment status') - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/create-workspace-mcp-server.ts b/apps/sim/lib/copilot/tools/client/workflow/create-workspace-mcp-server.ts deleted file mode 100644 index f50832184..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/create-workspace-mcp-server.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Plus, Server, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -export interface CreateWorkspaceMcpServerArgs { - /** Name of the MCP server */ - name: string - /** Optional description */ - description?: string - workspaceId?: string -} - -/** - * Create workspace MCP server tool. - * Creates a new MCP server in the workspace that workflows can be deployed to as tools. - */ -export class CreateWorkspaceMcpServerClientTool extends BaseClientTool { - static readonly id = 'create_workspace_mcp_server' - - constructor(toolCallId: string) { - super( - toolCallId, - CreateWorkspaceMcpServerClientTool.id, - CreateWorkspaceMcpServerClientTool.metadata - ) - } - - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as CreateWorkspaceMcpServerArgs | undefined - - const serverName = params?.name || 'MCP Server' - - return { - accept: { text: `Create "${serverName}"`, icon: Plus }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to create MCP server', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Create MCP server?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Creating MCP server', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Created MCP server', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to create MCP server', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted creating MCP server', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped creating MCP server', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Create', icon: Plus }, - reject: { text: 'Skip', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const name = params?.name || 'MCP server' - switch (state) { - case ClientToolCallState.success: - return `Created MCP server "${name}"` - case ClientToolCallState.executing: - return `Creating MCP server "${name}"` - case ClientToolCallState.generating: - return `Preparing to create "${name}"` - case ClientToolCallState.pending: - return `Create MCP server "${name}"?` - case ClientToolCallState.error: - return `Failed to create "${name}"` - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: CreateWorkspaceMcpServerArgs): Promise { - const logger = createLogger('CreateWorkspaceMcpServerClientTool') - try { - if (!args?.name) { - throw new Error('Server name is required') - } - - // Get workspace ID from active workflow if not provided - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - let workspaceId = args?.workspaceId - - if (!workspaceId && activeWorkflowId) { - workspaceId = workflows[activeWorkflowId]?.workspaceId - } - - if (!workspaceId) { - throw new Error('No workspace ID available') - } - - this.setState(ClientToolCallState.executing) - - const res = await fetch('/api/mcp/workflow-servers', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - workspaceId, - name: args.name.trim(), - description: args.description?.trim() || null, - }), - }) - - const data = await res.json() - - if (!res.ok) { - throw new Error(data.error || `Failed to create MCP server (${res.status})`) - } - - const server = data.data?.server - if (!server) { - throw new Error('Server creation response missing server data') - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `MCP server "${args.name}" created successfully. You can now deploy workflows to it using deploy_mcp.`, - { - success: true, - serverId: server.id, - serverName: server.name, - description: server.description, - } - ) - - logger.info(`Created MCP server: ${server.name} (${server.id})`) - } catch (e: any) { - logger.error('Failed to create MCP server', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to create MCP server', { - success: false, - error: e?.message, - }) - } - } - - async execute(args?: CreateWorkspaceMcpServerArgs): Promise { - await this.handleAccept(args) - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/deploy-api.ts b/apps/sim/lib/copilot/tools/client/workflow/deploy-api.ts deleted file mode 100644 index c850dd493..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/deploy-api.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Rocket, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { getInputFormatExample } from '@/lib/workflows/operations/deployment-utils' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface DeployApiArgs { - action: 'deploy' | 'undeploy' - workflowId?: string -} - -/** - * Deploy API tool for deploying workflows as REST APIs. - * This tool handles both deploying and undeploying workflows via the API endpoint. - */ -export class DeployApiClientTool extends BaseClientTool { - static readonly id = 'deploy_api' - - constructor(toolCallId: string) { - super(toolCallId, DeployApiClientTool.id, DeployApiClientTool.metadata) - } - - /** - * Override to provide dynamic button text based on action - */ - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as DeployApiArgs | undefined - - const action = params?.action || 'deploy' - - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - const isAlreadyDeployed = workflowId - ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed - : false - - let buttonText = action === 'undeploy' ? 'Undeploy' : 'Deploy' - - if (action === 'deploy' && isAlreadyDeployed) { - buttonText = 'Redeploy' - } - - return { - accept: { text: buttonText, icon: Rocket }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy API', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy as API?', icon: Rocket }, - [ClientToolCallState.executing]: { text: 'Deploying API', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed API', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to deploy API', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted deploying API', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped deploying API', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Deploy', icon: Rocket }, - reject: { text: 'Skip', icon: XCircle }, - }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy', icon: Rocket }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' - - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - const isAlreadyDeployed = workflowId - ? useWorkflowRegistry.getState().getWorkflowDeploymentStatus(workflowId)?.isDeployed - : false - - let actionText = action - let actionTextIng = action === 'undeploy' ? 'undeploying' : 'deploying' - const actionTextPast = action === 'undeploy' ? 'undeployed' : 'deployed' - - if (action === 'deploy' && isAlreadyDeployed) { - actionText = 'redeploy' - actionTextIng = 'redeploying' - } - - const actionCapitalized = actionText.charAt(0).toUpperCase() + actionText.slice(1) - - switch (state) { - case ClientToolCallState.success: - return `API ${actionTextPast}` - case ClientToolCallState.executing: - return `${actionCapitalized}ing API` - case ClientToolCallState.generating: - return `Preparing to ${actionText} API` - case ClientToolCallState.pending: - return `${actionCapitalized} API?` - case ClientToolCallState.error: - return `Failed to ${actionText} API` - case ClientToolCallState.aborted: - return `Aborted ${actionTextIng} API` - case ClientToolCallState.rejected: - return `Skipped ${actionTextIng} API` - } - return undefined - }, - } - - /** - * Checks if the user has any API keys (workspace or personal) - */ - private async hasApiKeys(workspaceId: string): Promise { - try { - const [workspaceRes, personalRes] = await Promise.all([ - fetch(`/api/workspaces/${workspaceId}/api-keys`), - fetch('/api/users/me/api-keys'), - ]) - - if (!workspaceRes.ok || !personalRes.ok) { - return false - } - - const workspaceData = await workspaceRes.json() - const personalData = await personalRes.json() - - const workspaceKeys = (workspaceData?.keys || []) as Array - const personalKeys = (personalData?.keys || []) as Array - - return workspaceKeys.length > 0 || personalKeys.length > 0 - } catch (error) { - const logger = createLogger('DeployApiClientTool') - logger.warn('Failed to check API keys:', error) - return false - } - } - - /** - * Opens the settings modal to the API keys tab - */ - private openApiKeysModal(): void { - window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'apikeys' } })) - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: DeployApiArgs): Promise { - const logger = createLogger('DeployApiClientTool') - try { - const action = args?.action || 'deploy' - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - const workflowId = args?.workflowId || activeWorkflowId - - if (!workflowId) { - throw new Error('No workflow ID provided') - } - - const workflow = workflows[workflowId] - const workspaceId = workflow?.workspaceId - - // For deploy action, check if user has API keys first - if (action === 'deploy') { - if (!workspaceId) { - throw new Error('Workflow workspace not found') - } - - const hasKeys = await this.hasApiKeys(workspaceId) - - if (!hasKeys) { - this.setState(ClientToolCallState.rejected) - this.openApiKeysModal() - - await this.markToolComplete( - 200, - 'Cannot deploy without an API key. Opened API key settings so you can create one. Once you have an API key, try deploying again.', - { - needsApiKey: true, - message: - 'You need to create an API key before you can deploy your workflow. The API key settings have been opened for you. After creating an API key, you can deploy your workflow.', - } - ) - return - } - } - - this.setState(ClientToolCallState.executing) - - const endpoint = `/api/workflows/${workflowId}/deploy` - const method = action === 'deploy' ? 'POST' : 'DELETE' - - const res = await fetch(endpoint, { - method, - headers: { 'Content-Type': 'application/json' }, - body: action === 'deploy' ? JSON.stringify({ deployChatEnabled: false }) : undefined, - }) - - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Server error (${res.status})`) - } - - const json = await res.json() - - let successMessage = '' - let resultData: any = { - action, - isDeployed: action === 'deploy', - deployedAt: json.deployedAt, - } - - if (action === 'deploy') { - const appUrl = getBaseUrl() - const apiEndpoint = `${appUrl}/api/workflows/${workflowId}/execute` - const apiKeyPlaceholder = '$SIM_API_KEY' - - const inputExample = getInputFormatExample(false) - const curlCommand = `curl -X POST -H "X-API-Key: ${apiKeyPlaceholder}" -H "Content-Type: application/json"${inputExample} ${apiEndpoint}` - - successMessage = 'Workflow deployed successfully as API. You can now call it via REST.' - - resultData = { - ...resultData, - endpoint: apiEndpoint, - curlCommand, - apiKeyPlaceholder, - } - } else { - successMessage = 'Workflow undeployed successfully.' - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, successMessage, resultData) - - // Refresh the workflow registry to update deployment status - try { - const setDeploymentStatus = useWorkflowRegistry.getState().setDeploymentStatus - if (action === 'deploy') { - setDeploymentStatus( - workflowId, - true, - json.deployedAt ? new Date(json.deployedAt) : undefined, - json.apiKey || '' - ) - } else { - setDeploymentStatus(workflowId, false, undefined, '') - } - const actionPast = action === 'undeploy' ? 'undeployed' : 'deployed' - logger.info(`Workflow ${actionPast} as API and registry updated`) - } catch (error) { - logger.warn('Failed to update workflow registry:', error) - } - } catch (e: any) { - logger.error('Deploy API failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to deploy API') - } - } - - async execute(args?: DeployApiArgs): Promise { - await this.handleAccept(args) - } -} - -// Register UI config at module load -registerToolUIConfig(DeployApiClientTool.id, DeployApiClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/workflow/deploy-chat.ts b/apps/sim/lib/copilot/tools/client/workflow/deploy-chat.ts deleted file mode 100644 index 24ad19a53..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/deploy-chat.ts +++ /dev/null @@ -1,381 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, MessageSquare, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -export type ChatAuthType = 'public' | 'password' | 'email' | 'sso' - -export interface OutputConfig { - blockId: string - path: string -} - -export interface DeployChatArgs { - action: 'deploy' | 'undeploy' - workflowId?: string - /** URL slug for the chat (lowercase letters, numbers, hyphens only) */ - identifier?: string - /** Display title for the chat interface */ - title?: string - /** Optional description */ - description?: string - /** Authentication type: public, password, email, or sso */ - authType?: ChatAuthType - /** Password for password-protected chats */ - password?: string - /** List of allowed emails/domains for email or SSO auth */ - allowedEmails?: string[] - /** Welcome message shown to users */ - welcomeMessage?: string - /** Output configurations specifying which block outputs to display in chat */ - outputConfigs?: OutputConfig[] -} - -/** - * Deploy Chat tool for deploying workflows as chat interfaces. - * This tool handles deploying workflows with chat-specific configuration - * including authentication, customization, and output selection. - */ -export class DeployChatClientTool extends BaseClientTool { - static readonly id = 'deploy_chat' - - constructor(toolCallId: string) { - super(toolCallId, DeployChatClientTool.id, DeployChatClientTool.metadata) - } - - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const toolCallsById = useCopilotStore.getState().toolCallsById - const toolCall = toolCallsById[this.toolCallId] - const params = toolCall?.params as DeployChatArgs | undefined - - const action = params?.action || 'deploy' - const buttonText = action === 'undeploy' ? 'Undeploy' : 'Deploy Chat' - - return { - accept: { text: buttonText, icon: MessageSquare }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy chat', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy as chat?', icon: MessageSquare }, - [ClientToolCallState.executing]: { text: 'Deploying chat', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed chat', icon: MessageSquare }, - [ClientToolCallState.error]: { text: 'Failed to deploy chat', icon: XCircle }, - [ClientToolCallState.aborted]: { - text: 'Aborted deploying chat', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped deploying chat', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Deploy Chat', icon: MessageSquare }, - reject: { text: 'Skip', icon: XCircle }, - }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy Chat', icon: MessageSquare }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const action = params?.action === 'undeploy' ? 'undeploy' : 'deploy' - - switch (state) { - case ClientToolCallState.success: - return action === 'undeploy' ? 'Chat undeployed' : 'Chat deployed' - case ClientToolCallState.executing: - return action === 'undeploy' ? 'Undeploying chat' : 'Deploying chat' - case ClientToolCallState.generating: - return `Preparing to ${action} chat` - case ClientToolCallState.pending: - return action === 'undeploy' ? 'Undeploy chat?' : 'Deploy as chat?' - case ClientToolCallState.error: - return `Failed to ${action} chat` - case ClientToolCallState.aborted: - return action === 'undeploy' ? 'Aborted undeploying chat' : 'Aborted deploying chat' - case ClientToolCallState.rejected: - return action === 'undeploy' ? 'Skipped undeploying chat' : 'Skipped deploying chat' - } - return undefined - }, - } - - /** - * Generates a default identifier from the workflow name - */ - private generateIdentifier(workflowName: string): string { - return workflowName - .toLowerCase() - .replace(/[^a-z0-9]+/g, '-') - .replace(/^-|-$/g, '') - .substring(0, 50) - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: DeployChatArgs): Promise { - const logger = createLogger('DeployChatClientTool') - try { - const action = args?.action || 'deploy' - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - const workflowId = args?.workflowId || activeWorkflowId - - if (!workflowId) { - throw new Error('No workflow ID provided') - } - - const workflow = workflows[workflowId] - - // Handle undeploy action - if (action === 'undeploy') { - this.setState(ClientToolCallState.executing) - - // First get the chat deployment ID - const statusRes = await fetch(`/api/workflows/${workflowId}/chat/status`) - if (!statusRes.ok) { - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, 'Failed to check chat deployment status', { - success: false, - action: 'undeploy', - isDeployed: false, - error: 'Failed to check chat deployment status', - errorCode: 'SERVER_ERROR', - }) - return - } - - const statusJson = await statusRes.json() - if (!statusJson.isDeployed || !statusJson.deployment?.id) { - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'No active chat deployment found for this workflow', { - success: false, - action: 'undeploy', - isDeployed: false, - error: 'No active chat deployment found for this workflow', - errorCode: 'VALIDATION_ERROR', - }) - return - } - - const chatId = statusJson.deployment.id - - // Delete the chat deployment - const res = await fetch(`/api/chat/manage/${chatId}`, { - method: 'DELETE', - headers: { 'Content-Type': 'application/json' }, - }) - - if (!res.ok) { - const txt = await res.text().catch(() => '') - this.setState(ClientToolCallState.error) - await this.markToolComplete(res.status, txt || `Server error (${res.status})`, { - success: false, - action: 'undeploy', - isDeployed: true, - error: txt || 'Failed to undeploy chat', - errorCode: 'SERVER_ERROR', - }) - return - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Chat deployment removed successfully.', { - success: true, - action: 'undeploy', - isDeployed: false, - }) - return - } - - this.setState(ClientToolCallState.executing) - - const statusRes = await fetch(`/api/workflows/${workflowId}/chat/status`) - const statusJson = statusRes.ok ? await statusRes.json() : null - const existingDeployment = statusJson?.deployment || null - - const baseIdentifier = - existingDeployment?.identifier || this.generateIdentifier(workflow?.name || 'chat') - const baseTitle = existingDeployment?.title || workflow?.name || 'Chat' - const baseDescription = existingDeployment?.description || '' - const baseAuthType = existingDeployment?.authType || 'public' - const baseWelcomeMessage = - existingDeployment?.customizations?.welcomeMessage || 'Hi there! How can I help you today?' - const basePrimaryColor = - existingDeployment?.customizations?.primaryColor || 'var(--brand-primary-hover-hex)' - const baseAllowedEmails = Array.isArray(existingDeployment?.allowedEmails) - ? existingDeployment.allowedEmails - : [] - const baseOutputConfigs = Array.isArray(existingDeployment?.outputConfigs) - ? existingDeployment.outputConfigs - : [] - - const identifier = args?.identifier || baseIdentifier - const title = args?.title || baseTitle - const description = args?.description ?? baseDescription - const authType = args?.authType || baseAuthType - const welcomeMessage = args?.welcomeMessage || baseWelcomeMessage - const outputConfigs = args?.outputConfigs || baseOutputConfigs - const allowedEmails = args?.allowedEmails || baseAllowedEmails - const primaryColor = basePrimaryColor - - if (!identifier || !title) { - throw new Error('Chat identifier and title are required') - } - - if (authType === 'password' && !args?.password && !existingDeployment?.hasPassword) { - throw new Error('Password is required when using password protection') - } - - if ((authType === 'email' || authType === 'sso') && allowedEmails.length === 0) { - throw new Error(`At least one email or domain is required when using ${authType} access`) - } - - const payload = { - workflowId, - identifier: identifier.trim(), - title: title.trim(), - description: description.trim(), - customizations: { - primaryColor, - welcomeMessage: welcomeMessage.trim(), - }, - authType, - password: authType === 'password' ? args?.password : undefined, - allowedEmails: authType === 'email' || authType === 'sso' ? allowedEmails : [], - outputConfigs, - } - - const isUpdating = Boolean(existingDeployment?.id) - const endpoint = isUpdating ? `/api/chat/manage/${existingDeployment.id}` : '/api/chat' - const method = isUpdating ? 'PATCH' : 'POST' - - const res = await fetch(endpoint, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }) - - const json = await res.json() - - if (!res.ok) { - if (json.error === 'Identifier already in use') { - this.setState(ClientToolCallState.error) - await this.markToolComplete( - 400, - `The identifier "${identifier}" is already in use. Please choose a different one.`, - { - success: false, - action: 'deploy', - isDeployed: false, - identifier, - error: `Identifier "${identifier}" is already taken`, - errorCode: 'IDENTIFIER_TAKEN', - } - ) - return - } - - // Handle validation errors - if (json.code === 'VALIDATION_ERROR') { - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, json.error || 'Validation error', { - success: false, - action: 'deploy', - isDeployed: false, - error: json.error, - errorCode: 'VALIDATION_ERROR', - }) - return - } - - this.setState(ClientToolCallState.error) - await this.markToolComplete(res.status, json.error || 'Failed to deploy chat', { - success: false, - action: 'deploy', - isDeployed: false, - error: json.error || 'Server error', - errorCode: 'SERVER_ERROR', - }) - return - } - - if (!json.chatUrl) { - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, 'Response missing chat URL', { - success: false, - action: 'deploy', - isDeployed: false, - error: 'Response missing chat URL', - errorCode: 'SERVER_ERROR', - }) - return - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `Chat deployed successfully! Available at: ${json.chatUrl}`, - { - success: true, - action: 'deploy', - isDeployed: true, - chatId: json.id, - chatUrl: json.chatUrl, - identifier, - title, - authType, - } - ) - - // Update the workflow registry to reflect deployment status - // Chat deployment also deploys the API, so we update the registry - try { - const setDeploymentStatus = useWorkflowRegistry.getState().setDeploymentStatus - setDeploymentStatus(workflowId, true, new Date(), '') - logger.info('Workflow deployment status updated in registry') - } catch (error) { - logger.warn('Failed to update workflow registry:', error) - } - - logger.info('Chat deployed successfully:', json.chatUrl) - } catch (e: any) { - logger.error('Deploy chat failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to deploy chat', { - success: false, - action: 'deploy', - isDeployed: false, - error: e?.message || 'Failed to deploy chat', - errorCode: 'SERVER_ERROR', - }) - } - } - - async execute(args?: DeployChatArgs): Promise { - await this.handleAccept(args) - } -} - -// Register UI config at module load -registerToolUIConfig(DeployChatClientTool.id, DeployChatClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/workflow/deploy-mcp.ts b/apps/sim/lib/copilot/tools/client/workflow/deploy-mcp.ts deleted file mode 100644 index bcd87fc25..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/deploy-mcp.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Server, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -export interface ParameterDescription { - name: string - description: string -} - -export interface DeployMcpArgs { - /** The MCP server ID to deploy to (get from list_workspace_mcp_servers) */ - serverId: string - /** Optional workflow ID (defaults to active workflow) */ - workflowId?: string - /** Custom tool name (defaults to workflow name) */ - toolName?: string - /** Custom tool description */ - toolDescription?: string - /** Parameter descriptions to include in the schema */ - parameterDescriptions?: ParameterDescription[] -} - -/** - * Deploy MCP tool. - * Deploys the workflow as an MCP tool to a workspace MCP server. - */ -export class DeployMcpClientTool extends BaseClientTool { - static readonly id = 'deploy_mcp' - - constructor(toolCallId: string) { - super(toolCallId, DeployMcpClientTool.id, DeployMcpClientTool.metadata) - } - - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - return { - accept: { text: 'Deploy to MCP', icon: Server }, - reject: { text: 'Skip', icon: XCircle }, - } - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to deploy to MCP', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Deploy to MCP server?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Deploying to MCP', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Deployed to MCP', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to deploy to MCP', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted MCP deployment', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped MCP deployment', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Deploy', icon: Server }, - reject: { text: 'Skip', icon: XCircle }, - }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Deploy', icon: Server }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - }, - getDynamicText: (params, state) => { - const toolName = params?.toolName || 'workflow' - switch (state) { - case ClientToolCallState.success: - return `Deployed "${toolName}" to MCP` - case ClientToolCallState.executing: - return `Deploying "${toolName}" to MCP` - case ClientToolCallState.generating: - return `Preparing to deploy to MCP` - case ClientToolCallState.pending: - return `Deploy "${toolName}" to MCP?` - case ClientToolCallState.error: - return `Failed to deploy to MCP` - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: DeployMcpArgs): Promise { - const logger = createLogger('DeployMcpClientTool') - try { - if (!args?.serverId) { - throw new Error( - 'Server ID is required. Use list_workspace_mcp_servers to get available servers.' - ) - } - - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - const workflowId = args?.workflowId || activeWorkflowId - - if (!workflowId) { - throw new Error('No workflow ID available') - } - - const workflow = workflows[workflowId] - const workspaceId = workflow?.workspaceId - - if (!workspaceId) { - throw new Error('Workflow workspace not found') - } - - // Check if workflow is deployed - const deploymentStatus = useWorkflowRegistry - .getState() - .getWorkflowDeploymentStatus(workflowId) - if (!deploymentStatus?.isDeployed) { - throw new Error( - 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.' - ) - } - - this.setState(ClientToolCallState.executing) - - let parameterSchema: Record | undefined - if (args?.parameterDescriptions && args.parameterDescriptions.length > 0) { - const properties: Record = {} - for (const param of args.parameterDescriptions) { - properties[param.name] = { description: param.description } - } - parameterSchema = { properties } - } - - const res = await fetch( - `/api/mcp/workflow-servers/${args.serverId}/tools?workspaceId=${workspaceId}`, - { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - workflowId, - toolName: args.toolName?.trim(), - toolDescription: args.toolDescription?.trim(), - parameterSchema, - }), - } - ) - - const data = await res.json() - - if (!res.ok) { - if (data.error?.includes('already added')) { - const toolsRes = await fetch( - `/api/mcp/workflow-servers/${args.serverId}/tools?workspaceId=${workspaceId}` - ) - const toolsJson = toolsRes.ok ? await toolsRes.json() : null - const tools = toolsJson?.data?.tools || [] - const existingTool = tools.find((tool: any) => tool.workflowId === workflowId) - if (!existingTool?.id) { - throw new Error('This workflow is already deployed to this MCP server') - } - const patchRes = await fetch( - `/api/mcp/workflow-servers/${args.serverId}/tools/${existingTool.id}?workspaceId=${workspaceId}`, - { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - toolName: args.toolName?.trim(), - toolDescription: args.toolDescription?.trim(), - parameterSchema, - }), - } - ) - const patchJson = patchRes.ok ? await patchRes.json() : null - if (!patchRes.ok) { - const patchError = patchJson?.error || `Failed to update MCP tool (${patchRes.status})` - throw new Error(patchError) - } - const updatedTool = patchJson?.data?.tool - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `Workflow MCP tool updated to "${updatedTool?.toolName || existingTool.toolName}".`, - { - success: true, - toolId: updatedTool?.id || existingTool.id, - toolName: updatedTool?.toolName || existingTool.toolName, - toolDescription: updatedTool?.toolDescription || existingTool.toolDescription, - serverId: args.serverId, - updated: true, - } - ) - logger.info('Updated workflow MCP tool', { toolId: existingTool.id }) - return - } - if (data.error?.includes('not deployed')) { - throw new Error('Workflow must be deployed before adding as an MCP tool') - } - if (data.error?.includes('Start block')) { - throw new Error('Workflow must have a Start block to be used as an MCP tool') - } - if (data.error?.includes('Server not found')) { - throw new Error( - 'MCP server not found. Use list_workspace_mcp_servers to see available servers.' - ) - } - throw new Error(data.error || `Failed to deploy to MCP (${res.status})`) - } - - const tool = data.data?.tool - if (!tool) { - throw new Error('Response missing tool data') - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `Workflow deployed as MCP tool "${tool.toolName}" to server.`, - { - success: true, - toolId: tool.id, - toolName: tool.toolName, - toolDescription: tool.toolDescription, - serverId: args.serverId, - } - ) - - logger.info(`Deployed workflow as MCP tool: ${tool.toolName}`) - } catch (e: any) { - logger.error('Failed to deploy to MCP', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to deploy to MCP', { - success: false, - error: e?.message, - }) - } - } - - async execute(args?: DeployMcpArgs): Promise { - await this.handleAccept(args) - } -} - -// Register UI config at module load -registerToolUIConfig(DeployMcpClientTool.id, DeployMcpClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts deleted file mode 100644 index 6c56dc140..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/edit-workflow.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { Grid2x2, Grid2x2Check, Grid2x2X, Loader2, MinusCircle, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' - -export class EditWorkflowClientTool extends BaseClientTool { - static readonly id = 'edit_workflow' - - constructor(toolCallId: string) { - super(toolCallId, EditWorkflowClientTool.id, EditWorkflowClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Editing your workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Editing your workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Edited your workflow', icon: Grid2x2Check }, - [ClientToolCallState.error]: { text: 'Failed to edit your workflow', icon: XCircle }, - [ClientToolCallState.review]: { text: 'Review your workflow changes', icon: Grid2x2 }, - [ClientToolCallState.rejected]: { text: 'Rejected workflow changes', icon: Grid2x2X }, - [ClientToolCallState.aborted]: { text: 'Aborted editing your workflow', icon: MinusCircle }, - [ClientToolCallState.pending]: { text: 'Editing your workflow', icon: Loader2 }, - }, - uiConfig: { - isSpecial: true, - customRenderer: 'edit_summary', - }, - } - - async handleAccept(): Promise { - // Diff store calls this after review acceptance. - this.setState(ClientToolCallState.success) - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // The store's tool_result SSE handler applies the diff preview - // via diffStore.setProposedChanges() when the result arrives. - this.setState(ClientToolCallState.success) - } -} - -// Register UI config at module load -registerToolUIConfig(EditWorkflowClientTool.id, EditWorkflowClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-block-outputs.ts b/apps/sim/lib/copilot/tools/client/workflow/get-block-outputs.ts deleted file mode 100644 index d835678d3..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-block-outputs.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Tag, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { - computeBlockOutputPaths, - formatOutputsWithPrefix, - getSubflowInsidePaths, - getWorkflowSubBlockValues, - getWorkflowVariables, -} from '@/lib/copilot/tools/client/workflow/block-output-utils' -import { - GetBlockOutputsResult, - type GetBlockOutputsResultType, -} from '@/lib/copilot/tools/shared/schemas' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' - -const logger = createLogger('GetBlockOutputsClientTool') - -interface GetBlockOutputsArgs { - blockIds?: string[] -} - -export class GetBlockOutputsClientTool extends BaseClientTool { - static readonly id = 'get_block_outputs' - - constructor(toolCallId: string) { - super(toolCallId, GetBlockOutputsClientTool.id, GetBlockOutputsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting block outputs', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting block outputs', icon: Tag }, - [ClientToolCallState.executing]: { text: 'Getting block outputs', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted getting outputs', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved block outputs', icon: Tag }, - [ClientToolCallState.error]: { text: 'Failed to get outputs', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped getting outputs', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const blockIds = params?.blockIds - if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { - const count = blockIds.length - switch (state) { - case ClientToolCallState.success: - return `Retrieved outputs for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Getting outputs for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.error: - return `Failed to get outputs for ${count} block${count > 1 ? 's' : ''}` - } - } - return undefined - }, - } - - async execute(args?: GetBlockOutputsArgs): Promise { - try { - this.setState(ClientToolCallState.executing) - - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (!activeWorkflowId) { - await this.markToolComplete(400, 'No active workflow found') - this.setState(ClientToolCallState.error) - return - } - - const workflowStore = useWorkflowStore.getState() - const blocks = workflowStore.blocks || {} - const loops = workflowStore.loops || {} - const parallels = workflowStore.parallels || {} - const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId) - - const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues } - const targetBlockIds = - args?.blockIds && args.blockIds.length > 0 ? args.blockIds : Object.keys(blocks) - - const blockOutputs: GetBlockOutputsResultType['blocks'] = [] - - for (const blockId of targetBlockIds) { - const block = blocks[blockId] - if (!block?.type) continue - - const blockName = block.name || block.type - - const blockOutput: GetBlockOutputsResultType['blocks'][0] = { - blockId, - blockName, - blockType: block.type, - outputs: [], - } - - // Include triggerMode if the block is in trigger mode - if (block.triggerMode) { - blockOutput.triggerMode = true - } - - if (block.type === 'loop' || block.type === 'parallel') { - const insidePaths = getSubflowInsidePaths(block.type, blockId, loops, parallels) - blockOutput.insideSubflowOutputs = formatOutputsWithPrefix(insidePaths, blockName) - blockOutput.outsideSubflowOutputs = formatOutputsWithPrefix(['results'], blockName) - } else { - const outputPaths = computeBlockOutputPaths(block, ctx) - blockOutput.outputs = formatOutputsWithPrefix(outputPaths, blockName) - } - - blockOutputs.push(blockOutput) - } - - const includeVariables = !args?.blockIds || args.blockIds.length === 0 - const resultData: { - blocks: typeof blockOutputs - variables?: ReturnType - } = { - blocks: blockOutputs, - } - if (includeVariables) { - resultData.variables = getWorkflowVariables(activeWorkflowId) - } - - const result = GetBlockOutputsResult.parse(resultData) - - logger.info('Retrieved block outputs', { - blockCount: blockOutputs.length, - variableCount: resultData.variables?.length ?? 0, - }) - - await this.markToolComplete(200, 'Retrieved block outputs', result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message }) - await this.markToolComplete(500, message || 'Failed to get block outputs') - this.setState(ClientToolCallState.error) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-block-upstream-references.ts b/apps/sim/lib/copilot/tools/client/workflow/get-block-upstream-references.ts deleted file mode 100644 index f02c9958c..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-block-upstream-references.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { createLogger } from '@sim/logger' -import { GitBranch, Loader2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { - computeBlockOutputPaths, - formatOutputsWithPrefix, - getSubflowInsidePaths, - getWorkflowSubBlockValues, - getWorkflowVariables, -} from '@/lib/copilot/tools/client/workflow/block-output-utils' -import { - GetBlockUpstreamReferencesResult, - type GetBlockUpstreamReferencesResultType, -} from '@/lib/copilot/tools/shared/schemas' -import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator' -import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' -import type { Loop, Parallel } from '@/stores/workflows/workflow/types' - -const logger = createLogger('GetBlockUpstreamReferencesClientTool') - -interface GetBlockUpstreamReferencesArgs { - blockIds: string[] -} - -export class GetBlockUpstreamReferencesClientTool extends BaseClientTool { - static readonly id = 'get_block_upstream_references' - - constructor(toolCallId: string) { - super( - toolCallId, - GetBlockUpstreamReferencesClientTool.id, - GetBlockUpstreamReferencesClientTool.metadata - ) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Getting upstream references', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Getting upstream references', icon: GitBranch }, - [ClientToolCallState.executing]: { text: 'Getting upstream references', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted getting references', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved upstream references', icon: GitBranch }, - [ClientToolCallState.error]: { text: 'Failed to get references', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped getting references', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const blockIds = params?.blockIds - if (blockIds && Array.isArray(blockIds) && blockIds.length > 0) { - const count = blockIds.length - switch (state) { - case ClientToolCallState.success: - return `Retrieved references for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Getting references for ${count} block${count > 1 ? 's' : ''}` - case ClientToolCallState.error: - return `Failed to get references for ${count} block${count > 1 ? 's' : ''}` - } - } - return undefined - }, - } - - async execute(args?: GetBlockUpstreamReferencesArgs): Promise { - try { - this.setState(ClientToolCallState.executing) - - if (!args?.blockIds || args.blockIds.length === 0) { - await this.markToolComplete(400, 'blockIds array is required') - this.setState(ClientToolCallState.error) - return - } - - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (!activeWorkflowId) { - await this.markToolComplete(400, 'No active workflow found') - this.setState(ClientToolCallState.error) - return - } - - const workflowStore = useWorkflowStore.getState() - const blocks = workflowStore.blocks || {} - const edges = workflowStore.edges || [] - const loops = workflowStore.loops || {} - const parallels = workflowStore.parallels || {} - const subBlockValues = getWorkflowSubBlockValues(activeWorkflowId) - - const ctx = { workflowId: activeWorkflowId, blocks, loops, parallels, subBlockValues } - const variableOutputs = getWorkflowVariables(activeWorkflowId) - const graphEdges = edges.map((edge) => ({ source: edge.source, target: edge.target })) - - const results: GetBlockUpstreamReferencesResultType['results'] = [] - - for (const blockId of args.blockIds) { - const targetBlock = blocks[blockId] - if (!targetBlock) { - logger.warn(`Block ${blockId} not found`) - continue - } - - const insideSubflows: { blockId: string; blockName: string; blockType: string }[] = [] - const containingLoopIds = new Set() - const containingParallelIds = new Set() - - Object.values(loops as Record).forEach((loop) => { - if (loop?.nodes?.includes(blockId)) { - containingLoopIds.add(loop.id) - const loopBlock = blocks[loop.id] - if (loopBlock) { - insideSubflows.push({ - blockId: loop.id, - blockName: loopBlock.name || loopBlock.type, - blockType: 'loop', - }) - } - } - }) - - Object.values(parallels as Record).forEach((parallel) => { - if (parallel?.nodes?.includes(blockId)) { - containingParallelIds.add(parallel.id) - const parallelBlock = blocks[parallel.id] - if (parallelBlock) { - insideSubflows.push({ - blockId: parallel.id, - blockName: parallelBlock.name || parallelBlock.type, - blockType: 'parallel', - }) - } - } - }) - - const ancestorIds = BlockPathCalculator.findAllPathNodes(graphEdges, blockId) - const accessibleIds = new Set(ancestorIds) - accessibleIds.add(blockId) - - const starterBlock = Object.values(blocks).find((b) => isInputDefinitionTrigger(b.type)) - if (starterBlock && ancestorIds.includes(starterBlock.id)) { - accessibleIds.add(starterBlock.id) - } - - containingLoopIds.forEach((loopId) => { - accessibleIds.add(loopId) - loops[loopId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId)) - }) - - containingParallelIds.forEach((parallelId) => { - accessibleIds.add(parallelId) - parallels[parallelId]?.nodes?.forEach((nodeId) => accessibleIds.add(nodeId)) - }) - - const accessibleBlocks: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'] = - [] - - for (const accessibleBlockId of accessibleIds) { - const block = blocks[accessibleBlockId] - if (!block?.type) continue - - const canSelfReference = block.type === 'approval' || block.type === 'human_in_the_loop' - if (accessibleBlockId === blockId && !canSelfReference) continue - - const blockName = block.name || block.type - let accessContext: 'inside' | 'outside' | undefined - let outputPaths: string[] - - if (block.type === 'loop' || block.type === 'parallel') { - const isInside = - (block.type === 'loop' && containingLoopIds.has(accessibleBlockId)) || - (block.type === 'parallel' && containingParallelIds.has(accessibleBlockId)) - - accessContext = isInside ? 'inside' : 'outside' - outputPaths = isInside - ? getSubflowInsidePaths(block.type, accessibleBlockId, loops, parallels) - : ['results'] - } else { - outputPaths = computeBlockOutputPaths(block, ctx) - } - - const formattedOutputs = formatOutputsWithPrefix(outputPaths, blockName) - - const entry: GetBlockUpstreamReferencesResultType['results'][0]['accessibleBlocks'][0] = { - blockId: accessibleBlockId, - blockName, - blockType: block.type, - outputs: formattedOutputs, - } - - // Include triggerMode if the block is in trigger mode - if (block.triggerMode) { - entry.triggerMode = true - } - - if (accessContext) entry.accessContext = accessContext - accessibleBlocks.push(entry) - } - - const resultEntry: GetBlockUpstreamReferencesResultType['results'][0] = { - blockId, - blockName: targetBlock.name || targetBlock.type, - accessibleBlocks, - variables: variableOutputs, - } - - if (insideSubflows.length > 0) resultEntry.insideSubflows = insideSubflows - results.push(resultEntry) - } - - const result = GetBlockUpstreamReferencesResult.parse({ results }) - - logger.info('Retrieved upstream references', { - blockIds: args.blockIds, - resultCount: results.length, - }) - - await this.markToolComplete(200, 'Retrieved upstream references', result) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Error in tool execution', { toolCallId: this.toolCallId, error, message }) - await this.markToolComplete(500, message || 'Failed to get upstream references') - this.setState(ClientToolCallState.error) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-user-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/get-user-workflow.ts deleted file mode 100644 index c67f92a9e..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-user-workflow.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Workflow as WorkflowIcon, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { stripWorkflowDiffMarkers } from '@/lib/workflows/diff' -import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -import { mergeSubblockState } from '@/stores/workflows/utils' -import { useWorkflowStore } from '@/stores/workflows/workflow/store' - -interface GetUserWorkflowArgs { - workflowId?: string - includeMetadata?: boolean -} - -const logger = createLogger('GetUserWorkflowClientTool') - -export class GetUserWorkflowClientTool extends BaseClientTool { - static readonly id = 'get_user_workflow' - - constructor(toolCallId: string) { - super(toolCallId, GetUserWorkflowClientTool.id, GetUserWorkflowClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading your workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading your workflow', icon: WorkflowIcon }, - [ClientToolCallState.executing]: { text: 'Reading your workflow', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted reading your workflow', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Read your workflow', icon: WorkflowIcon }, - [ClientToolCallState.error]: { text: 'Failed to read your workflow', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped reading your workflow', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - if (workflowId) { - const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name - if (workflowName) { - switch (state) { - case ClientToolCallState.success: - return `Read ${workflowName}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Reading ${workflowName}` - case ClientToolCallState.error: - return `Failed to read ${workflowName}` - case ClientToolCallState.aborted: - return `Aborted reading ${workflowName}` - case ClientToolCallState.rejected: - return `Skipped reading ${workflowName}` - } - } - } - return undefined - }, - } - - async execute(args?: GetUserWorkflowArgs): Promise { - try { - this.setState(ClientToolCallState.executing) - - // Determine workflow ID (explicit or active) - let workflowId = args?.workflowId - if (!workflowId) { - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (!activeWorkflowId) { - await this.markToolComplete(400, 'No active workflow found') - this.setState(ClientToolCallState.error) - return - } - workflowId = activeWorkflowId as any - } - - logger.info('Fetching user workflow from stores', { - workflowId, - includeMetadata: args?.includeMetadata, - }) - - // Always use main workflow store as the source of truth - const workflowStore = useWorkflowStore.getState() - const fullWorkflowState = workflowStore.getWorkflowState() - - let workflowState: any = null - - if (!fullWorkflowState || !fullWorkflowState.blocks) { - const workflowRegistry = useWorkflowRegistry.getState() - const wfKey = String(workflowId) - const workflow = (workflowRegistry as any).workflows?.[wfKey] - - if (!workflow) { - await this.markToolComplete(404, `Workflow ${workflowId} not found in any store`) - this.setState(ClientToolCallState.error) - return - } - - logger.warn('No workflow state found, using workflow metadata only', { workflowId }) - workflowState = workflow - } else { - workflowState = stripWorkflowDiffMarkers(fullWorkflowState) - logger.info('Using workflow state from workflow store', { - workflowId, - blockCount: Object.keys(fullWorkflowState.blocks || {}).length, - }) - } - - // Normalize required properties - if (workflowState) { - if (!workflowState.loops) workflowState.loops = {} - if (!workflowState.parallels) workflowState.parallels = {} - if (!workflowState.edges) workflowState.edges = [] - if (!workflowState.blocks) workflowState.blocks = {} - } - - // Merge latest subblock values so edits are reflected - try { - if (workflowState?.blocks) { - workflowState = { - ...workflowState, - blocks: mergeSubblockState(workflowState.blocks, workflowId as any), - } - logger.info('Merged subblock values into workflow state', { - workflowId, - blockCount: Object.keys(workflowState.blocks || {}).length, - }) - } - } catch (mergeError) { - logger.warn('Failed to merge subblock values; proceeding with raw workflow state', { - workflowId, - error: mergeError instanceof Error ? mergeError.message : String(mergeError), - }) - } - - logger.info('Validating workflow state', { - workflowId, - hasWorkflowState: !!workflowState, - hasBlocks: !!workflowState?.blocks, - workflowStateType: typeof workflowState, - }) - - if (!workflowState || !workflowState.blocks) { - await this.markToolComplete(422, 'Workflow state is empty or invalid') - this.setState(ClientToolCallState.error) - return - } - - // Sanitize workflow state for copilot (remove UI-specific data) - const sanitizedState = sanitizeForCopilot(workflowState) - - // Convert to JSON string for transport - let workflowJson = '' - try { - workflowJson = JSON.stringify(sanitizedState, null, 2) - logger.info('Successfully stringified sanitized workflow state', { - workflowId, - jsonLength: workflowJson.length, - }) - } catch (stringifyError) { - await this.markToolComplete( - 500, - `Failed to convert workflow to JSON: ${ - stringifyError instanceof Error ? stringifyError.message : 'Unknown error' - }` - ) - this.setState(ClientToolCallState.error) - return - } - - // Mark complete with data; keep state success for store render - await this.markToolComplete(200, 'Workflow analyzed', { userWorkflow: workflowJson }) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - logger.error('Error in tool execution', { - toolCallId: this.toolCallId, - error, - message, - }) - await this.markToolComplete(500, message || 'Failed to fetch workflow') - this.setState(ClientToolCallState.error) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts b/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts deleted file mode 100644 index 24f27713b..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-console.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Loader2, MinusCircle, TerminalSquare, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' - -export class GetWorkflowConsoleClientTool extends BaseClientTool { - static readonly id = 'get_workflow_console' - - constructor(toolCallId: string) { - super(toolCallId, GetWorkflowConsoleClientTool.id, GetWorkflowConsoleClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching execution logs', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Fetching execution logs', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Fetched execution logs', icon: TerminalSquare }, - [ClientToolCallState.error]: { text: 'Failed to fetch execution logs', icon: XCircle }, - [ClientToolCallState.rejected]: { - text: 'Skipped fetching execution logs', - icon: MinusCircle, - }, - [ClientToolCallState.aborted]: { - text: 'Aborted fetching execution logs', - icon: MinusCircle, - }, - [ClientToolCallState.pending]: { text: 'Fetching execution logs', icon: Loader2 }, - }, - getDynamicText: (params, state) => { - const limit = params?.limit - if (limit && typeof limit === 'number') { - const logText = limit === 1 ? 'execution log' : 'execution logs' - - switch (state) { - case ClientToolCallState.success: - return `Fetched last ${limit} ${logText}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Fetching last ${limit} ${logText}` - case ClientToolCallState.error: - return `Failed to fetch last ${limit} ${logText}` - case ClientToolCallState.rejected: - return `Skipped fetching last ${limit} ${logText}` - case ClientToolCallState.aborted: - return `Aborted fetching last ${limit} ${logText}` - } - } - return undefined - }, - } - - async execute(): Promise { - // Tool execution is handled server-side by the orchestrator. - // Client tool classes are retained for UI display configuration only. - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-data.ts b/apps/sim/lib/copilot/tools/client/workflow/get-workflow-data.ts deleted file mode 100644 index 657daa0a0..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-data.ts +++ /dev/null @@ -1,269 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Database, Loader2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -const logger = createLogger('GetWorkflowDataClientTool') - -/** Data type enum for the get_workflow_data tool */ -export type WorkflowDataType = 'global_variables' | 'custom_tools' | 'mcp_tools' | 'files' - -interface GetWorkflowDataArgs { - data_type: WorkflowDataType -} - -export class GetWorkflowDataClientTool extends BaseClientTool { - static readonly id = 'get_workflow_data' - - constructor(toolCallId: string) { - super(toolCallId, GetWorkflowDataClientTool.id, GetWorkflowDataClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Fetching workflow data', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Fetching workflow data', icon: Database }, - [ClientToolCallState.executing]: { text: 'Fetching workflow data', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted fetching data', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Retrieved workflow data', icon: Database }, - [ClientToolCallState.error]: { text: 'Failed to fetch data', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped fetching data', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const dataType = params?.data_type as WorkflowDataType | undefined - if (!dataType) return undefined - - const typeLabels: Record = { - global_variables: 'variables', - custom_tools: 'custom tools', - mcp_tools: 'MCP tools', - files: 'files', - } - - const label = typeLabels[dataType] || dataType - - switch (state) { - case ClientToolCallState.success: - return `Retrieved ${label}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - return `Fetching ${label}` - case ClientToolCallState.pending: - return `Fetch ${label}?` - case ClientToolCallState.error: - return `Failed to fetch ${label}` - case ClientToolCallState.aborted: - return `Aborted fetching ${label}` - case ClientToolCallState.rejected: - return `Skipped fetching ${label}` - } - return undefined - }, - } - - async execute(args?: GetWorkflowDataArgs): Promise { - try { - this.setState(ClientToolCallState.executing) - - const dataType = args?.data_type - if (!dataType) { - await this.markToolComplete(400, 'Missing data_type parameter') - this.setState(ClientToolCallState.error) - return - } - - const { activeWorkflowId, hydration } = useWorkflowRegistry.getState() - const activeWorkspaceId = hydration.workspaceId - - switch (dataType) { - case 'global_variables': - await this.fetchGlobalVariables(activeWorkflowId) - break - case 'custom_tools': - await this.fetchCustomTools(activeWorkspaceId) - break - case 'mcp_tools': - await this.fetchMcpTools(activeWorkspaceId) - break - case 'files': - await this.fetchFiles(activeWorkspaceId) - break - default: - await this.markToolComplete(400, `Unknown data_type: ${dataType}`) - this.setState(ClientToolCallState.error) - return - } - } catch (error: unknown) { - const message = error instanceof Error ? error.message : String(error) - await this.markToolComplete(500, message || 'Failed to fetch workflow data') - this.setState(ClientToolCallState.error) - } - } - - /** - * Fetch global workflow variables - */ - private async fetchGlobalVariables(workflowId: string | null): Promise { - if (!workflowId) { - await this.markToolComplete(400, 'No active workflow found') - this.setState(ClientToolCallState.error) - return - } - - const res = await fetch(`/api/workflows/${workflowId}/variables`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch workflow variables') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const varsRecord = (json?.data as Record) || {} - const variables = Object.values(varsRecord).map((v: unknown) => { - const variable = v as { id?: string; name?: string; value?: unknown } - return { - id: String(variable?.id || ''), - name: String(variable?.name || ''), - value: variable?.value, - } - }) - - logger.info('Fetched workflow variables', { count: variables.length }) - await this.markToolComplete(200, `Found ${variables.length} variable(s)`, { variables }) - this.setState(ClientToolCallState.success) - } - - /** - * Fetch custom tools for the workspace - */ - private async fetchCustomTools(workspaceId: string | null): Promise { - if (!workspaceId) { - await this.markToolComplete(400, 'No active workspace found') - this.setState(ClientToolCallState.error) - return - } - - const res = await fetch(`/api/tools/custom?workspaceId=${workspaceId}`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch custom tools') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const toolsData = (json?.data as unknown[]) || [] - const customTools = toolsData.map((tool: unknown) => { - const t = tool as { - id?: string - title?: string - schema?: { function?: { name?: string; description?: string; parameters?: unknown } } - code?: string - } - return { - id: String(t?.id || ''), - title: String(t?.title || ''), - functionName: String(t?.schema?.function?.name || ''), - description: String(t?.schema?.function?.description || ''), - parameters: t?.schema?.function?.parameters, - } - }) - - logger.info('Fetched custom tools', { count: customTools.length }) - await this.markToolComplete(200, `Found ${customTools.length} custom tool(s)`, { customTools }) - this.setState(ClientToolCallState.success) - } - - /** - * Fetch MCP tools for the workspace - */ - private async fetchMcpTools(workspaceId: string | null): Promise { - if (!workspaceId) { - await this.markToolComplete(400, 'No active workspace found') - this.setState(ClientToolCallState.error) - return - } - - const res = await fetch(`/api/mcp/tools/discover?workspaceId=${workspaceId}`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch MCP tools') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const toolsData = (json?.data?.tools as unknown[]) || [] - const mcpTools = toolsData.map((tool: unknown) => { - const t = tool as { - name?: string - serverId?: string - serverName?: string - description?: string - inputSchema?: unknown - } - return { - name: String(t?.name || ''), - serverId: String(t?.serverId || ''), - serverName: String(t?.serverName || ''), - description: String(t?.description || ''), - inputSchema: t?.inputSchema, - } - }) - - logger.info('Fetched MCP tools', { count: mcpTools.length }) - await this.markToolComplete(200, `Found ${mcpTools.length} MCP tool(s)`, { mcpTools }) - this.setState(ClientToolCallState.success) - } - - /** - * Fetch workspace files metadata - */ - private async fetchFiles(workspaceId: string | null): Promise { - if (!workspaceId) { - await this.markToolComplete(400, 'No active workspace found') - this.setState(ClientToolCallState.error) - return - } - - const res = await fetch(`/api/workspaces/${workspaceId}/files`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch files') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const filesData = (json?.files as unknown[]) || [] - const files = filesData.map((file: unknown) => { - const f = file as { - id?: string - name?: string - key?: string - path?: string - size?: number - type?: string - uploadedAt?: string - } - return { - id: String(f?.id || ''), - name: String(f?.name || ''), - key: String(f?.key || ''), - path: String(f?.path || ''), - size: Number(f?.size || 0), - type: String(f?.type || ''), - uploadedAt: String(f?.uploadedAt || ''), - } - }) - - logger.info('Fetched workspace files', { count: files.length }) - await this.markToolComplete(200, `Found ${files.length} file(s)`, { files }) - this.setState(ClientToolCallState.success) - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-from-name.ts b/apps/sim/lib/copilot/tools/client/workflow/get-workflow-from-name.ts deleted file mode 100644 index cb001a57c..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/get-workflow-from-name.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { createLogger } from '@sim/logger' -import { FileText, Loader2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { - formatWorkflowStateForCopilot, - normalizeWorkflowName, -} from '@/lib/copilot/tools/shared/workflow-utils' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -const logger = createLogger('GetWorkflowFromNameClientTool') - -interface GetWorkflowFromNameArgs { - workflow_name: string -} - -export class GetWorkflowFromNameClientTool extends BaseClientTool { - static readonly id = 'get_workflow_from_name' - - constructor(toolCallId: string) { - super(toolCallId, GetWorkflowFromNameClientTool.id, GetWorkflowFromNameClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Reading workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Reading workflow', icon: FileText }, - [ClientToolCallState.executing]: { text: 'Reading workflow', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted reading workflow', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Read workflow', icon: FileText }, - [ClientToolCallState.error]: { text: 'Failed to read workflow', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped reading workflow', icon: XCircle }, - }, - getDynamicText: (params, state) => { - if (params?.workflow_name && typeof params.workflow_name === 'string') { - const workflowName = params.workflow_name - - switch (state) { - case ClientToolCallState.success: - return `Read ${workflowName}` - case ClientToolCallState.executing: - case ClientToolCallState.generating: - case ClientToolCallState.pending: - return `Reading ${workflowName}` - case ClientToolCallState.error: - return `Failed to read ${workflowName}` - case ClientToolCallState.aborted: - return `Aborted reading ${workflowName}` - case ClientToolCallState.rejected: - return `Skipped reading ${workflowName}` - } - } - return undefined - }, - } - - async execute(args?: GetWorkflowFromNameArgs): Promise { - try { - this.setState(ClientToolCallState.executing) - - const workflowName = args?.workflow_name?.trim() - if (!workflowName) { - await this.markToolComplete(400, 'workflow_name is required') - this.setState(ClientToolCallState.error) - return - } - - // Try to find by name from registry first to get ID - const registry = useWorkflowRegistry.getState() - const targetName = normalizeWorkflowName(workflowName) - const match = Object.values((registry as any).workflows || {}).find( - (w: any) => normalizeWorkflowName(w?.name) === targetName - ) as any - - if (!match?.id) { - await this.markToolComplete(404, `Workflow not found: ${workflowName}`) - this.setState(ClientToolCallState.error) - return - } - - // Fetch full workflow from API route (normalized tables) - const res = await fetch(`/api/workflows/${encodeURIComponent(match.id)}`, { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch workflow by name') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const wf = json?.data - if (!wf?.state?.blocks) { - await this.markToolComplete(422, 'Workflow state is empty or invalid') - this.setState(ClientToolCallState.error) - return - } - - // Convert state to the same string format as get_user_workflow - const userWorkflow = formatWorkflowStateForCopilot({ - blocks: wf.state.blocks || {}, - edges: wf.state.edges || [], - loops: wf.state.loops || {}, - parallels: wf.state.parallels || {}, - }) - - await this.markToolComplete(200, `Retrieved workflow ${workflowName}`, { userWorkflow }) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - await this.markToolComplete(500, message || 'Failed to retrieve workflow by name') - this.setState(ClientToolCallState.error) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts b/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts deleted file mode 100644 index 33a9df881..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/list-user-workflows.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { createLogger } from '@sim/logger' -import { ListChecks, Loader2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { extractWorkflowNames } from '@/lib/copilot/tools/shared/workflow-utils' - -const logger = createLogger('ListUserWorkflowsClientTool') - -export class ListUserWorkflowsClientTool extends BaseClientTool { - static readonly id = 'list_user_workflows' - - constructor(toolCallId: string) { - super(toolCallId, ListUserWorkflowsClientTool.id, ListUserWorkflowsClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Listing your workflows', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Listing your workflows', icon: ListChecks }, - [ClientToolCallState.executing]: { text: 'Listing your workflows', icon: Loader2 }, - [ClientToolCallState.aborted]: { text: 'Aborted listing workflows', icon: XCircle }, - [ClientToolCallState.success]: { text: 'Listed your workflows', icon: ListChecks }, - [ClientToolCallState.error]: { text: 'Failed to list workflows', icon: X }, - [ClientToolCallState.rejected]: { text: 'Skipped listing workflows', icon: XCircle }, - }, - } - - async execute(): Promise { - try { - this.setState(ClientToolCallState.executing) - - const res = await fetch('/api/workflows', { method: 'GET' }) - if (!res.ok) { - const text = await res.text().catch(() => '') - await this.markToolComplete(res.status, text || 'Failed to fetch workflows') - this.setState(ClientToolCallState.error) - return - } - - const json = await res.json() - const workflows = Array.isArray(json?.data) ? json.data : [] - const names = extractWorkflowNames(workflows) - - logger.info('Found workflows', { count: names.length }) - - await this.markToolComplete(200, `Found ${names.length} workflow(s)`, { - workflow_names: names, - }) - this.setState(ClientToolCallState.success) - } catch (error: any) { - const message = error instanceof Error ? error.message : String(error) - await this.markToolComplete(500, message || 'Failed to list workflows') - this.setState(ClientToolCallState.error) - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/list-workspace-mcp-servers.ts b/apps/sim/lib/copilot/tools/client/workflow/list-workspace-mcp-servers.ts deleted file mode 100644 index 1dad9fbf7..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/list-workspace-mcp-servers.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Server, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface ListWorkspaceMcpServersArgs { - workspaceId?: string -} - -export interface WorkspaceMcpServer { - id: string - name: string - description: string | null - toolCount: number - toolNames: string[] -} - -/** - * List workspace MCP servers tool. - * Returns a list of MCP servers available in the workspace that workflows can be deployed to. - */ -export class ListWorkspaceMcpServersClientTool extends BaseClientTool { - static readonly id = 'list_workspace_mcp_servers' - - constructor(toolCallId: string) { - super( - toolCallId, - ListWorkspaceMcpServersClientTool.id, - ListWorkspaceMcpServersClientTool.metadata - ) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Getting MCP servers', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Getting MCP servers', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Getting MCP servers', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Retrieved MCP servers', icon: Server }, - [ClientToolCallState.error]: { text: 'Failed to get MCP servers', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted getting MCP servers', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped getting MCP servers', icon: XCircle }, - }, - interrupt: undefined, - } - - async execute(args?: ListWorkspaceMcpServersArgs): Promise { - const logger = createLogger('ListWorkspaceMcpServersClientTool') - try { - this.setState(ClientToolCallState.executing) - - // Get workspace ID from active workflow if not provided - const { activeWorkflowId, workflows } = useWorkflowRegistry.getState() - let workspaceId = args?.workspaceId - - if (!workspaceId && activeWorkflowId) { - workspaceId = workflows[activeWorkflowId]?.workspaceId - } - - if (!workspaceId) { - throw new Error('No workspace ID available') - } - - const res = await fetch(`/api/mcp/workflow-servers?workspaceId=${workspaceId}`) - - if (!res.ok) { - const data = await res.json().catch(() => ({})) - throw new Error(data.error || `Failed to fetch MCP servers (${res.status})`) - } - - const data = await res.json() - const servers: WorkspaceMcpServer[] = (data.data?.servers || []).map((s: any) => ({ - id: s.id, - name: s.name, - description: s.description, - toolCount: s.toolCount || 0, - toolNames: s.toolNames || [], - })) - - this.setState(ClientToolCallState.success) - - if (servers.length === 0) { - await this.markToolComplete( - 200, - 'No MCP servers found in this workspace. Use create_workspace_mcp_server to create one.', - { servers: [], count: 0 } - ) - } else { - await this.markToolComplete( - 200, - `Found ${servers.length} MCP server(s) in the workspace.`, - { - servers, - count: servers.length, - } - ) - } - - logger.info(`Listed ${servers.length} MCP servers`) - } catch (e: any) { - logger.error('Failed to list MCP servers', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to list MCP servers') - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts deleted file mode 100644 index 58a823637..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Check, Loader2, Plus, X, XCircle } from 'lucide-react' -import { client } from '@/lib/auth/auth-client' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { getCustomTool } from '@/hooks/queries/custom-tools' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface CustomToolSchema { - type: 'function' - function: { - name: string - description?: string - parameters: { - type: string - properties: Record - required?: string[] - } - } -} - -interface ManageCustomToolArgs { - operation: 'add' | 'edit' | 'delete' | 'list' - toolId?: string - schema?: CustomToolSchema - code?: string -} - -const API_ENDPOINT = '/api/tools/custom' - -async function checkCustomToolsPermission(): Promise { - const activeOrgResponse = await client.organization.getFullOrganization() - const organizationId = activeOrgResponse.data?.id - if (!organizationId) return - - const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`) - if (!response.ok) return - - const data = await response.json() - if (data?.config?.disableCustomTools) { - throw new Error('Custom tools are not allowed based on your permission group settings') - } -} - -/** - * Client tool for creating, editing, and deleting custom tools via the copilot. - */ -export class ManageCustomToolClientTool extends BaseClientTool { - static readonly id = 'manage_custom_tool' - private currentArgs?: ManageCustomToolArgs - - constructor(toolCallId: string) { - super(toolCallId, ManageCustomToolClientTool.id, ManageCustomToolClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Managing custom tool', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Manage custom tool?', icon: Plus }, - [ClientToolCallState.executing]: { text: 'Managing custom tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed custom tool', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to manage custom tool', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted managing custom tool', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped managing custom tool', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Allow', icon: Check }, - reject: { text: 'Skip', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined - - if (!operation) return undefined - - let toolName = params?.schema?.function?.name - if (!toolName && params?.toolId) { - try { - const tool = getCustomTool(params.toolId) - toolName = tool?.schema?.function?.name - } catch { - // Ignore errors accessing cache - } - } - - const getActionText = (verb: 'present' | 'past' | 'gerund') => { - switch (operation) { - case 'add': - return verb === 'present' ? 'Create' : verb === 'past' ? 'Created' : 'Creating' - case 'edit': - return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': - return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' - case 'list': - return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' - default: - return verb === 'present' ? 'Manage' : verb === 'past' ? 'Managed' : 'Managing' - } - } - - // For add: only show tool name in past tense (success) - // For edit/delete: always show tool name - // For list: never show individual tool name, use plural - const shouldShowToolName = (currentState: ClientToolCallState) => { - if (operation === 'list') return false - if (operation === 'add') { - return currentState === ClientToolCallState.success - } - return true // edit and delete always show tool name - } - - const nameText = - operation === 'list' - ? ' custom tools' - : shouldShowToolName(state) && toolName - ? ` ${toolName}` - : ' custom tool' - - switch (state) { - case ClientToolCallState.success: - return `${getActionText('past')}${nameText}` - case ClientToolCallState.executing: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.generating: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.pending: - return `${getActionText('present')}${nameText}?` - case ClientToolCallState.error: - return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` - case ClientToolCallState.aborted: - return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` - case ClientToolCallState.rejected: - return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` - } - return undefined - }, - } - - /** - * Gets the tool call args from the copilot store (needed before execute() is called) - */ - private getArgsFromStore(): ManageCustomToolArgs | undefined { - try { - const { toolCallsById } = useCopilotStore.getState() - const toolCall = toolCallsById[this.toolCallId] - return (toolCall as any)?.params as ManageCustomToolArgs | undefined - } catch { - return undefined - } - } - - /** - * Override getInterruptDisplays to only show confirmation for edit and delete operations. - * Add operations execute directly without confirmation. - */ - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const args = this.currentArgs || this.getArgsFromStore() - const operation = args?.operation - if (operation === 'edit' || operation === 'delete') { - return this.metadata.interrupt - } - return undefined - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: ManageCustomToolArgs): Promise { - const logger = createLogger('ManageCustomToolClientTool') - try { - this.setState(ClientToolCallState.executing) - await this.executeOperation(args, logger) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to manage custom tool', { - success: false, - error: e?.message || 'Failed to manage custom tool', - }) - } - } - - async execute(args?: ManageCustomToolArgs): Promise { - this.currentArgs = args - if (args?.operation === 'add' || args?.operation === 'list') { - await this.handleAccept(args) - } - } - - /** - * Executes the custom tool operation (add, edit, delete, or list) - */ - private async executeOperation( - args: ManageCustomToolArgs | undefined, - logger: ReturnType - ): Promise { - if (!args?.operation) { - throw new Error('Operation is required') - } - - await checkCustomToolsPermission() - - const { operation, toolId, schema, code } = args - - const { hydration } = useWorkflowRegistry.getState() - const workspaceId = hydration.workspaceId - if (!workspaceId) { - throw new Error('No active workspace found') - } - - logger.info(`Executing custom tool operation: ${operation}`, { - operation, - toolId, - functionName: schema?.function?.name, - workspaceId, - }) - - switch (operation) { - case 'add': - await this.addCustomTool({ schema, code, workspaceId }, logger) - break - case 'edit': - await this.editCustomTool({ toolId, schema, code, workspaceId }, logger) - break - case 'delete': - await this.deleteCustomTool({ toolId, workspaceId }, logger) - break - case 'list': - await this.markToolComplete(200, 'Listed custom tools') - break - default: - throw new Error(`Unknown operation: ${operation}`) - } - } - - /** - * Creates a new custom tool - */ - private async addCustomTool( - params: { - schema?: CustomToolSchema - code?: string - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { schema, code, workspaceId } = params - - if (!schema) { - throw new Error('Schema is required for adding a custom tool') - } - if (!code) { - throw new Error('Code is required for adding a custom tool') - } - - const functionName = schema.function.name - - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tools: [{ title: functionName, schema, code }], - workspaceId, - }), - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to create custom tool') - } - - if (!data.data || !Array.isArray(data.data) || data.data.length === 0) { - throw new Error('Invalid API response: missing tool data') - } - - const createdTool = data.data[0] - logger.info(`Created custom tool: ${functionName}`, { toolId: createdTool.id }) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Created custom tool "${functionName}"`, { - success: true, - operation: 'add', - toolId: createdTool.id, - functionName, - }) - } - - /** - * Updates an existing custom tool - */ - private async editCustomTool( - params: { - toolId?: string - schema?: CustomToolSchema - code?: string - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { toolId, schema, code, workspaceId } = params - - if (!toolId) { - throw new Error('Tool ID is required for editing a custom tool') - } - - if (!schema && !code) { - throw new Error('At least one of schema or code must be provided for editing') - } - - const existingResponse = await fetch(`${API_ENDPOINT}?workspaceId=${workspaceId}`) - const existingData = await existingResponse.json() - - if (!existingResponse.ok) { - throw new Error(existingData.error || 'Failed to fetch existing tools') - } - - const existingTool = existingData.data?.find((t: any) => t.id === toolId) - if (!existingTool) { - throw new Error(`Tool with ID ${toolId} not found`) - } - - const mergedSchema = schema ?? existingTool.schema - const updatedTool = { - id: toolId, - title: mergedSchema.function.name, - schema: mergedSchema, - code: code ?? existingTool.code, - } - - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tools: [updatedTool], - workspaceId, - }), - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to update custom tool') - } - - const functionName = updatedTool.schema.function.name - logger.info(`Updated custom tool: ${functionName}`, { toolId }) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Updated custom tool "${functionName}"`, { - success: true, - operation: 'edit', - toolId, - functionName, - }) - } - - /** - * Deletes a custom tool - */ - private async deleteCustomTool( - params: { - toolId?: string - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { toolId, workspaceId } = params - - if (!toolId) { - throw new Error('Tool ID is required for deleting a custom tool') - } - - const url = `${API_ENDPOINT}?id=${toolId}&workspaceId=${workspaceId}` - const response = await fetch(url, { - method: 'DELETE', - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to delete custom tool') - } - - logger.info(`Deleted custom tool: ${toolId}`) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Deleted custom tool`, { - success: true, - operation: 'delete', - toolId, - }) - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts deleted file mode 100644 index 796574dc1..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Check, Loader2, Server, X, XCircle } from 'lucide-react' -import { client } from '@/lib/auth/auth-client' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useCopilotStore } from '@/stores/panel/copilot/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface McpServerConfig { - name: string - transport: 'streamable-http' - url?: string - headers?: Record - timeout?: number - enabled?: boolean -} - -interface ManageMcpToolArgs { - operation: 'add' | 'edit' | 'delete' - serverId?: string - config?: McpServerConfig -} - -const API_ENDPOINT = '/api/mcp/servers' - -async function checkMcpToolsPermission(): Promise { - const activeOrgResponse = await client.organization.getFullOrganization() - const organizationId = activeOrgResponse.data?.id - if (!organizationId) return - - const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`) - if (!response.ok) return - - const data = await response.json() - if (data?.config?.disableMcpTools) { - throw new Error('MCP tools are not allowed based on your permission group settings') - } -} - -/** - * Client tool for creating, editing, and deleting MCP tool servers via the copilot. - */ -export class ManageMcpToolClientTool extends BaseClientTool { - static readonly id = 'manage_mcp_tool' - private currentArgs?: ManageMcpToolArgs - - constructor(toolCallId: string) { - super(toolCallId, ManageMcpToolClientTool.id, ManageMcpToolClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Managing MCP tool', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Manage MCP tool?', icon: Server }, - [ClientToolCallState.executing]: { text: 'Managing MCP tool', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed MCP tool', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to manage MCP tool', icon: X }, - [ClientToolCallState.aborted]: { - text: 'Aborted managing MCP tool', - icon: XCircle, - }, - [ClientToolCallState.rejected]: { - text: 'Skipped managing MCP tool', - icon: XCircle, - }, - }, - interrupt: { - accept: { text: 'Allow', icon: Check }, - reject: { text: 'Skip', icon: XCircle }, - }, - getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined - - if (!operation) return undefined - - const serverName = params?.config?.name || params?.serverName - - const getActionText = (verb: 'present' | 'past' | 'gerund') => { - switch (operation) { - case 'add': - return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' - case 'edit': - return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': - return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' - } - } - - const shouldShowServerName = (currentState: ClientToolCallState) => { - if (operation === 'add') { - return currentState === ClientToolCallState.success - } - return true - } - - const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' - - switch (state) { - case ClientToolCallState.success: - return `${getActionText('past')}${nameText}` - case ClientToolCallState.executing: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.generating: - return `${getActionText('gerund')}${nameText}` - case ClientToolCallState.pending: - return `${getActionText('present')}${nameText}?` - case ClientToolCallState.error: - return `Failed to ${getActionText('present')?.toLowerCase()}${nameText}` - case ClientToolCallState.aborted: - return `Aborted ${getActionText('gerund')?.toLowerCase()}${nameText}` - case ClientToolCallState.rejected: - return `Skipped ${getActionText('gerund')?.toLowerCase()}${nameText}` - } - return undefined - }, - } - - /** - * Gets the tool call args from the copilot store (needed before execute() is called) - */ - private getArgsFromStore(): ManageMcpToolArgs | undefined { - try { - const { toolCallsById } = useCopilotStore.getState() - const toolCall = toolCallsById[this.toolCallId] - return (toolCall as any)?.params as ManageMcpToolArgs | undefined - } catch { - return undefined - } - } - - /** - * Override getInterruptDisplays to only show confirmation for edit and delete operations. - * Add operations execute directly without confirmation. - */ - getInterruptDisplays(): BaseClientToolMetadata['interrupt'] | undefined { - const args = this.currentArgs || this.getArgsFromStore() - const operation = args?.operation - if (operation === 'edit' || operation === 'delete') { - return this.metadata.interrupt - } - return undefined - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: ManageMcpToolArgs): Promise { - const logger = createLogger('ManageMcpToolClientTool') - try { - this.setState(ClientToolCallState.executing) - await this.executeOperation(args, logger) - } catch (e: any) { - logger.error('execute failed', { message: e?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool', { - success: false, - error: e?.message || 'Failed to manage MCP tool', - }) - } - } - - async execute(args?: ManageMcpToolArgs): Promise { - this.currentArgs = args - if (args?.operation === 'add') { - await this.handleAccept(args) - } - } - - /** - * Executes the MCP tool operation (add, edit, or delete) - */ - private async executeOperation( - args: ManageMcpToolArgs | undefined, - logger: ReturnType - ): Promise { - if (!args?.operation) { - throw new Error('Operation is required') - } - - await checkMcpToolsPermission() - - const { operation, serverId, config } = args - - const { hydration } = useWorkflowRegistry.getState() - const workspaceId = hydration.workspaceId - if (!workspaceId) { - throw new Error('No active workspace found') - } - - logger.info(`Executing MCP tool operation: ${operation}`, { - operation, - serverId, - serverName: config?.name, - workspaceId, - }) - - switch (operation) { - case 'add': - await this.addMcpServer({ config, workspaceId }, logger) - break - case 'edit': - await this.editMcpServer({ serverId, config, workspaceId }, logger) - break - case 'delete': - await this.deleteMcpServer({ serverId, workspaceId }, logger) - break - default: - throw new Error(`Unknown operation: ${operation}`) - } - } - - /** - * Creates a new MCP server - */ - private async addMcpServer( - params: { - config?: McpServerConfig - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { config, workspaceId } = params - - if (!config) { - throw new Error('Config is required for adding an MCP tool') - } - if (!config.name) { - throw new Error('Server name is required') - } - if (!config.url) { - throw new Error('Server URL is required for streamable-http transport') - } - - const serverData = { - ...config, - workspaceId, - transport: config.transport || 'streamable-http', - timeout: config.timeout || 30000, - enabled: config.enabled !== false, - } - - const response = await fetch(API_ENDPOINT, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(serverData), - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to create MCP tool') - } - - const serverId = data.data?.serverId - logger.info(`Created MCP tool: ${config.name}`, { serverId }) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Created MCP tool "${config.name}"`, { - success: true, - operation: 'add', - serverId, - serverName: config.name, - }) - } - - /** - * Updates an existing MCP server - */ - private async editMcpServer( - params: { - serverId?: string - config?: McpServerConfig - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { serverId, config, workspaceId } = params - - if (!serverId) { - throw new Error('Server ID is required for editing an MCP tool') - } - - if (!config) { - throw new Error('Config is required for editing an MCP tool') - } - - const updateData = { - ...config, - workspaceId, - } - - const response = await fetch(`${API_ENDPOINT}/${serverId}?workspaceId=${workspaceId}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updateData), - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to update MCP tool') - } - - const serverName = config.name || data.data?.server?.name || serverId - logger.info(`Updated MCP tool: ${serverName}`, { serverId }) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Updated MCP tool "${serverName}"`, { - success: true, - operation: 'edit', - serverId, - serverName, - }) - } - - /** - * Deletes an MCP server - */ - private async deleteMcpServer( - params: { - serverId?: string - workspaceId: string - }, - logger: ReturnType - ): Promise { - const { serverId, workspaceId } = params - - if (!serverId) { - throw new Error('Server ID is required for deleting an MCP tool') - } - - const url = `${API_ENDPOINT}?serverId=${serverId}&workspaceId=${workspaceId}` - const response = await fetch(url, { - method: 'DELETE', - }) - - const data = await response.json() - - if (!response.ok) { - throw new Error(data.error || 'Failed to delete MCP tool') - } - - logger.info(`Deleted MCP tool: ${serverId}`) - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, `Deleted MCP tool`, { - success: true, - operation: 'delete', - serverId, - }) - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/redeploy.ts b/apps/sim/lib/copilot/tools/client/workflow/redeploy.ts deleted file mode 100644 index 2fef023fb..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/redeploy.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Rocket, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -export class RedeployClientTool extends BaseClientTool { - static readonly id = 'redeploy' - private hasExecuted = false - - constructor(toolCallId: string) { - super(toolCallId, RedeployClientTool.id, RedeployClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Redeploying workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Redeploy workflow', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Redeploying workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Redeployed workflow', icon: Rocket }, - [ClientToolCallState.error]: { text: 'Failed to redeploy workflow', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted redeploy', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped redeploy', icon: XCircle }, - }, - interrupt: undefined, - } - - async execute(): Promise { - const logger = createLogger('RedeployClientTool') - try { - if (this.hasExecuted) { - logger.info('execute skipped (already executed)', { toolCallId: this.toolCallId }) - return - } - this.hasExecuted = true - - this.setState(ClientToolCallState.executing) - - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (!activeWorkflowId) { - throw new Error('No workflow ID provided') - } - - const res = await fetch(`/api/workflows/${activeWorkflowId}/deploy`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ deployChatEnabled: false }), - }) - - const json = await res.json().catch(() => ({})) - if (!res.ok) { - const errorText = json?.error || `Server error (${res.status})` - throw new Error(errorText) - } - - this.setState(ClientToolCallState.success) - await this.markToolComplete(200, 'Workflow redeployed', { - workflowId: activeWorkflowId, - deployedAt: json?.deployedAt || null, - schedule: json?.schedule, - }) - } catch (error: any) { - logger.error('Redeploy failed', { message: error?.message }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, error?.message || 'Failed to redeploy workflow') - } - } -} diff --git a/apps/sim/lib/copilot/tools/client/workflow/run-workflow.ts b/apps/sim/lib/copilot/tools/client/workflow/run-workflow.ts deleted file mode 100644 index 3b2c89df6..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/run-workflow.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, MinusCircle, Play, XCircle } from 'lucide-react' -import { v4 as uuidv4 } from 'uuid' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, - WORKFLOW_EXECUTION_TIMEOUT_MS, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { executeWorkflowWithFullLogging } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' -import { useExecutionStore } from '@/stores/execution' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface RunWorkflowArgs { - workflowId?: string - description?: string - workflow_input?: Record -} - -export class RunWorkflowClientTool extends BaseClientTool { - static readonly id = 'run_workflow' - - constructor(toolCallId: string) { - super(toolCallId, RunWorkflowClientTool.id, RunWorkflowClientTool.metadata) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { text: 'Preparing to run your workflow', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Run this workflow?', icon: Play }, - [ClientToolCallState.executing]: { text: 'Running your workflow', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Executed workflow', icon: Play }, - [ClientToolCallState.error]: { text: 'Errored running workflow', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped workflow execution', icon: MinusCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted workflow execution', icon: MinusCircle }, - [ClientToolCallState.background]: { text: 'Running in background', icon: Play }, - }, - interrupt: { - accept: { text: 'Run', icon: Play }, - reject: { text: 'Skip', icon: MinusCircle }, - }, - uiConfig: { - isSpecial: true, - interrupt: { - accept: { text: 'Run', icon: Play }, - reject: { text: 'Skip', icon: MinusCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - secondaryAction: { - text: 'Move to Background', - title: 'Move to Background', - variant: 'tertiary', - showInStates: [ClientToolCallState.executing], - completionMessage: - 'The user has chosen to move the workflow execution to the background. Check back with them later to know when the workflow execution is complete', - targetState: ClientToolCallState.background, - }, - paramsTable: { - columns: [ - { key: 'input', label: 'Input', width: '36%' }, - { key: 'value', label: 'Value', width: '64%', editable: true, mono: true }, - ], - extractRows: (params) => { - let inputs = params.input || params.inputs || params.workflow_input - if (typeof inputs === 'string') { - try { - inputs = JSON.parse(inputs) - } catch { - inputs = {} - } - } - if (params.workflow_input && typeof params.workflow_input === 'object') { - inputs = params.workflow_input - } - if (!inputs || typeof inputs !== 'object') { - const { workflowId, workflow_input, ...rest } = params - inputs = rest - } - const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} - return Object.entries(safeInputs).map(([key, value]) => [key, key, String(value)]) - }, - }, - }, - getDynamicText: (params, state) => { - const workflowId = params?.workflowId || useWorkflowRegistry.getState().activeWorkflowId - if (workflowId) { - const workflowName = useWorkflowRegistry.getState().workflows[workflowId]?.name - if (workflowName) { - switch (state) { - case ClientToolCallState.success: - return `Ran ${workflowName}` - case ClientToolCallState.executing: - return `Running ${workflowName}` - case ClientToolCallState.generating: - return `Preparing to run ${workflowName}` - case ClientToolCallState.pending: - return `Run ${workflowName}?` - case ClientToolCallState.error: - return `Failed to run ${workflowName}` - case ClientToolCallState.rejected: - return `Skipped running ${workflowName}` - case ClientToolCallState.aborted: - return `Aborted running ${workflowName}` - case ClientToolCallState.background: - return `Running ${workflowName} in background` - } - } - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: RunWorkflowArgs): Promise { - const logger = createLogger('RunWorkflowClientTool') - - // Use longer timeout for workflow execution (10 minutes) - await this.executeWithTimeout(async () => { - const params = args || {} - logger.debug('handleAccept() called', { - toolCallId: this.toolCallId, - state: this.getState(), - hasArgs: !!args, - argKeys: args ? Object.keys(args) : [], - }) - - // prevent concurrent execution - const { isExecuting, setIsExecuting } = useExecutionStore.getState() - if (isExecuting) { - logger.debug('Execution prevented: already executing') - this.setState(ClientToolCallState.error) - await this.markToolComplete( - 409, - 'The workflow is already in the middle of an execution. Try again later' - ) - return - } - - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (!activeWorkflowId) { - logger.debug('Execution prevented: no active workflow') - this.setState(ClientToolCallState.error) - await this.markToolComplete(400, 'No active workflow found') - return - } - logger.debug('Using active workflow', { activeWorkflowId }) - - const workflowInput = params.workflow_input || undefined - if (workflowInput) { - logger.debug('Workflow input provided', { - inputFields: Object.keys(workflowInput), - inputPreview: JSON.stringify(workflowInput).slice(0, 120), - }) - } - - setIsExecuting(true) - logger.debug('Set isExecuting(true) and switching state to executing') - this.setState(ClientToolCallState.executing) - - const executionId = uuidv4() - const executionStartTime = new Date().toISOString() - logger.debug('Starting workflow execution', { - executionStartTime, - executionId, - toolCallId: this.toolCallId, - }) - - try { - const result = await executeWorkflowWithFullLogging({ - workflowInput, - executionId, - }) - - // Determine success for both non-streaming and streaming executions - let succeeded = true - let errorMessage: string | undefined - try { - if (result && typeof result === 'object' && 'success' in (result as any)) { - succeeded = Boolean((result as any).success) - if (!succeeded) { - errorMessage = (result as any)?.error || (result as any)?.output?.error - } - } else if ( - result && - typeof result === 'object' && - 'execution' in (result as any) && - (result as any).execution && - typeof (result as any).execution === 'object' - ) { - succeeded = Boolean((result as any).execution.success) - if (!succeeded) { - errorMessage = - (result as any).execution?.error || (result as any).execution?.output?.error - } - } - } catch {} - - if (succeeded) { - logger.debug('Workflow execution finished with success') - this.setState(ClientToolCallState.success) - await this.markToolComplete( - 200, - `Workflow execution completed. Started at: ${executionStartTime}` - ) - } else { - const msg = errorMessage || 'Workflow execution failed' - logger.error('Workflow execution finished with failure', { message: msg }) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, msg) - } - } finally { - // Always clean up execution state - setIsExecuting(false) - } - }, WORKFLOW_EXECUTION_TIMEOUT_MS) - } - - async execute(args?: RunWorkflowArgs): Promise { - // For compatibility if execute() is explicitly invoked, route to handleAccept - await this.handleAccept(args) - } -} - -// Register UI config at module load -registerToolUIConfig(RunWorkflowClientTool.id, RunWorkflowClientTool.metadata.uiConfig!) diff --git a/apps/sim/lib/copilot/tools/client/workflow/set-global-workflow-variables.ts b/apps/sim/lib/copilot/tools/client/workflow/set-global-workflow-variables.ts deleted file mode 100644 index 63f4c6c6f..000000000 --- a/apps/sim/lib/copilot/tools/client/workflow/set-global-workflow-variables.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { createLogger } from '@sim/logger' -import { Loader2, Settings2, X, XCircle } from 'lucide-react' -import { - BaseClientTool, - type BaseClientToolMetadata, - ClientToolCallState, -} from '@/lib/copilot/tools/client/base-tool' -import { registerToolUIConfig } from '@/lib/copilot/tools/client/ui-config' -import { useVariablesStore } from '@/stores/panel/variables/store' -import { useWorkflowRegistry } from '@/stores/workflows/registry/store' - -interface OperationItem { - operation: 'add' | 'edit' | 'delete' - name: string - type?: 'plain' | 'number' | 'boolean' | 'array' | 'object' - value?: string -} - -interface SetGlobalVarsArgs { - operations: OperationItem[] - workflowId?: string -} - -export class SetGlobalWorkflowVariablesClientTool extends BaseClientTool { - static readonly id = 'set_global_workflow_variables' - - constructor(toolCallId: string) { - super( - toolCallId, - SetGlobalWorkflowVariablesClientTool.id, - SetGlobalWorkflowVariablesClientTool.metadata - ) - } - - static readonly metadata: BaseClientToolMetadata = { - displayNames: { - [ClientToolCallState.generating]: { - text: 'Preparing to set workflow variables', - icon: Loader2, - }, - [ClientToolCallState.pending]: { text: 'Set workflow variables?', icon: Settings2 }, - [ClientToolCallState.executing]: { text: 'Setting workflow variables', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Updated workflow variables', icon: Settings2 }, - [ClientToolCallState.error]: { text: 'Failed to set workflow variables', icon: X }, - [ClientToolCallState.aborted]: { text: 'Aborted setting variables', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped setting variables', icon: XCircle }, - }, - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - }, - uiConfig: { - interrupt: { - accept: { text: 'Apply', icon: Settings2 }, - reject: { text: 'Skip', icon: XCircle }, - showAllowOnce: true, - showAllowAlways: true, - }, - paramsTable: { - columns: [ - { key: 'name', label: 'Name', width: '40%', editable: true, mono: true }, - { key: 'value', label: 'Value', width: '60%', editable: true, mono: true }, - ], - extractRows: (params) => { - const operations = params.operations || [] - return operations.map((op: any, idx: number) => [ - String(idx), - op.name || '', - String(op.value ?? ''), - ]) - }, - }, - }, - getDynamicText: (params, state) => { - if (params?.operations && Array.isArray(params.operations)) { - const varNames = params.operations - .slice(0, 2) - .map((op: any) => op.name) - .filter(Boolean) - - if (varNames.length > 0) { - const varList = varNames.join(', ') - const more = params.operations.length > 2 ? '...' : '' - const displayText = `${varList}${more}` - - switch (state) { - case ClientToolCallState.success: - return `Set ${displayText}` - case ClientToolCallState.executing: - return `Setting ${displayText}` - case ClientToolCallState.generating: - return `Preparing to set ${displayText}` - case ClientToolCallState.pending: - return `Set ${displayText}?` - case ClientToolCallState.error: - return `Failed to set ${displayText}` - case ClientToolCallState.aborted: - return `Aborted setting ${displayText}` - case ClientToolCallState.rejected: - return `Skipped setting ${displayText}` - } - } - } - return undefined - }, - } - - async handleReject(): Promise { - await super.handleReject() - this.setState(ClientToolCallState.rejected) - } - - async handleAccept(args?: SetGlobalVarsArgs): Promise { - const logger = createLogger('SetGlobalWorkflowVariablesClientTool') - try { - this.setState(ClientToolCallState.executing) - const payload: SetGlobalVarsArgs = { ...(args || { operations: [] }) } - if (!payload.workflowId) { - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (activeWorkflowId) payload.workflowId = activeWorkflowId - } - if (!payload.workflowId) { - throw new Error('No active workflow found') - } - - // Fetch current variables so we can construct full array payload - const getRes = await fetch(`/api/workflows/${payload.workflowId}/variables`, { - method: 'GET', - }) - if (!getRes.ok) { - const txt = await getRes.text().catch(() => '') - throw new Error(txt || 'Failed to load current variables') - } - const currentJson = await getRes.json() - const currentVarsRecord = (currentJson?.data as Record) || {} - - // Helper to convert string -> typed value - function coerceValue( - value: string | undefined, - type?: 'plain' | 'number' | 'boolean' | 'array' | 'object' - ) { - if (value === undefined) return value - const t = type || 'plain' - try { - if (t === 'number') { - const n = Number(value) - if (Number.isNaN(n)) return value - return n - } - if (t === 'boolean') { - const v = String(value).trim().toLowerCase() - if (v === 'true') return true - if (v === 'false') return false - return value - } - if (t === 'array' || t === 'object') { - const parsed = JSON.parse(value) - if (t === 'array' && Array.isArray(parsed)) return parsed - if (t === 'object' && parsed && typeof parsed === 'object' && !Array.isArray(parsed)) - return parsed - return value - } - } catch {} - return value - } - - // Build mutable map by variable name - const byName: Record = {} - Object.values(currentVarsRecord).forEach((v: any) => { - if (v && typeof v === 'object' && v.id && v.name) byName[String(v.name)] = v - }) - - // Apply operations in order - for (const op of payload.operations || []) { - const key = String(op.name) - const nextType = (op.type as any) || byName[key]?.type || 'plain' - if (op.operation === 'delete') { - delete byName[key] - continue - } - const typedValue = coerceValue(op.value, nextType) - if (op.operation === 'add') { - byName[key] = { - id: crypto.randomUUID(), - workflowId: payload.workflowId, - name: key, - type: nextType, - value: typedValue, - } - continue - } - if (op.operation === 'edit') { - if (!byName[key]) { - // If editing a non-existent variable, create it - byName[key] = { - id: crypto.randomUUID(), - workflowId: payload.workflowId, - name: key, - type: nextType, - value: typedValue, - } - } else { - byName[key] = { - ...byName[key], - type: nextType, - ...(op.value !== undefined ? { value: typedValue } : {}), - } - } - } - } - - // Convert byName (keyed by name) to record keyed by ID for the API - const variablesRecord: Record = {} - for (const v of Object.values(byName)) { - variablesRecord[v.id] = v - } - - // POST full variables record to persist - const res = await fetch(`/api/workflows/${payload.workflowId}/variables`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ variables: variablesRecord }), - }) - if (!res.ok) { - const txt = await res.text().catch(() => '') - throw new Error(txt || `Failed to update variables (${res.status})`) - } - - try { - const { activeWorkflowId } = useWorkflowRegistry.getState() - if (activeWorkflowId) { - // Fetch the updated variables from the API - const refreshRes = await fetch(`/api/workflows/${activeWorkflowId}/variables`, { - method: 'GET', - }) - - if (refreshRes.ok) { - const refreshJson = await refreshRes.json() - const updatedVarsRecord = (refreshJson?.data as Record) || {} - - // Update the variables store with the fresh data - useVariablesStore.setState((state) => { - // Remove old variables for this workflow - const withoutWorkflow = Object.fromEntries( - Object.entries(state.variables).filter(([, v]) => v.workflowId !== activeWorkflowId) - ) - // Add the updated variables - return { - variables: { ...withoutWorkflow, ...updatedVarsRecord }, - } - }) - - logger.info('Refreshed variables in store', { workflowId: activeWorkflowId }) - } - } - } catch (refreshError) { - logger.warn('Failed to refresh variables in store', { error: refreshError }) - } - - await this.markToolComplete(200, 'Workflow variables updated', { variables: byName }) - this.setState(ClientToolCallState.success) - } catch (e: any) { - const message = e instanceof Error ? e.message : String(e) - this.setState(ClientToolCallState.error) - await this.markToolComplete(500, message || 'Failed to set workflow variables') - } - } - - async execute(args?: SetGlobalVarsArgs): Promise { - await this.handleAccept(args) - } -} - -// Register UI config at module load -registerToolUIConfig( - SetGlobalWorkflowVariablesClientTool.id, - SetGlobalWorkflowVariablesClientTool.metadata.uiConfig! -) diff --git a/apps/sim/stores/panel/copilot/store.ts b/apps/sim/stores/panel/copilot/store.ts index be32d1c72..4bf168593 100644 --- a/apps/sim/stores/panel/copilot/store.ts +++ b/apps/sim/stores/panel/copilot/store.ts @@ -10,76 +10,11 @@ import { shouldSkipToolCallEvent, shouldSkipToolResultEvent, } from '@/lib/copilot/orchestrator/sse-utils' -import type { - BaseClientToolMetadata, - ClientToolDisplay, -} from '@/lib/copilot/tools/client/base-tool' -import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' -import { GetBlockConfigClientTool } from '@/lib/copilot/tools/client/blocks/get-block-config' -import { GetBlockOptionsClientTool } from '@/lib/copilot/tools/client/blocks/get-block-options' -import { GetBlocksAndToolsClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-and-tools' -import { GetBlocksMetadataClientTool } from '@/lib/copilot/tools/client/blocks/get-blocks-metadata' -import { GetTriggerBlocksClientTool } from '@/lib/copilot/tools/client/blocks/get-trigger-blocks' -import { GetExamplesRagClientTool } from '@/lib/copilot/tools/client/examples/get-examples-rag' -import { GetOperationsExamplesClientTool } from '@/lib/copilot/tools/client/examples/get-operations-examples' -import { GetTriggerExamplesClientTool } from '@/lib/copilot/tools/client/examples/get-trigger-examples' -import { SummarizeClientTool } from '@/lib/copilot/tools/client/examples/summarize' -import { KnowledgeBaseClientTool } from '@/lib/copilot/tools/client/knowledge/knowledge-base' import { - getClientTool, - registerClientTool, - registerToolStateSync, -} from '@/lib/copilot/tools/client/manager' -import { NavigateUIClientTool } from '@/lib/copilot/tools/client/navigation/navigate-ui' -import { AuthClientTool } from '@/lib/copilot/tools/client/other/auth' -import { CheckoffTodoClientTool } from '@/lib/copilot/tools/client/other/checkoff-todo' -import { CrawlWebsiteClientTool } from '@/lib/copilot/tools/client/other/crawl-website' -import { CustomToolClientTool } from '@/lib/copilot/tools/client/other/custom-tool' -import { DebugClientTool } from '@/lib/copilot/tools/client/other/debug' -import { DeployClientTool } from '@/lib/copilot/tools/client/other/deploy' -import { EditClientTool } from '@/lib/copilot/tools/client/other/edit' -import { EvaluateClientTool } from '@/lib/copilot/tools/client/other/evaluate' -import { GetPageContentsClientTool } from '@/lib/copilot/tools/client/other/get-page-contents' -import { InfoClientTool } from '@/lib/copilot/tools/client/other/info' -import { KnowledgeClientTool } from '@/lib/copilot/tools/client/other/knowledge' -import { MakeApiRequestClientTool } from '@/lib/copilot/tools/client/other/make-api-request' -import { MarkTodoInProgressClientTool } from '@/lib/copilot/tools/client/other/mark-todo-in-progress' -import { OAuthRequestAccessClientTool } from '@/lib/copilot/tools/client/other/oauth-request-access' -import { PlanClientTool } from '@/lib/copilot/tools/client/other/plan' -import { RememberDebugClientTool } from '@/lib/copilot/tools/client/other/remember-debug' -import { ResearchClientTool } from '@/lib/copilot/tools/client/other/research' -import { ScrapePageClientTool } from '@/lib/copilot/tools/client/other/scrape-page' -import { SearchDocumentationClientTool } from '@/lib/copilot/tools/client/other/search-documentation' -import { SearchErrorsClientTool } from '@/lib/copilot/tools/client/other/search-errors' -import { SearchLibraryDocsClientTool } from '@/lib/copilot/tools/client/other/search-library-docs' -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 { TestClientTool } from '@/lib/copilot/tools/client/other/test' -import { TourClientTool } from '@/lib/copilot/tools/client/other/tour' -import { WorkflowClientTool } from '@/lib/copilot/tools/client/other/workflow' -import { 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' -import { CheckDeploymentStatusClientTool } from '@/lib/copilot/tools/client/workflow/check-deployment-status' -import { CreateWorkspaceMcpServerClientTool } from '@/lib/copilot/tools/client/workflow/create-workspace-mcp-server' -import { DeployApiClientTool } from '@/lib/copilot/tools/client/workflow/deploy-api' -import { DeployChatClientTool } from '@/lib/copilot/tools/client/workflow/deploy-chat' -import { DeployMcpClientTool } from '@/lib/copilot/tools/client/workflow/deploy-mcp' -import { EditWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/edit-workflow' -import { GetBlockOutputsClientTool } from '@/lib/copilot/tools/client/workflow/get-block-outputs' -import { GetBlockUpstreamReferencesClientTool } from '@/lib/copilot/tools/client/workflow/get-block-upstream-references' -import { GetUserWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/get-user-workflow' -import { GetWorkflowConsoleClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-console' -import { GetWorkflowDataClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-data' -import { GetWorkflowFromNameClientTool } from '@/lib/copilot/tools/client/workflow/get-workflow-from-name' -import { ListUserWorkflowsClientTool } from '@/lib/copilot/tools/client/workflow/list-user-workflows' -import { ListWorkspaceMcpServersClientTool } from '@/lib/copilot/tools/client/workflow/list-workspace-mcp-servers' -import { ManageCustomToolClientTool } from '@/lib/copilot/tools/client/workflow/manage-custom-tool' -import { ManageMcpToolClientTool } from '@/lib/copilot/tools/client/workflow/manage-mcp-tool' -import { RedeployClientTool } from '@/lib/copilot/tools/client/workflow/redeploy' -import { RunWorkflowClientTool } from '@/lib/copilot/tools/client/workflow/run-workflow' -import { SetGlobalWorkflowVariablesClientTool } from '@/lib/copilot/tools/client/workflow/set-global-workflow-variables' + ClientToolCallState, + type ClientToolDisplay, + TOOL_DISPLAY_REGISTRY, +} from '@/lib/copilot/tools/client/tool-display-registry' import { getQueryClient } from '@/app/_shell/providers/query-provider' import { subscriptionKeys } from '@/hooks/queries/subscription' import type { @@ -175,144 +110,6 @@ try { } } catch {} -// Known class-based client tools: map tool name -> instantiator -const CLIENT_TOOL_INSTANTIATORS: Record any> = { - plan: (id) => new PlanClientTool(id), - edit: (id) => new EditClientTool(id), - debug: (id) => new DebugClientTool(id), - test: (id) => new TestClientTool(id), - deploy: (id) => new DeployClientTool(id), - evaluate: (id) => new EvaluateClientTool(id), - auth: (id) => new AuthClientTool(id), - research: (id) => new ResearchClientTool(id), - knowledge: (id) => new KnowledgeClientTool(id), - custom_tool: (id) => new CustomToolClientTool(id), - tour: (id) => new TourClientTool(id), - info: (id) => new InfoClientTool(id), - workflow: (id) => new WorkflowClientTool(id), - run_workflow: (id) => new RunWorkflowClientTool(id), - get_workflow_console: (id) => new GetWorkflowConsoleClientTool(id), - get_blocks_and_tools: (id) => new GetBlocksAndToolsClientTool(id), - get_blocks_metadata: (id) => new GetBlocksMetadataClientTool(id), - get_block_options: (id) => new GetBlockOptionsClientTool(id), - get_block_config: (id) => new GetBlockConfigClientTool(id), - get_trigger_blocks: (id) => new GetTriggerBlocksClientTool(id), - search_online: (id) => new SearchOnlineClientTool(id), - search_documentation: (id) => new SearchDocumentationClientTool(id), - search_library_docs: (id) => new SearchLibraryDocsClientTool(id), - search_patterns: (id) => new SearchPatternsClientTool(id), - search_errors: (id) => new SearchErrorsClientTool(id), - scrape_page: (id) => new ScrapePageClientTool(id), - get_page_contents: (id) => new GetPageContentsClientTool(id), - crawl_website: (id) => new CrawlWebsiteClientTool(id), - remember_debug: (id) => new RememberDebugClientTool(id), - set_environment_variables: (id) => new SetEnvironmentVariablesClientTool(id), - get_credentials: (id) => new GetCredentialsClientTool(id), - knowledge_base: (id) => new KnowledgeBaseClientTool(id), - make_api_request: (id) => new MakeApiRequestClientTool(id), - checkoff_todo: (id) => new CheckoffTodoClientTool(id), - mark_todo_in_progress: (id) => new MarkTodoInProgressClientTool(id), - oauth_request_access: (id) => new OAuthRequestAccessClientTool(id), - edit_workflow: (id) => new EditWorkflowClientTool(id), - get_user_workflow: (id) => new GetUserWorkflowClientTool(id), - list_user_workflows: (id) => new ListUserWorkflowsClientTool(id), - get_workflow_from_name: (id) => new GetWorkflowFromNameClientTool(id), - get_workflow_data: (id) => new GetWorkflowDataClientTool(id), - set_global_workflow_variables: (id) => new SetGlobalWorkflowVariablesClientTool(id), - get_trigger_examples: (id) => new GetTriggerExamplesClientTool(id), - get_examples_rag: (id) => new GetExamplesRagClientTool(id), - get_operations_examples: (id) => new GetOperationsExamplesClientTool(id), - summarize_conversation: (id) => new SummarizeClientTool(id), - deploy_api: (id) => new DeployApiClientTool(id), - deploy_chat: (id) => new DeployChatClientTool(id), - deploy_mcp: (id) => new DeployMcpClientTool(id), - redeploy: (id) => new RedeployClientTool(id), - list_workspace_mcp_servers: (id) => new ListWorkspaceMcpServersClientTool(id), - create_workspace_mcp_server: (id) => new CreateWorkspaceMcpServerClientTool(id), - check_deployment_status: (id) => new CheckDeploymentStatusClientTool(id), - 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), - get_block_outputs: (id) => new GetBlockOutputsClientTool(id), - get_block_upstream_references: (id) => new GetBlockUpstreamReferencesClientTool(id), -} - -// Read-only static metadata for class-based tools (no instances) -export const CLASS_TOOL_METADATA: Record = { - plan: (PlanClientTool as any)?.metadata, - edit: (EditClientTool as any)?.metadata, - debug: (DebugClientTool as any)?.metadata, - test: (TestClientTool as any)?.metadata, - deploy: (DeployClientTool as any)?.metadata, - evaluate: (EvaluateClientTool as any)?.metadata, - auth: (AuthClientTool as any)?.metadata, - research: (ResearchClientTool as any)?.metadata, - knowledge: (KnowledgeClientTool as any)?.metadata, - custom_tool: (CustomToolClientTool as any)?.metadata, - tour: (TourClientTool as any)?.metadata, - info: (InfoClientTool as any)?.metadata, - workflow: (WorkflowClientTool as any)?.metadata, - run_workflow: (RunWorkflowClientTool as any)?.metadata, - get_workflow_console: (GetWorkflowConsoleClientTool as any)?.metadata, - get_blocks_and_tools: (GetBlocksAndToolsClientTool as any)?.metadata, - get_blocks_metadata: (GetBlocksMetadataClientTool as any)?.metadata, - get_block_options: (GetBlockOptionsClientTool as any)?.metadata, - get_block_config: (GetBlockConfigClientTool as any)?.metadata, - get_trigger_blocks: (GetTriggerBlocksClientTool as any)?.metadata, - search_online: (SearchOnlineClientTool as any)?.metadata, - search_documentation: (SearchDocumentationClientTool as any)?.metadata, - search_library_docs: (SearchLibraryDocsClientTool as any)?.metadata, - search_patterns: (SearchPatternsClientTool as any)?.metadata, - search_errors: (SearchErrorsClientTool as any)?.metadata, - scrape_page: (ScrapePageClientTool as any)?.metadata, - get_page_contents: (GetPageContentsClientTool as any)?.metadata, - crawl_website: (CrawlWebsiteClientTool as any)?.metadata, - remember_debug: (RememberDebugClientTool as any)?.metadata, - set_environment_variables: (SetEnvironmentVariablesClientTool as any)?.metadata, - get_credentials: (GetCredentialsClientTool as any)?.metadata, - knowledge_base: (KnowledgeBaseClientTool as any)?.metadata, - make_api_request: (MakeApiRequestClientTool as any)?.metadata, - checkoff_todo: (CheckoffTodoClientTool as any)?.metadata, - mark_todo_in_progress: (MarkTodoInProgressClientTool as any)?.metadata, - edit_workflow: (EditWorkflowClientTool as any)?.metadata, - get_user_workflow: (GetUserWorkflowClientTool as any)?.metadata, - list_user_workflows: (ListUserWorkflowsClientTool as any)?.metadata, - get_workflow_from_name: (GetWorkflowFromNameClientTool as any)?.metadata, - get_workflow_data: (GetWorkflowDataClientTool as any)?.metadata, - set_global_workflow_variables: (SetGlobalWorkflowVariablesClientTool as any)?.metadata, - get_trigger_examples: (GetTriggerExamplesClientTool as any)?.metadata, - get_examples_rag: (GetExamplesRagClientTool as any)?.metadata, - oauth_request_access: (OAuthRequestAccessClientTool as any)?.metadata, - get_operations_examples: (GetOperationsExamplesClientTool as any)?.metadata, - summarize_conversation: (SummarizeClientTool as any)?.metadata, - deploy_api: (DeployApiClientTool as any)?.metadata, - deploy_chat: (DeployChatClientTool as any)?.metadata, - deploy_mcp: (DeployMcpClientTool as any)?.metadata, - redeploy: (RedeployClientTool as any)?.metadata, - list_workspace_mcp_servers: (ListWorkspaceMcpServersClientTool as any)?.metadata, - create_workspace_mcp_server: (CreateWorkspaceMcpServerClientTool as any)?.metadata, - check_deployment_status: (CheckDeploymentStatusClientTool as any)?.metadata, - navigate_ui: (NavigateUIClientTool as any)?.metadata, - manage_custom_tool: (ManageCustomToolClientTool as any)?.metadata, - manage_mcp_tool: (ManageMcpToolClientTool as any)?.metadata, - sleep: (SleepClientTool as any)?.metadata, - get_block_outputs: (GetBlockOutputsClientTool as any)?.metadata, - get_block_upstream_references: (GetBlockUpstreamReferencesClientTool as any)?.metadata, -} - -function ensureClientToolInstance(toolName: string | undefined, toolCallId: string | undefined) { - try { - if (!toolName || !toolCallId) return - if (getClientTool(toolCallId)) return - const make = CLIENT_TOOL_INSTANTIATORS[toolName] - if (make) { - const inst = make(toolCallId) - registerClientTool(toolCallId, inst) - } - } catch {} -} - // Constants const TEXT_BLOCK_TYPE = 'text' const THINKING_BLOCK_TYPE = 'thinking' @@ -324,75 +121,54 @@ const CONTINUE_OPTIONS_TAG = '{"1":"Continue"}' function resolveToolDisplay( toolName: string | undefined, state: ClientToolCallState, - toolCallId?: string, + _toolCallId?: string, params?: Record ): ClientToolDisplay | undefined { - try { - if (!toolName) return undefined - const def = getTool(toolName) as any - const toolMetadata = def?.metadata || CLASS_TOOL_METADATA[toolName] - const meta = toolMetadata?.displayNames || {} + if (!toolName) return undefined + const entry = TOOL_DISPLAY_REGISTRY[toolName] + if (!entry) return humanizedFallback(toolName, state) - // Exact state first - const ds = meta?.[state] - if (ds?.text || ds?.icon) { - // Check if tool has a dynamic text formatter - const getDynamicText = toolMetadata?.getDynamicText - if (getDynamicText && params) { - try { - const dynamicText = getDynamicText(params, state) - if (dynamicText) { - return { text: dynamicText, icon: ds.icon } - } - } catch (e) { - // Fall back to static text if formatter fails - } - } - return { text: ds.text, icon: ds.icon } + // Check dynamic text first + if (entry.uiConfig?.dynamicText && params) { + const dynamicText = entry.uiConfig.dynamicText(params, state) + const stateDisplay = entry.displayNames[state] + if (dynamicText && stateDisplay?.icon) { + return { text: dynamicText, icon: stateDisplay.icon } } + } - // Fallback order (prefer pre-execution states for unknown states like pending) - const fallbackOrder: ClientToolCallState[] = [ - (ClientToolCallState as any).generating, - (ClientToolCallState as any).executing, - (ClientToolCallState as any).review, - (ClientToolCallState as any).success, - (ClientToolCallState as any).error, - (ClientToolCallState as any).rejected, - ] - for (const key of fallbackOrder) { - const cand = meta?.[key] - if (cand?.text || cand?.icon) return { text: cand.text, icon: cand.icon } - } - } catch {} - // Humanized fallback as last resort - include state verb for proper verb-noun styling - try { - if (toolName) { - const formattedName = toolName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) - // Add state verb prefix for verb-noun rendering in tool-call component - let stateVerb: string - switch (state) { - case ClientToolCallState.pending: - case ClientToolCallState.executing: - stateVerb = 'Executing' - break - case ClientToolCallState.success: - stateVerb = 'Executed' - break - case ClientToolCallState.error: - stateVerb = 'Failed' - break - case ClientToolCallState.rejected: - case ClientToolCallState.aborted: - stateVerb = 'Skipped' - break - default: - stateVerb = 'Executing' - } - return { text: `${stateVerb} ${formattedName}`, icon: undefined as any } - } - } catch {} - return undefined + // Exact state match + const display = entry.displayNames[state] + if (display?.text || display?.icon) return display + + // Fallback through states + const fallbackOrder = [ + ClientToolCallState.generating, + ClientToolCallState.executing, + ClientToolCallState.success, + ] + for (const fallbackState of fallbackOrder) { + const fallback = entry.displayNames[fallbackState] + if (fallback?.text || fallback?.icon) return fallback + } + + return humanizedFallback(toolName, state) +} + +function humanizedFallback( + toolName: string, + state: ClientToolCallState +): ClientToolDisplay | undefined { + const formattedName = toolName.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + const stateVerb = + state === ClientToolCallState.success + ? 'Executed' + : state === ClientToolCallState.error + ? 'Failed' + : state === ClientToolCallState.rejected || state === ClientToolCallState.aborted + ? 'Skipped' + : 'Executing' + return { text: `${stateVerb} ${formattedName}`, icon: undefined as any } } // Helper: check if a tool state is rejected @@ -512,9 +288,8 @@ function abortAllInProgressTools(set: any, get: () => CopilotStore) { /** * Loads messages from DB for UI rendering. * Messages are stored exactly as they render, so we just need to: - * 1. Register client tool instances for any tool calls - * 2. Clear any streaming flags (messages loaded from DB are never actively streaming) - * 3. Return the messages + * 1. Clear any streaming flags (messages loaded from DB are never actively streaming) + * 2. Return the messages */ function normalizeMessagesForUI(messages: CopilotMessage[]): CopilotMessage[] { try { @@ -530,12 +305,11 @@ function normalizeMessagesForUI(messages: CopilotMessage[]): CopilotMessage[] { } } - // Register client tool instances and clear streaming flags for all tool calls + // Clear streaming flags for all tool calls for (const message of messages) { if (message.contentBlocks) { for (const block of message.contentBlocks as any[]) { if (block?.type === 'tool_call' && block.toolCall) { - registerToolCallInstances(block.toolCall) clearStreamingFlags(block.toolCall) } } @@ -578,28 +352,6 @@ function clearStreamingFlags(toolCall: any): void { } } -/** - * Recursively registers client tool instances for a tool call and its nested subagent tool calls. - */ -function registerToolCallInstances(toolCall: any): void { - if (!toolCall?.id) return - ensureClientToolInstance(toolCall.name, toolCall.id) - - // Register nested subagent tool calls - if (Array.isArray(toolCall.subAgentBlocks)) { - for (const block of toolCall.subAgentBlocks) { - if (block?.type === 'subagent_tool_call' && block.toolCall) { - registerToolCallInstances(block.toolCall) - } - } - } - if (Array.isArray(toolCall.subAgentToolCalls)) { - for (const subTc of toolCall.subAgentToolCalls) { - registerToolCallInstances(subTc) - } - } -} - // Simple object pool for content blocks class ObjectPool { private pool: T[] = [] @@ -1431,9 +1183,6 @@ const sseHandlers: Record = { if (!toolCallId || !toolName) return const { toolCallsById } = get() - // Ensure class-based client tool instances are registered (for interrupts/display) - ensureClientToolInstance(toolName, toolCallId) - if (!toolCallsById[toolCallId]) { // Show as pending until we receive full tool_call (with arguments) to decide execution const initialState = ClientToolCallState.pending @@ -1461,9 +1210,6 @@ const sseHandlers: Record = { const isPartial = toolData.partial === true const { toolCallsById } = get() - // Ensure class-based client tool instances are registered (for interrupts/display) - ensureClientToolInstance(name, id) - const existing = toolCallsById[id] const next: CopilotToolCall = existing ? { @@ -1939,9 +1685,6 @@ const subAgentSSEHandlers: Record = { context.subAgentBlocks[parentToolCallId] = [] } - // Ensure client tool instance is registered (for execution) - ensureClientToolInstance(name, id) - // Create or update the subagent tool call const existingIndex = context.subAgentToolCalls[parentToolCallId].findIndex( (tc) => tc.id === id @@ -4287,56 +4030,3 @@ export const useCopilotStore = create()( }, })) ) - -// Sync class-based tool instance state changes back into the store map -try { - registerToolStateSync((toolCallId: string, nextState: any) => { - const state = useCopilotStore.getState() - const current = state.toolCallsById[toolCallId] - if (!current) return - let mapped: ClientToolCallState = current.state - if (nextState === 'executing') mapped = ClientToolCallState.executing - else if (nextState === 'pending') mapped = ClientToolCallState.pending - else if (nextState === 'success' || nextState === 'accepted') - mapped = ClientToolCallState.success - else if (nextState === 'error' || nextState === 'errored') mapped = ClientToolCallState.error - else if (nextState === 'rejected') mapped = ClientToolCallState.rejected - else if (nextState === 'aborted') mapped = ClientToolCallState.aborted - else if (nextState === 'review') mapped = (ClientToolCallState as any).review - else if (nextState === 'background') mapped = (ClientToolCallState as any).background - else if (typeof nextState === 'number') mapped = nextState as unknown as ClientToolCallState - - // Store-authoritative gating: ignore invalid/downgrade transitions - const isTerminal = (s: ClientToolCallState) => - s === ClientToolCallState.success || - s === ClientToolCallState.error || - s === ClientToolCallState.rejected || - s === ClientToolCallState.aborted || - (s as any) === (ClientToolCallState as any).review || - (s as any) === (ClientToolCallState as any).background - - // If we've already reached a terminal state, ignore any further non-terminal updates - if (isTerminal(current.state) && !isTerminal(mapped)) { - return - } - // Prevent downgrades (executing → pending, pending → generating) - if ( - (current.state === ClientToolCallState.executing && mapped === ClientToolCallState.pending) || - (current.state === ClientToolCallState.pending && - mapped === (ClientToolCallState as any).generating) - ) { - return - } - // No-op if unchanged - if (mapped === current.state) return - const updated = { - ...state.toolCallsById, - [toolCallId]: { - ...current, - state: mapped, - display: resolveToolDisplay(current.name, mapped, toolCallId, current.params), - }, - } - useCopilotStore.setState({ toolCallsById: updated }) - }) -} catch {} diff --git a/apps/sim/stores/workflow-diff/store.ts b/apps/sim/stores/workflow-diff/store.ts index abd57d0ae..5b9ba8b6b 100644 --- a/apps/sim/stores/workflow-diff/store.ts +++ b/apps/sim/stores/workflow-diff/store.ts @@ -1,7 +1,6 @@ import { createLogger } from '@sim/logger' import { create } from 'zustand' import { devtools } from 'zustand/middleware' -import { getClientTool } from '@/lib/copilot/tools/client/manager' import { stripWorkflowDiffMarkers, WorkflowDiffEngine } from '@/lib/workflows/diff' import { enqueueReplaceWorkflowState } from '@/lib/workflows/operations/socket-operations' import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' @@ -350,10 +349,12 @@ export const useWorkflowDiffStore = create { if (toolCallId) { - getClientTool(toolCallId) - ?.handleAccept?.() - ?.catch?.((error: Error) => { - logger.warn('Failed to notify tool accept state', { error }) + import('@/stores/panel/copilot/store') + .then(({ useCopilotStore }) => { + useCopilotStore.getState().updatePreviewToolCallState('accepted', toolCallId) + }) + .catch((error) => { + logger.warn('Failed to update tool accept state', { error }) }) } }) @@ -458,10 +459,12 @@ export const useWorkflowDiffStore = create { if (toolCallId) { - getClientTool(toolCallId) - ?.handleReject?.() - ?.catch?.((error: Error) => { - logger.warn('Failed to notify tool reject state', { error }) + import('@/stores/panel/copilot/store') + .then(({ useCopilotStore }) => { + useCopilotStore.getState().updatePreviewToolCallState('rejected', toolCallId) + }) + .catch((error) => { + logger.warn('Failed to update tool reject state', { error }) }) } })