From 485dce7bed5ba09d28ea407906fb0d3f9eb57ffa Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 9 Apr 2026 17:15:45 -0700 Subject: [PATCH] Tool call names --- .../message-content/message-content.tsx | 2 +- .../[workspaceId]/home/hooks/use-chat.ts | 387 +++++++++++++++--- .../app/workspace/[workspaceId]/home/types.ts | 52 +-- .../lib/copilot/generated/tool-catalog-v1.ts | 375 +++++++++++++---- .../lib/copilot/generated/tool-schemas-v1.ts | 154 +++---- .../lib/copilot/tools/client/store-utils.ts | 16 +- .../tools/client/tool-display-registry.ts | 178 ++++++-- scripts/sync-tool-catalog.ts | 96 ++++- 8 files changed, 976 insertions(+), 284 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index 318a2e89c2..4fac008731 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -92,7 +92,7 @@ function mapToolStatusToClientState( } function getOverrideDisplayTitle(tc: NonNullable): string | undefined { - if (tc.name === ReadTool.id || tc.name.endsWith('_respond')) { + if (tc.name === ReadTool.id || tc.name === 'respond' || tc.name.endsWith('_respond')) { return resolveToolDisplay(tc.name, mapToolStatusToClientState(tc.status), tc.id, tc.params) ?.text } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 2fa506bb0c..723c6afc6e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -20,6 +20,7 @@ import { MothershipStreamV1ToolPhase, } from '@/lib/copilot/generated/mothership-stream-v1' import { + CrawlWebsite, CreateFolder, DeleteFolder, DeleteWorkflow, @@ -27,13 +28,33 @@ import { DeployChat, DeployMcp, File as FileTool, + GetPageContents, + GetWorkflowLogs, + Glob, + Grep, + ManageCredential, + ManageCredentialOperation, + ManageCustomTool, + ManageCustomToolOperation, + ManageJob, + ManageJobOperation, + ManageMcpTool, + ManageMcpToolOperation, + ManageSkill, + ManageSkillOperation, MoveFolder, MoveWorkflow, Read as ReadTool, Redeploy, RenameWorkflow, + RunFromBlock, + RunWorkflow, + RunWorkflowUntilBlock, + ScrapePage, + SearchOnline, ToolSearchToolRegex, WorkspaceFile, + WorkspaceFileOperation, } from '@/lib/copilot/generated/tool-catalog-v1' import type { StreamBatchEvent } from '@/lib/copilot/request/session/types' import { @@ -72,6 +93,7 @@ import type { ChatContext } from '@/stores/panel' import { useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import type { WorkflowMetadata } from '@/stores/workflows/registry/types' +import { useWorkflowStore } from '@/stores/workflows/workflow/store' import type { ChatMessage, ContentBlock, @@ -132,6 +154,314 @@ const logger = createLogger('useChat') type StreamPayload = Record +function stringParam(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() ? value.trim() : undefined +} + +function stringArrayParam(value: unknown): string[] { + if (!Array.isArray(value)) return [] + return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0) +} + +function resolveWorkflowNameForDisplay(workflowId: unknown): string | undefined { + const id = stringParam(workflowId) + if (!id) return undefined + const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId + if (!workspaceId) return undefined + return getWorkflowById(workspaceId, id)?.name +} + +function resolveBlockNameForDisplay(blockId: unknown): string | undefined { + const id = stringParam(blockId) + if (!id) return undefined + return useWorkflowStore.getState().blocks[id]?.name +} + +function resolveWorkspaceFileDisplayTitle( + operation: unknown, + title: unknown, + targetFileName?: unknown +): string | undefined { + const chunkTitle = stringParam(title) + const fileName = stringParam(targetFileName) + let verb = 'Writing' + + switch (operation) { + case WorkspaceFileOperation.append: + verb = 'Adding' + break + case WorkspaceFileOperation.patch: + verb = 'Editing' + break + case WorkspaceFileOperation.update: + verb = 'Writing' + break + } + + if (chunkTitle) return `${verb} ${chunkTitle}` + if (fileName) return `${verb} ${fileName}` + return undefined +} + +function resolveOperationDisplayTitle( + operation: unknown, + labels: Partial>, + fallback: string +): string { + const label = typeof operation === 'string' ? labels[operation] : undefined + return label ?? fallback +} + +function resolveToolDisplayTitle(name: string, args?: Record): string | undefined { + if (!args) return undefined + + if (name === WorkspaceFile.id) { + const target = asPayloadRecord(args.target) + return resolveWorkspaceFileDisplayTitle(args.operation, args.title, target?.fileName) + } + + if (name === SearchOnline.id) { + const toolTitle = stringParam(args.toolTitle) + return toolTitle ? `Searching online for ${toolTitle}` : 'Searching online' + } + + if (name === Grep.id) { + const toolTitle = stringParam(args.toolTitle) + return toolTitle ? `Searching for ${toolTitle}` : 'Searching' + } + + if (name === Glob.id) { + const toolTitle = stringParam(args.toolTitle) + return toolTitle ? `Finding ${toolTitle}` : 'Finding files' + } + + if (name === ScrapePage.id) { + const url = stringParam(args.url) + return url ? `Scraping ${url}` : 'Scraping page' + } + + if (name === CrawlWebsite.id) { + const url = stringParam(args.url) + return url ? `Crawling ${url}` : 'Crawling website' + } + + if (name === GetPageContents.id) { + const urls = stringArrayParam(args.urls) + if (urls.length === 1) return `Getting ${urls[0]}` + if (urls.length > 1) return `Getting ${urls.length} pages` + return 'Getting page contents' + } + + if (name === ManageCustomTool.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageCustomToolOperation.add]: 'Creating custom tool', + [ManageCustomToolOperation.edit]: 'Updating custom tool', + [ManageCustomToolOperation.delete]: 'Deleting custom tool', + [ManageCustomToolOperation.list]: 'Listing custom tools', + }, + 'Custom tool action' + ) + } + + if (name === ManageMcpTool.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageMcpToolOperation.add]: 'Creating MCP server', + [ManageMcpToolOperation.edit]: 'Updating MCP server', + [ManageMcpToolOperation.delete]: 'Deleting MCP server', + [ManageMcpToolOperation.list]: 'Listing MCP servers', + }, + 'MCP server action' + ) + } + + if (name === ManageSkill.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageSkillOperation.add]: 'Creating skill', + [ManageSkillOperation.edit]: 'Updating skill', + [ManageSkillOperation.delete]: 'Deleting skill', + [ManageSkillOperation.list]: 'Listing skills', + }, + 'Skill action' + ) + } + + if (name === ManageJob.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageJobOperation.create]: 'Creating job', + [ManageJobOperation.get]: 'Getting job', + [ManageJobOperation.update]: 'Updating job', + [ManageJobOperation.delete]: 'Deleting job', + [ManageJobOperation.list]: 'Listing jobs', + }, + 'Job action' + ) + } + + if (name === ManageCredential.id) { + return resolveOperationDisplayTitle( + args.operation, + { + [ManageCredentialOperation.rename]: 'Renaming credential', + [ManageCredentialOperation.delete]: 'Deleting credential', + }, + 'Credential action' + ) + } + + if (name === RunWorkflow.id) { + const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + return workflowName ? `Running ${workflowName}` : 'Running workflow' + } + + if (name === RunFromBlock.id) { + const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + const blockName = resolveBlockNameForDisplay(args.startBlockId) + if (workflowName && blockName) return `Running ${workflowName} from ${blockName}` + if (workflowName) return `Running ${workflowName}` + if (blockName) return `Running from ${blockName}` + return 'Running workflow' + } + + if (name === RunWorkflowUntilBlock.id) { + const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + const blockName = resolveBlockNameForDisplay(args.stopAfterBlockId) + if (workflowName && blockName) return `Running ${workflowName} until ${blockName}` + if (workflowName) return `Running ${workflowName}` + if (blockName) return `Running until ${blockName}` + return 'Running workflow' + } + + if (name === GetWorkflowLogs.id) { + const workflowName = resolveWorkflowNameForDisplay(args.workflowId) + return workflowName ? `Getting logs for ${workflowName}` : 'Getting logs' + } + + return undefined +} + +function decodeStreamingString(value: string): string { + return value + .replace(/\\u([0-9a-fA-F]{4})/g, (_: string, hex: string) => + String.fromCharCode(Number.parseInt(hex, 16)) + ) + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\') +} + +function matchStreamingStringArg(streamingArgs: string, key: string): string | undefined { + const match = streamingArgs.match(new RegExp(`"${key}"\\s*:\\s*"([^"]*)"`, 'm')) + return match?.[1] ? decodeStreamingString(match[1]) : undefined +} + +function resolveStreamingToolDisplayTitle(name: string, streamingArgs: string): string | undefined { + if (name === WorkspaceFile.id) { + return resolveWorkspaceFileDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + matchStreamingStringArg(streamingArgs, 'title'), + matchStreamingStringArg(streamingArgs, 'fileName') + ) + } + + if (name === SearchOnline.id) { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + return toolTitle ? `Searching online for ${toolTitle}` : undefined + } + + if (name === Grep.id) { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + return toolTitle ? `Searching for ${toolTitle}` : undefined + } + + if (name === Glob.id) { + const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') + return toolTitle ? `Finding ${toolTitle}` : undefined + } + + if (name === ScrapePage.id) { + const url = matchStreamingStringArg(streamingArgs, 'url') + return url ? `Scraping ${url}` : undefined + } + + if (name === CrawlWebsite.id) { + const url = matchStreamingStringArg(streamingArgs, 'url') + return url ? `Crawling ${url}` : undefined + } + + if (name === ManageCustomTool.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageCustomToolOperation.add]: 'Creating custom tool', + [ManageCustomToolOperation.edit]: 'Updating custom tool', + [ManageCustomToolOperation.delete]: 'Deleting custom tool', + [ManageCustomToolOperation.list]: 'Listing custom tools', + }, + 'Custom tool action' + ) + } + + if (name === ManageMcpTool.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageMcpToolOperation.add]: 'Creating MCP server', + [ManageMcpToolOperation.edit]: 'Updating MCP server', + [ManageMcpToolOperation.delete]: 'Deleting MCP server', + [ManageMcpToolOperation.list]: 'Listing MCP servers', + }, + 'MCP server action' + ) + } + + if (name === ManageSkill.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageSkillOperation.add]: 'Creating skill', + [ManageSkillOperation.edit]: 'Updating skill', + [ManageSkillOperation.delete]: 'Deleting skill', + [ManageSkillOperation.list]: 'Listing skills', + }, + 'Skill action' + ) + } + + if (name === ManageJob.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageJobOperation.create]: 'Creating job', + [ManageJobOperation.get]: 'Getting job', + [ManageJobOperation.update]: 'Updating job', + [ManageJobOperation.delete]: 'Deleting job', + [ManageJobOperation.list]: 'Listing jobs', + }, + 'Job action' + ) + } + + if (name === ManageCredential.id) { + return resolveOperationDisplayTitle( + matchStreamingStringArg(streamingArgs, 'operation'), + { + [ManageCredentialOperation.rename]: 'Renaming credential', + [ManageCredentialOperation.delete]: 'Deleting credential', + }, + 'Credential action' + ) + } + + return undefined +} + type StreamToolUI = { hidden?: boolean title?: string @@ -1068,35 +1398,8 @@ export function useChat( if (idx !== undefined && blocks[idx].toolCall) { const tc = blocks[idx].toolCall! tc.streamingArgs = (tc.streamingArgs ?? '') + delta - - if (tc.name === WorkspaceFile.id) { - const opMatch = tc.streamingArgs.match(/"operation"\s*:\s*"(\w+)"/) - const op = opMatch?.[1] ?? '' - const verb = - op === 'create' - ? 'Creating' - : op === 'append' - ? 'Adding' - : op === 'patch' - ? 'Editing' - : op === 'update' - ? 'Writing' - : op === 'rename' - ? 'Renaming' - : op === 'delete' - ? 'Deleting' - : 'Writing' - const titleMatch = tc.streamingArgs.match(/"title"\s*:\s*"([^"]*)"/) - if (titleMatch?.[1]) { - const unescaped = titleMatch[1] - .replace(/\\u([0-9a-fA-F]{4})/g, (_: string, hex: string) => - String.fromCharCode(Number.parseInt(hex, 16)) - ) - .replace(/\\"/g, '"') - .replace(/\\\\/g, '\\') - tc.displayTitle = `${verb} ${unescaped}` - } - } + const displayTitle = resolveStreamingToolDisplayTitle(tc.name, tc.streamingArgs) + if (displayTitle) tc.displayTitle = displayTitle flush() } @@ -1252,31 +1555,7 @@ export function useChat( | Record | undefined - if (name === WorkspaceFile.id) { - const operation = typeof args?.operation === 'string' ? args.operation : '' - const verb = - operation === 'create' - ? 'Creating' - : operation === 'append' - ? 'Adding' - : operation === 'patch' - ? 'Editing' - : operation === 'update' - ? 'Writing' - : operation === 'rename' - ? 'Renaming' - : operation === 'delete' - ? 'Deleting' - : 'Writing' - const chunkTitle = args?.title as string | undefined - const target = args ? asPayloadRecord(args.target) : undefined - const targetFileName = target?.fileName as string | undefined - if (chunkTitle) { - displayTitle = `${verb} ${chunkTitle}` - } else if (targetFileName) { - displayTitle = `${verb} ${targetFileName}` - } - } + displayTitle = resolveToolDisplayTitle(name, args) ?? displayTitle if (name === 'edit_content') { const parentToolCallId = diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 26f39e689f..80f091de4a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -180,19 +180,19 @@ export interface ChatMessage { } export const SUBAGENT_LABELS: Record = { - workflow: 'Workflow agent', - deploy: 'Deploy agent', - auth: 'Integration agent', - research: 'Research agent', - knowledge: 'Knowledge agent', - table: 'Table agent', - custom_tool: 'Custom Tool agent', + workflow: 'Workflow Agent', + deploy: 'Deploy Agent', + auth: 'Auth Agent', + research: 'Research Agent', + knowledge: 'Knowledge Agent', + table: 'Table Agent', + custom_tool: 'Custom Tool Agent', superagent: 'Superagent', - debug: 'Debug agent', - run: 'Run agent', - agent: 'Agent manager', - job: 'Job agent', - file: 'File', + debug: 'Debug Agent', + run: 'Run Agent', + agent: 'Tools Agent', + job: 'Job Agent', + file: 'File Agent', } as const export interface ToolUIMetadata { @@ -208,12 +208,12 @@ export interface ToolUIMetadata { */ export const TOOL_UI_METADATA: Record = { [Glob.id]: { - title: 'Searching files', + title: 'Finding files', phaseLabel: 'Workspace', phase: 'workspace', }, [Grep.id]: { - title: 'Searching code', + title: 'Searching', phaseLabel: 'Workspace', phase: 'workspace', }, @@ -239,12 +239,12 @@ export const TOOL_UI_METADATA: Record = { phase: 'search', }, [ManageMcpTool.id]: { - title: 'Managing MCP tool', + title: 'MCP server action', phaseLabel: 'Management', phase: 'management', }, [ManageSkill.id]: { - title: 'Managing skill', + title: 'Skill action', phaseLabel: 'Management', phase: 'management', }, @@ -288,16 +288,16 @@ export const TOOL_UI_METADATA: Record = { phaseLabel: 'Resource', phase: 'resource', }, - [Workflow.id]: { title: 'Managing workflow', phaseLabel: 'Workflow', phase: 'subagent' }, - [Run.id]: { title: 'Running', phaseLabel: 'Run', phase: 'subagent' }, - [Deploy.id]: { title: 'Deploying', phaseLabel: 'Deploy', phase: 'subagent' }, + [Workflow.id]: { title: 'Workflow Agent', phaseLabel: 'Workflow', phase: 'subagent' }, + [Run.id]: { title: 'Run Agent', phaseLabel: 'Run', phase: 'subagent' }, + [Deploy.id]: { title: 'Deploy Agent', phaseLabel: 'Deploy', phase: 'subagent' }, [Auth.id]: { - title: 'Connecting credentials', + title: 'Auth Agent', phaseLabel: 'Auth', phase: 'subagent', }, [Knowledge.id]: { - title: 'Managing knowledge', + title: 'Knowledge Agent', phaseLabel: 'Knowledge', phase: 'subagent', }, @@ -306,16 +306,16 @@ export const TOOL_UI_METADATA: Record = { phaseLabel: 'Resource', phase: 'resource', }, - [Table.id]: { title: 'Managing tables', phaseLabel: 'Table', phase: 'subagent' }, - [Job.id]: { title: 'Managing jobs', phaseLabel: 'Job', phase: 'subagent' }, - [Agent.id]: { title: 'Agent action', phaseLabel: 'Agent', phase: 'subagent' }, + [Table.id]: { title: 'Table Agent', phaseLabel: 'Table', phase: 'subagent' }, + [Job.id]: { title: 'Job Agent', phaseLabel: 'Job', phase: 'subagent' }, + [Agent.id]: { title: 'Tools Agent', phaseLabel: 'Agent', phase: 'subagent' }, custom_tool: { title: 'Creating tool', phaseLabel: 'Tool', phase: 'subagent', }, - [Research.id]: { title: 'Researching', phaseLabel: 'Research', phase: 'subagent' }, - [Debug.id]: { title: 'Debugging', phaseLabel: 'Debug', phase: 'subagent' }, + [Research.id]: { title: 'Research Agent', phaseLabel: 'Research', phase: 'subagent' }, + [Debug.id]: { title: 'Debug Agent', phaseLabel: 'Debug', phase: 'subagent' }, [OpenResource.id]: { title: 'Opening resource', phaseLabel: 'Resource', diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 591b20d794..9e6005b25d 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -701,6 +701,38 @@ export const DownloadToWorkspaceFile: ToolCatalogEntry = { requiredPermission: 'write', } +export const EditContent: ToolCatalogEntry = { + id: 'edit_content', + name: 'edit_content', + executor: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + content: { + type: 'string', + description: + 'The text content to write. For append: text to append. For update: full replacement text. For patch with search_replace: the replacement text. For patch with anchored: the insert/replacement text.', + }, + }, + required: ['content'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: 'object', + description: + 'Optional operation metadata such as file id, file name, size, and content type.', + }, + message: { type: 'string', description: 'Human-readable summary of the outcome.' }, + success: { type: 'boolean', description: 'Whether the content was applied successfully.' }, + }, + required: ['success', 'message'], + }, + requiredPermission: 'write', +} + export const EditWorkflow: ToolCatalogEntry = { id: 'edit_workflow', name: 'edit_workflow', @@ -1138,10 +1170,10 @@ export const Glob: ToolCatalogEntry = { description: 'Glob pattern to match file paths. Supports * (any segment) and ** (any depth).', }, - title: { + toolTitle: { type: 'string', description: - "Short human-readable label shown in the UI while this search runs (e.g. 'Finding workflow configs', 'Listing knowledge bases').", + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "workflow configs" or "knowledge bases", not a full sentence like "Finding workflow configs".', }, }, required: ['pattern'], @@ -1183,10 +1215,10 @@ export const Grep: ToolCatalogEntry = { "Optional path prefix to scope the search (e.g. 'workflows/', 'environment/', 'internal/', 'components/blocks/').", }, pattern: { type: 'string', description: 'Regex pattern to search for in file contents.' }, - title: { + toolTitle: { type: 'string', description: - "Short human-readable label shown in the UI while this search runs (e.g. 'Searching Slack integrations', 'Finding deployed workflows').", + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "Slack integrations" or "deployed workflows", not a full sentence like "Searching for Slack integrations".', }, }, required: ['pattern'], @@ -2170,6 +2202,11 @@ export const SearchOnline: ToolCatalogEntry = { include_text: { type: 'boolean', description: 'Include page text content (default true)' }, num_results: { type: 'number', description: 'Number of results (default 10, max 25)' }, query: { type: 'string', description: 'Natural language search query' }, + toolTitle: { + type: 'string', + description: + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "pricing changes" or "Slack webhook docs", not a full sentence like "Searching online for pricing changes".', + }, }, required: ['query'], }, @@ -2593,11 +2630,6 @@ export const WorkspaceFile: ToolCatalogEntry = { type: 'object', description: 'Explicit file target. Use kind=file_id + fileId for existing files.', properties: { - kind: { - type: 'string', - description: 'How the file target is identified.', - enum: ['new_file', 'file_id'], - }, fileId: { type: 'string', description: 'Canonical existing workspace file ID. Required when target.kind=file_id.', @@ -2607,6 +2639,11 @@ export const WorkspaceFile: ToolCatalogEntry = { description: 'Plain workspace filename including extension, e.g. "main.py" or "report.docx". Required when target.kind=new_file.', }, + kind: { + type: 'string', + description: 'How the file target is identified.', + enum: ['new_file', 'file_id'], + }, }, required: ['kind'], }, @@ -2635,35 +2672,6 @@ export const WorkspaceFile: ToolCatalogEntry = { description: 'Patch metadata. Use strategy=search_replace for exact text replacement, or strategy=anchored for line-based inserts/replacements/deletions. The actual replacement/insert content is provided via the paired edit_content tool call.', properties: { - strategy: { - type: 'string', - description: 'Patch strategy.', - enum: ['search_replace', 'anchored'], - }, - search: { - type: 'string', - description: - 'Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true.', - }, - replaceAll: { - type: 'boolean', - description: - 'When true and strategy=search_replace, replace every match instead of requiring a unique single match.', - }, - mode: { - type: 'string', - description: 'Anchored edit mode when strategy=anchored.', - enum: ['replace_between', 'insert_after', 'delete_between'], - }, - occurrence: { - type: 'number', - description: '1-based occurrence for repeated anchor lines. Optional; defaults to 1.', - }, - before_anchor: { - type: 'string', - description: - 'Boundary line kept before inserted replacement content. Required for mode=replace_between.', - }, after_anchor: { type: 'string', description: @@ -2674,16 +2682,49 @@ export const WorkspaceFile: ToolCatalogEntry = { description: 'Anchor line after which new content is inserted. Required for mode=insert_after.', }, - start_anchor: { + before_anchor: { type: 'string', - description: 'First line to delete. Required for mode=delete_between.', + description: + 'Boundary line kept before inserted replacement content. Required for mode=replace_between.', }, end_anchor: { type: 'string', description: 'First line to keep after deletion. Required for mode=delete_between.', }, + mode: { + type: 'string', + description: 'Anchored edit mode when strategy=anchored.', + enum: ['replace_between', 'insert_after', 'delete_between'], + }, + occurrence: { + type: 'number', + description: '1-based occurrence for repeated anchor lines. Optional; defaults to 1.', + }, + replaceAll: { + type: 'boolean', + description: + 'When true and strategy=search_replace, replace every match instead of requiring a unique single match.', + }, + search: { + type: 'string', + description: + 'Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true.', + }, + start_anchor: { + type: 'string', + description: 'First line to delete. Required for mode=delete_between.', + }, + strategy: { + type: 'string', + description: 'Patch strategy.', + enum: ['search_replace', 'anchored'], + }, }, }, + newName: { + type: 'string', + description: 'New file name for rename. Must be a plain workspace filename like "main.py".', + }, }, required: ['operation', 'target', 'title'], }, @@ -2703,37 +2744,227 @@ export const WorkspaceFile: ToolCatalogEntry = { requiredPermission: 'write', } -export const EditContent: ToolCatalogEntry = { - id: 'edit_content', - name: 'edit_content', - executor: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - content: { - type: 'string', - description: - 'The text content to write. For append: text to append. For update: full replacement text. For patch with search_replace: the replacement text. For patch with anchored: the insert/replacement text.', - }, - }, - required: ['content'], - }, - resultSchema: { - type: 'object', - properties: { - data: { - type: 'object', - description: - 'Optional operation metadata such as file id, file name, size, and content type.', - }, - message: { type: 'string', description: 'Human-readable summary of the outcome.' }, - success: { type: 'boolean', description: 'Whether the content was applied successfully.' }, - }, - required: ['success', 'message'], - }, - requiredPermission: 'write', -} +export const KnowledgeBaseOperation = { + create: 'create', + get: 'get', + query: 'query', + addFile: 'add_file', + update: 'update', + delete: 'delete', + deleteDocument: 'delete_document', + updateDocument: 'update_document', + listTags: 'list_tags', + createTag: 'create_tag', + updateTag: 'update_tag', + deleteTag: 'delete_tag', + getTagUsage: 'get_tag_usage', + addConnector: 'add_connector', + updateConnector: 'update_connector', + deleteConnector: 'delete_connector', + syncConnector: 'sync_connector', +} as const + +export type KnowledgeBaseOperation = + (typeof KnowledgeBaseOperation)[keyof typeof KnowledgeBaseOperation] + +export const KnowledgeBaseOperationValues = [ + KnowledgeBaseOperation.create, + KnowledgeBaseOperation.get, + KnowledgeBaseOperation.query, + KnowledgeBaseOperation.addFile, + KnowledgeBaseOperation.update, + KnowledgeBaseOperation.delete, + KnowledgeBaseOperation.deleteDocument, + KnowledgeBaseOperation.updateDocument, + KnowledgeBaseOperation.listTags, + KnowledgeBaseOperation.createTag, + KnowledgeBaseOperation.updateTag, + KnowledgeBaseOperation.deleteTag, + KnowledgeBaseOperation.getTagUsage, + KnowledgeBaseOperation.addConnector, + KnowledgeBaseOperation.updateConnector, + KnowledgeBaseOperation.deleteConnector, + KnowledgeBaseOperation.syncConnector, +] as const + +export const ManageCredentialOperation = { + rename: 'rename', + delete: 'delete', +} as const + +export type ManageCredentialOperation = + (typeof ManageCredentialOperation)[keyof typeof ManageCredentialOperation] + +export const ManageCredentialOperationValues = [ + ManageCredentialOperation.rename, + ManageCredentialOperation.delete, +] as const + +export const ManageCustomToolOperation = { + add: 'add', + edit: 'edit', + delete: 'delete', + list: 'list', +} as const + +export type ManageCustomToolOperation = + (typeof ManageCustomToolOperation)[keyof typeof ManageCustomToolOperation] + +export const ManageCustomToolOperationValues = [ + ManageCustomToolOperation.add, + ManageCustomToolOperation.edit, + ManageCustomToolOperation.delete, + ManageCustomToolOperation.list, +] as const + +export const ManageJobOperation = { + create: 'create', + list: 'list', + get: 'get', + update: 'update', + delete: 'delete', +} as const + +export type ManageJobOperation = (typeof ManageJobOperation)[keyof typeof ManageJobOperation] + +export const ManageJobOperationValues = [ + ManageJobOperation.create, + ManageJobOperation.list, + ManageJobOperation.get, + ManageJobOperation.update, + ManageJobOperation.delete, +] as const + +export const ManageMcpToolOperation = { + add: 'add', + edit: 'edit', + delete: 'delete', + list: 'list', +} as const + +export type ManageMcpToolOperation = + (typeof ManageMcpToolOperation)[keyof typeof ManageMcpToolOperation] + +export const ManageMcpToolOperationValues = [ + ManageMcpToolOperation.add, + ManageMcpToolOperation.edit, + ManageMcpToolOperation.delete, + ManageMcpToolOperation.list, +] as const + +export const ManageSkillOperation = { + add: 'add', + edit: 'edit', + delete: 'delete', + list: 'list', +} as const + +export type ManageSkillOperation = (typeof ManageSkillOperation)[keyof typeof ManageSkillOperation] + +export const ManageSkillOperationValues = [ + ManageSkillOperation.add, + ManageSkillOperation.edit, + ManageSkillOperation.delete, + ManageSkillOperation.list, +] as const + +export const MaterializeFileOperation = { + save: 'save', + import: 'import', + table: 'table', + knowledgeBase: 'knowledge_base', +} as const + +export type MaterializeFileOperation = + (typeof MaterializeFileOperation)[keyof typeof MaterializeFileOperation] + +export const MaterializeFileOperationValues = [ + MaterializeFileOperation.save, + MaterializeFileOperation.import, + MaterializeFileOperation.table, + MaterializeFileOperation.knowledgeBase, +] as const + +export const UserMemoryOperation = { + add: 'add', + search: 'search', + delete: 'delete', + correct: 'correct', + list: 'list', +} as const + +export type UserMemoryOperation = (typeof UserMemoryOperation)[keyof typeof UserMemoryOperation] + +export const UserMemoryOperationValues = [ + UserMemoryOperation.add, + UserMemoryOperation.search, + UserMemoryOperation.delete, + UserMemoryOperation.correct, + UserMemoryOperation.list, +] as const + +export const UserTableOperation = { + create: 'create', + createFromFile: 'create_from_file', + importFile: 'import_file', + get: 'get', + getSchema: 'get_schema', + delete: 'delete', + insertRow: 'insert_row', + batchInsertRows: 'batch_insert_rows', + getRow: 'get_row', + queryRows: 'query_rows', + updateRow: 'update_row', + deleteRow: 'delete_row', + updateRowsByFilter: 'update_rows_by_filter', + deleteRowsByFilter: 'delete_rows_by_filter', + batchUpdateRows: 'batch_update_rows', + batchDeleteRows: 'batch_delete_rows', + addColumn: 'add_column', + renameColumn: 'rename_column', + deleteColumn: 'delete_column', + updateColumn: 'update_column', +} as const + +export type UserTableOperation = (typeof UserTableOperation)[keyof typeof UserTableOperation] + +export const UserTableOperationValues = [ + UserTableOperation.create, + UserTableOperation.createFromFile, + UserTableOperation.importFile, + UserTableOperation.get, + UserTableOperation.getSchema, + UserTableOperation.delete, + UserTableOperation.insertRow, + UserTableOperation.batchInsertRows, + UserTableOperation.getRow, + UserTableOperation.queryRows, + UserTableOperation.updateRow, + UserTableOperation.deleteRow, + UserTableOperation.updateRowsByFilter, + UserTableOperation.deleteRowsByFilter, + UserTableOperation.batchUpdateRows, + UserTableOperation.batchDeleteRows, + UserTableOperation.addColumn, + UserTableOperation.renameColumn, + UserTableOperation.deleteColumn, + UserTableOperation.updateColumn, +] as const + +export const WorkspaceFileOperation = { + append: 'append', + update: 'update', + patch: 'patch', +} as const + +export type WorkspaceFileOperation = + (typeof WorkspaceFileOperation)[keyof typeof WorkspaceFileOperation] + +export const WorkspaceFileOperationValues = [ + WorkspaceFileOperation.append, + WorkspaceFileOperation.update, + WorkspaceFileOperation.patch, +] as const export const TOOL_CATALOG: Record = { [Agent.id]: Agent, @@ -2757,6 +2988,7 @@ export const TOOL_CATALOG: Record = { [DeployChat.id]: DeployChat, [DeployMcp.id]: DeployMcp, [DownloadToWorkspaceFile.id]: DownloadToWorkspaceFile, + [EditContent.id]: EditContent, [EditWorkflow.id]: EditWorkflow, [File.id]: File, [FunctionExecute.id]: FunctionExecute, @@ -2820,5 +3052,4 @@ export const TOOL_CATALOG: Record = { [UserTable.id]: UserTable, [Workflow.id]: Workflow, [WorkspaceFile.id]: WorkspaceFile, - [EditContent.id]: EditContent, } diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index 58d6162ce5..4f0f96f87f 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -513,6 +513,38 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + edit_content: { + parameters: { + type: 'object', + properties: { + content: { + type: 'string', + description: + 'The text content to write. For append: text to append. For update: full replacement text. For patch with search_replace: the replacement text. For patch with anchored: the insert/replacement text.', + }, + }, + required: ['content'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: 'object', + description: + 'Optional operation metadata such as file id, file name, size, and content type.', + }, + message: { + type: 'string', + description: 'Human-readable summary of the outcome.', + }, + success: { + type: 'boolean', + description: 'Whether the content was applied successfully.', + }, + }, + required: ['success', 'message'], + }, + }, edit_workflow: { parameters: { type: 'object', @@ -928,10 +960,10 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Glob pattern to match file paths. Supports * (any segment) and ** (any depth).', }, - title: { + toolTitle: { type: 'string', description: - "Short human-readable label shown in the UI while this search runs (e.g. 'Finding workflow configs', 'Listing knowledge bases').", + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "workflow configs" or "knowledge bases", not a full sentence like "Finding workflow configs".', }, }, required: ['pattern'], @@ -975,10 +1007,10 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: 'string', description: 'Regex pattern to search for in file contents.', }, - title: { + toolTitle: { type: 'string', description: - "Short human-readable label shown in the UI while this search runs (e.g. 'Searching Slack integrations', 'Finding deployed workflows').", + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "Slack integrations" or "deployed workflows", not a full sentence like "Searching for Slack integrations".', }, }, required: ['pattern'], @@ -1936,6 +1968,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: 'string', description: 'Natural language search query', }, + toolTitle: { + type: 'string', + description: + 'Optional target-only UI phrase for the search row. The UI verb is supplied for you, so pass text like "pricing changes" or "Slack webhook docs", not a full sentence like "Searching online for pricing changes".', + }, }, required: ['query'], }, @@ -2361,11 +2398,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: 'object', description: 'Explicit file target. Use kind=file_id + fileId for existing files.', properties: { - kind: { - type: 'string', - description: 'How the file target is identified.', - enum: ['new_file', 'file_id'], - }, fileId: { type: 'string', description: @@ -2376,6 +2408,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Plain workspace filename including extension, e.g. "main.py" or "report.docx". Required when target.kind=new_file.', }, + kind: { + type: 'string', + description: 'How the file target is identified.', + enum: ['new_file', 'file_id'], + }, }, required: ['kind'], }, @@ -2404,35 +2441,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Patch metadata. Use strategy=search_replace for exact text replacement, or strategy=anchored for line-based inserts/replacements/deletions. The actual replacement/insert content is provided via the paired edit_content tool call.', properties: { - strategy: { - type: 'string', - description: 'Patch strategy.', - enum: ['search_replace', 'anchored'], - }, - search: { - type: 'string', - description: - 'Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true.', - }, - replaceAll: { - type: 'boolean', - description: - 'When true and strategy=search_replace, replace every match instead of requiring a unique single match.', - }, - mode: { - type: 'string', - description: 'Anchored edit mode when strategy=anchored.', - enum: ['replace_between', 'insert_after', 'delete_between'], - }, - occurrence: { - type: 'number', - description: '1-based occurrence for repeated anchor lines. Optional; defaults to 1.', - }, - before_anchor: { - type: 'string', - description: - 'Boundary line kept before inserted replacement content. Required for mode=replace_between.', - }, after_anchor: { type: 'string', description: @@ -2443,16 +2451,50 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'Anchor line after which new content is inserted. Required for mode=insert_after.', }, - start_anchor: { + before_anchor: { type: 'string', - description: 'First line to delete. Required for mode=delete_between.', + description: + 'Boundary line kept before inserted replacement content. Required for mode=replace_between.', }, end_anchor: { type: 'string', description: 'First line to keep after deletion. Required for mode=delete_between.', }, + mode: { + type: 'string', + description: 'Anchored edit mode when strategy=anchored.', + enum: ['replace_between', 'insert_after', 'delete_between'], + }, + occurrence: { + type: 'number', + description: '1-based occurrence for repeated anchor lines. Optional; defaults to 1.', + }, + replaceAll: { + type: 'boolean', + description: + 'When true and strategy=search_replace, replace every match instead of requiring a unique single match.', + }, + search: { + type: 'string', + description: + 'Exact text to find when strategy=search_replace. Must match exactly once unless replaceAll=true.', + }, + start_anchor: { + type: 'string', + description: 'First line to delete. Required for mode=delete_between.', + }, + strategy: { + type: 'string', + description: 'Patch strategy.', + enum: ['search_replace', 'anchored'], + }, }, }, + newName: { + type: 'string', + description: + 'New file name for rename. Must be a plain workspace filename like "main.py".', + }, }, required: ['operation', 'target', 'title'], }, @@ -2476,36 +2518,4 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - edit_content: { - parameters: { - type: 'object', - properties: { - content: { - type: 'string', - description: - 'The text content to write. For append: text to append. For update: full replacement text. For patch with search_replace: the replacement text. For patch with anchored: the insert/replacement text.', - }, - }, - required: ['content'], - }, - resultSchema: { - type: 'object', - properties: { - data: { - type: 'object', - description: - 'Optional operation metadata such as file id, file name, size, and content type.', - }, - message: { - type: 'string', - description: 'Human-readable summary of the outcome.', - }, - success: { - type: 'boolean', - description: 'Whether the content was applied successfully.', - }, - }, - required: ['success', 'message'], - }, - }, } diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts index a050b8cf23..60f09356d9 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.ts @@ -35,6 +35,7 @@ const logger = createLogger('CopilotStoreUtils') /** Respond tools are internal handoff tools shown with a friendly generic label. */ const HIDDEN_TOOL_SUFFIX = '_respond' +const INTERNAL_RESPOND_TOOL = 'respond' const HIDDEN_TOOL_NAMES = new Set(['tool_search_tool_regex']) /** UI metadata sent by the copilot on SSE tool_call events. */ @@ -127,7 +128,7 @@ function specialToolDisplay( state: ClientToolCallState, params?: Record ): ClientToolDisplay | undefined { - if (toolName.endsWith(HIDDEN_TOOL_SUFFIX)) { + if (toolName === INTERNAL_RESPOND_TOOL || toolName.endsWith(HIDDEN_TOOL_SUFFIX)) { return { text: formatRespondLabel(state), icon: Loader2, @@ -146,17 +147,8 @@ function specialToolDisplay( } function formatRespondLabel(state: ClientToolCallState): string { - switch (state) { - case ClientToolCallState.success: - return 'Returned results' - case ClientToolCallState.error: - return 'Failed returning results' - case ClientToolCallState.rejected: - case ClientToolCallState.aborted: - return 'Skipped returning results' - default: - return 'Returning results' - } + void state + return 'Gathering thoughts' } function readStringParam( diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index 29a600657b..204790492f 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -38,6 +38,11 @@ import { XCircle, Zap, } from 'lucide-react' +import { + ManageCustomToolOperation, + ManageMcpToolOperation, + ManageSkillOperation, +} from '@/lib/copilot/generated/tool-catalog-v1' import { getQueryClient } from '@/app/_shell/providers/get-query-client' import type { CustomToolDefinition } from '@/hooks/queries/custom-tools' import type { WorkflowDeploymentInfo } from '@/hooks/queries/deployments' @@ -227,6 +232,15 @@ const META_check_deployment_status: ToolMetadata = { interrupt: undefined, } +const META_complete_job: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Completing job', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Completing job', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Completed job', icon: CheckCircle }, + [ClientToolCallState.error]: { text: 'Failed to complete job', icon: XCircle }, + }, +} + const META_checkoff_todo: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { text: 'Marking todo', icon: Loader2 }, @@ -270,6 +284,30 @@ const META_crawl_website: ToolMetadata = { }, } +const META_create_file: ToolMetadata = { + displayNames: { + [ClientToolCallState.generating]: { text: 'Creating file', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Creating file', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Created file', icon: FileText }, + [ClientToolCallState.error]: { text: 'Failed to create file', icon: XCircle }, + }, + getDynamicText: (params, state) => { + const fileName = params?.fileName + if (typeof fileName !== 'string' || !fileName.trim()) return undefined + + switch (state) { + case ClientToolCallState.success: + return `Created ${fileName}` + case ClientToolCallState.executing: + case ClientToolCallState.generating: + return `Creating ${fileName}` + case ClientToolCallState.error: + return `Failed to create ${fileName}` + } + return undefined + }, +} + const META_create_workspace_mcp_server: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { @@ -348,19 +386,19 @@ const META_agent: ToolMetadata = { const META_manage_skill: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { - text: 'Managing skill', + text: 'Skill action', icon: Loader2, }, - [ClientToolCallState.pending]: { text: 'Manage skill?', icon: BookOpen }, - [ClientToolCallState.executing]: { text: 'Managing skill', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Managed skill', icon: Check }, - [ClientToolCallState.error]: { text: 'Failed to manage skill', icon: X }, + [ClientToolCallState.pending]: { text: 'Skill action?', icon: BookOpen }, + [ClientToolCallState.executing]: { text: 'Skill action', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Updated skill', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed skill action', icon: X }, [ClientToolCallState.aborted]: { - text: 'Aborted managing skill', + text: 'Aborted skill action', icon: XCircle, }, [ClientToolCallState.rejected]: { - text: 'Skipped managing skill', + text: 'Skipped skill action', icon: XCircle, }, }, @@ -368,6 +406,66 @@ const META_manage_skill: ToolMetadata = { accept: { text: 'Allow', icon: Check }, reject: { text: 'Deny', icon: X }, }, + getDynamicText: (params, state) => { + const operation = params?.operation as ManageSkillOperation | undefined + if (!operation) return undefined + + const skillName = typeof params?.name === 'string' ? params.name : 'skill' + + switch (state) { + case ClientToolCallState.success: + switch (operation) { + case ManageSkillOperation.add: + return `Created ${skillName}` + case ManageSkillOperation.edit: + return `Updated ${skillName}` + case ManageSkillOperation.delete: + return `Deleted ${skillName}` + case ManageSkillOperation.list: + return 'Listed skills' + } + break + case ClientToolCallState.executing: + case ClientToolCallState.generating: + switch (operation) { + case ManageSkillOperation.add: + return `Creating ${skillName}` + case ManageSkillOperation.edit: + return `Updating ${skillName}` + case ManageSkillOperation.delete: + return `Deleting ${skillName}` + case ManageSkillOperation.list: + return 'Listing skills' + } + break + case ClientToolCallState.pending: + switch (operation) { + case ManageSkillOperation.add: + return `Create ${skillName}?` + case ManageSkillOperation.edit: + return `Update ${skillName}?` + case ManageSkillOperation.delete: + return `Delete ${skillName}?` + case ManageSkillOperation.list: + return 'List skills?' + } + break + case ClientToolCallState.error: + switch (operation) { + case ManageSkillOperation.add: + return `Failed to create ${skillName}` + case ManageSkillOperation.edit: + return `Failed to update ${skillName}` + case ManageSkillOperation.delete: + return `Failed to delete ${skillName}` + case ManageSkillOperation.list: + return 'Failed to list skills' + } + break + } + + return undefined + }, } const META_workflow: ToolMetadata = { @@ -962,19 +1060,19 @@ const META_list_workspace_mcp_servers: ToolMetadata = { const META_manage_custom_tool: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { - text: 'Managing custom tool', + text: 'Custom tool action', 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.pending]: { text: 'Custom tool action?', icon: Plus }, + [ClientToolCallState.executing]: { text: 'Custom tool action', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Updated custom tool', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed custom tool action', icon: X }, [ClientToolCallState.aborted]: { - text: 'Aborted managing custom tool', + text: 'Aborted custom tool action', icon: XCircle, }, [ClientToolCallState.rejected]: { - text: 'Skipped managing custom tool', + text: 'Skipped custom tool action', icon: XCircle, }, }, @@ -983,7 +1081,7 @@ const META_manage_custom_tool: ToolMetadata = { reject: { text: 'Skip', icon: XCircle }, }, getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | 'list' | undefined + const operation = params?.operation as ManageCustomToolOperation | undefined const workspaceId = getScopedWorkspaceId(params) if (!operation) return undefined @@ -1004,13 +1102,13 @@ const META_manage_custom_tool: ToolMetadata = { const getActionText = (verb: 'present' | 'past' | 'gerund') => { switch (operation) { - case 'add': + case ManageCustomToolOperation.add: return verb === 'present' ? 'Create' : verb === 'past' ? 'Created' : 'Creating' - case 'edit': + case ManageCustomToolOperation.edit: return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': + case ManageCustomToolOperation.delete: return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' - case 'list': + case ManageCustomToolOperation.list: return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' default: return verb === 'present' ? 'Manage' : verb === 'past' ? 'Managed' : 'Managing' @@ -1021,15 +1119,15 @@ const META_manage_custom_tool: ToolMetadata = { // 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') { + if (operation === ManageCustomToolOperation.list) return false + if (operation === ManageCustomToolOperation.add) { return currentState === ClientToolCallState.success } return true // edit and delete always show tool name } const nameText = - operation === 'list' + operation === ManageCustomToolOperation.list ? ' custom tools' : shouldShowToolName(state) && toolName ? ` ${toolName}` @@ -1058,19 +1156,19 @@ const META_manage_custom_tool: ToolMetadata = { const META_manage_mcp_tool: ToolMetadata = { displayNames: { [ClientToolCallState.generating]: { - text: 'Managing MCP tool', + text: 'MCP server action', 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.pending]: { text: 'MCP server action?', icon: Server }, + [ClientToolCallState.executing]: { text: 'MCP server action', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Updated MCP server', icon: Check }, + [ClientToolCallState.error]: { text: 'Failed MCP server action', icon: X }, [ClientToolCallState.aborted]: { - text: 'Aborted managing MCP tool', + text: 'Aborted MCP server action', icon: XCircle, }, [ClientToolCallState.rejected]: { - text: 'Skipped managing MCP tool', + text: 'Skipped MCP server action', icon: XCircle, }, }, @@ -1079,7 +1177,7 @@ const META_manage_mcp_tool: ToolMetadata = { reject: { text: 'Skip', icon: XCircle }, }, getDynamicText: (params, state) => { - const operation = params?.operation as 'add' | 'edit' | 'delete' | undefined + const operation = params?.operation as ManageMcpToolOperation | undefined if (!operation) return undefined @@ -1087,23 +1185,31 @@ const META_manage_mcp_tool: ToolMetadata = { const getActionText = (verb: 'present' | 'past' | 'gerund') => { switch (operation) { - case 'add': + case ManageMcpToolOperation.add: return verb === 'present' ? 'Add' : verb === 'past' ? 'Added' : 'Adding' - case 'edit': + case ManageMcpToolOperation.edit: return verb === 'present' ? 'Edit' : verb === 'past' ? 'Edited' : 'Editing' - case 'delete': + case ManageMcpToolOperation.delete: return verb === 'present' ? 'Delete' : verb === 'past' ? 'Deleted' : 'Deleting' + case ManageMcpToolOperation.list: + return verb === 'present' ? 'List' : verb === 'past' ? 'Listed' : 'Listing' } } const shouldShowServerName = (currentState: ClientToolCallState) => { - if (operation === 'add') { + if (operation === ManageMcpToolOperation.list) return false + if (operation === ManageMcpToolOperation.add) { return currentState === ClientToolCallState.success } return true } - const nameText = shouldShowServerName(state) && serverName ? ` ${serverName}` : ' MCP tool' + const nameText = + operation === ManageMcpToolOperation.list + ? ' MCP servers' + : shouldShowServerName(state) && serverName + ? ` ${serverName}` + : ' MCP server' switch (state) { case ClientToolCallState.success: @@ -2229,8 +2335,10 @@ const TOOL_METADATA_BY_ID: Record = { auth: META_auth, context_compaction: META_context_compaction, check_deployment_status: META_check_deployment_status, + complete_job: META_complete_job, checkoff_todo: META_checkoff_todo, crawl_website: META_crawl_website, + create_file: META_create_file, create_workspace_mcp_server: META_create_workspace_mcp_server, workflow: META_workflow, create_folder: META_create_folder, diff --git a/scripts/sync-tool-catalog.ts b/scripts/sync-tool-catalog.ts index 657de6373f..5edf31d782 100644 --- a/scripts/sync-tool-catalog.ts +++ b/scripts/sync-tool-catalog.ts @@ -4,10 +4,7 @@ import { fileURLToPath } from 'node:url' const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)) const ROOT = resolve(SCRIPT_DIR, '..') -const DEFAULT_CATALOG_PATH = resolve( - ROOT, - '../copilot/copilot/contracts/tool-catalog-v1.json' -) +const DEFAULT_CATALOG_PATH = resolve(ROOT, '../copilot/copilot/contracts/tool-catalog-v1.json') const OUTPUT_PATH = resolve(ROOT, 'apps/sim/lib/copilot/generated/tool-catalog-v1.ts') const RUNTIME_SCHEMA_OUTPUT_PATH = resolve( ROOT, @@ -15,14 +12,56 @@ const RUNTIME_SCHEMA_OUTPUT_PATH = resolve( ) function snakeToPascal(s: string): string { - return s.split('_').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('') + return s + .split('_') + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join('') +} + +function toCamelIdentifier(value: string): string { + const parts = value.split(/[^a-zA-Z0-9]+/).filter(Boolean) + if (parts.length === 0) return 'value' + + const camel = parts + .map((part, index) => { + const lower = part.toLowerCase() + if (index === 0) return lower + return lower.charAt(0).toUpperCase() + lower.slice(1) + }) + .join('') + + return /^[0-9]/.test(camel) ? `v${camel}` : camel +} + +function getTopLevelOperationEnum(tool: Record): string[] | undefined { + const parameters = + typeof tool.parameters === 'object' && tool.parameters !== null + ? (tool.parameters as Record) + : null + const properties = + parameters && typeof parameters.properties === 'object' && parameters.properties !== null + ? (parameters.properties as Record) + : null + const operation = + properties && typeof properties.operation === 'object' && properties.operation !== null + ? (properties.operation as Record) + : null + const values = operation?.enum + + if (!Array.isArray(values) || values.some((value) => typeof value !== 'string')) { + return undefined + } + return values as string[] } function inferTSType(values: unknown[]): string { const unique = [...new Set(values.filter((v) => v !== undefined && v !== null))] if (unique.length === 0) return 'string' if (unique.every((v) => typeof v === 'string')) { - return unique.map((v) => JSON.stringify(v)).sort().join(' | ') + return unique + .map((v) => JSON.stringify(v)) + .sort() + .join(' | ') } if (unique.every((v) => typeof v === 'boolean')) return 'boolean' if (unique.every((v) => typeof v === 'number')) return 'number' @@ -47,7 +86,8 @@ function renderRuntimeSchemaModule(catalog: { tools: Record[] } for (const tool of catalog.tools) { const id = JSON.stringify(tool.id) - const parameters = 'parameters' in tool ? JSON.stringify(tool.parameters ?? null, null, 2) : 'undefined' + const parameters = + 'parameters' in tool ? JSON.stringify(tool.parameters ?? null, null, 2) : 'undefined' const resultSchema = 'resultSchema' in tool ? JSON.stringify(tool.resultSchema ?? null, null, 2) : 'undefined' lines.push(` [${id}]: {`) @@ -96,7 +136,9 @@ function generateInterface(tools: Record[]): string { async function main() { const checkOnly = process.argv.includes('--check') const inputPathArg = process.argv.find((arg) => arg.startsWith('--input=')) - const inputPath = inputPathArg ? resolve(ROOT, inputPathArg.slice('--input='.length)) : DEFAULT_CATALOG_PATH + const inputPath = inputPathArg + ? resolve(ROOT, inputPathArg.slice('--input='.length)) + : DEFAULT_CATALOG_PATH const raw = await readFile(inputPath, 'utf8') const catalog = JSON.parse(raw) as { version: string; tools: Record[] } @@ -122,11 +164,43 @@ async function main() { fields.push(` ${key}: ${JSON.stringify(value)}`) } lines.push(`export const ${constName}: ToolCatalogEntry = {`) - lines.push(fields.join(',\n') + ',') + lines.push(`${fields.join(',\n')},`) lines.push('};') lines.push('') } + for (const tool of catalog.tools) { + const constName = snakeToPascal(tool.id as string) + const operationEnum = getTopLevelOperationEnum(tool) + if (!operationEnum || operationEnum.length === 0) continue + + const operationConstName = `${constName}Operation` + const seenKeys = new Set() + const members = operationEnum.map((value, index) => { + let key = toCamelIdentifier(value) + if (seenKeys.has(key)) key = `${key}${index + 1}` + seenKeys.add(key) + return { key, value } + }) + + lines.push(`export const ${operationConstName} = {`) + for (const member of members) { + lines.push(` ${member.key}: ${JSON.stringify(member.value)},`) + } + lines.push('} as const;') + lines.push('') + lines.push( + `export type ${operationConstName} = (typeof ${operationConstName})[keyof typeof ${operationConstName}];` + ) + lines.push('') + lines.push(`export const ${operationConstName}Values = [`) + for (const member of members) { + lines.push(` ${operationConstName}.${member.key},`) + } + lines.push(`] as const;`) + lines.push('') + } + lines.push(`export const TOOL_CATALOG: Record = {`) for (let i = 0; i < catalog.tools.length; i++) { lines.push(` [${constNames[i]}.id]: ${constNames[i]},`) @@ -141,9 +215,7 @@ async function main() { const existing = await readFile(OUTPUT_PATH, 'utf8').catch(() => null) const existingRuntime = await readFile(RUNTIME_SCHEMA_OUTPUT_PATH, 'utf8').catch(() => null) if (existing !== rendered || existingRuntime !== runtimeSchemaRendered) { - throw new Error( - `Generated tool catalog is stale. Run: bun run mship-tools:generate` - ) + throw new Error(`Generated tool catalog is stale. Run: bun run mship-tools:generate`) } return }