diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx index b00b5bc043..2a06b33e19 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.tsx @@ -4,10 +4,10 @@ import { ShimmerText } from '@/components/ui' import { BrowserRequestTakeover, CallIntegrationTool, + PrepareFileEdit, Read as ReadTool, Terminal as TerminalTool, Wait as WaitTool, - WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' import { getReadTargetBlock } from '@/lib/copilot/tools/client/read-block' import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args' @@ -138,7 +138,7 @@ export function ToolCallItem({ }, [toolName, params, streamingArgs]) const liveWorkspaceFileTitle = useMemo(() => { - if (toolName !== WorkspaceFile.id || !streamingArgs) return null + if (toolName !== PrepareFileEdit.id || !streamingArgs) return null const titleMatch = streamingArgs.match(/"title"\s*:\s*"([^"]+)"/) if (!titleMatch?.[1]) return null const opMatch = streamingArgs.match(/"operation"\s*:\s*"(\w+)"/) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts index 8226865967..a777c7006a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.test.ts @@ -135,7 +135,7 @@ describe('parseBlocks span-identity tree', () => { subagentStart('workflow', 'S1', 'main'), subagentToolCall('t1', 'create_workflow', 'S1', 'workflow'), subagentStart('deploy', 'S2', 'S1'), - subagentToolCall('t2', 'check_deployment_status', 'S2', 'deploy'), + subagentToolCall('t2', 'get_deployment_status', 'S2', 'deploy'), ] const segments = parseBlocks(blocks) @@ -185,9 +185,9 @@ describe('parseBlocks span-identity tree', () => { it('creates distinct groups for repeated deploy invocations (no collision)', () => { const blocks: ContentBlock[] = [ subagentStart('deploy', 'S2', 'main'), - subagentToolCall('t1', 'deploy_api', 'S2', 'deploy'), + subagentToolCall('t1', 'deploy_as_api', 'S2', 'deploy'), subagentStart('deploy', 'S4', 'main'), - subagentToolCall('t2', 'deploy_api', 'S4', 'deploy'), + subagentToolCall('t2', 'deploy_as_api', 'S4', 'deploy'), ] const segments = parseBlocks(blocks) @@ -311,7 +311,7 @@ describe('parseBlocks span-identity tree', () => { it('absorbs the dispatch tool of a nested file subagent from its parent span group', () => { const blocks: ContentBlock[] = [ subagentStart('workflow', 'S1', 'main'), - subagentToolCall('t1', 'workspace_file', 'S1', 'workflow'), + subagentToolCall('t1', 'prepare_file_edit', 'S1', 'workflow'), { type: 'subagent', content: 'file', spanId: 'S2', parentSpanId: 'S1', timestamp: 2 }, { type: 'subagent_text', content: 'writing', spanId: 'S2', timestamp: 3 }, ] @@ -321,7 +321,7 @@ describe('parseBlocks span-identity tree', () => { const workflow = segments[0] if (workflow.type !== 'agent_group') throw new Error('expected workflow group') - // The workspace_file dispatch tool is absorbed (not shown as a sibling tool); + // The prepare_file_edit dispatch tool is absorbed (not shown as a sibling tool); // only the nested file subagent remains under workflow. expect(workflow.items.some((item) => item.type === 'tool')).toBe(false) const nested = workflow.items.find((item) => item.type === 'agent_group') @@ -476,7 +476,7 @@ describe('completed tool titles', () => { type: 'tool_call', toolCall: { id: 'undeploy-api', - name: 'deploy_api', + name: 'deploy_as_api', status: 'success', params: { action: 'undeploy' }, }, @@ -485,7 +485,7 @@ describe('completed tool titles', () => { ]) ).toBe('Undeployed API') - expect(firstToolTitle([mainToolCall('deploy-mcp', 'deploy_mcp')])).toBe('Deployed MCP tool') + expect(firstToolTitle([mainToolCall('deploy-mcp', 'deploy_as_mcp')])).toBe('Deployed MCP tool') }) it('renders Compared after the full diff_workflows wire lifecycle succeeds', () => { @@ -755,7 +755,7 @@ describe('assistantMessageHasVisibleExecutingTool', () => { const blocks: ContentBlock[] = [ { type: 'tool_call', - toolCall: { id: 'dispatch-1', name: 'workspace_file', status: 'executing' }, + toolCall: { id: 'dispatch-1', name: 'prepare_file_edit', status: 'executing' }, timestamp: 1, }, { @@ -785,7 +785,7 @@ describe('deriveThinkingLabel', () => { it('shows Dispatching for the dispatch call, then yields to the opened lane', () => { expect(deriveThinkingLabel([mainToolCall('t1', 'workflow')])).toBe('Dispatching…') - expect(deriveThinkingLabel([mainToolCall('t1', 'workspace_file')])).toBe('Dispatching…') + expect(deriveThinkingLabel([mainToolCall('t1', 'prepare_file_edit')])).toBe('Dispatching…') expect(deriveThinkingLabel([mainToolCall('t1', 'grep')])).toBe('Thinking…') expect(deriveThinkingLabel([subagentStart('workflow', 'S1', 'main')])).toBeNull() }) 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 4b230fb4fd..24dfc3fd84 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 @@ -11,7 +11,7 @@ import { useState, } from 'react' import { cn } from '@sim/emcn' -import { Read as ReadTool, WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' +import { PrepareFileEdit, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools' import { resolveToolDisplay } from '@/lib/copilot/tools/client/store-utils' import { ClientToolCallState } from '@/lib/copilot/tools/client/tool-call-state' @@ -127,7 +127,7 @@ const SUBAGENT_KEYS = new Set(Object.keys(SUBAGENT_LABELS)) * group is absorbed so it doesn't render as a separate Mothership entry. */ const SUBAGENT_DISPATCH_TOOLS: Record = { - [FILE_SUBAGENT_ID]: WorkspaceFile.id, + [FILE_SUBAGENT_ID]: PrepareFileEdit.id, } function isToolResultRead(params?: Record): boolean { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts index 8079fc40de..1617998344 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/utils.ts @@ -33,19 +33,19 @@ const TOOL_ICONS: Record = { mv: FolderCode, cp: Layout, mkdir: FolderCode, - search_online: Search, - scrape_page: Search, - get_page_contents: Search, + web_search: Search, + web_scrape: Search, + web_fetch: Search, search_library_docs: Library, - manage_mcp_tool: Settings, + manage_mcp_connection: Settings, manage_skill: Asterisk, user_memory: Database, - function_execute: TerminalWindow, + run_function: TerminalWindow, run_code: TerminalWindow, superagent: Blimp, user_table: TableIcon, - workspace_file: File, - edit_content: File, + prepare_file_edit: File, + apply_file_edit: File, create_workflow: Layout, edit_workflow: Pencil, workflow: Hammer, diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 4378ba9a3c..48f3769b17 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -256,6 +256,7 @@ export const ResourceContent = memo(function ResourceContent({ tableId={resource.id} embedded viewsEnabled={tableViewsEnabled} + initialViewId={resource.viewId} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts index e00e851626..2f23df785b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/handle-tool-event.ts @@ -4,7 +4,7 @@ import { MothershipStreamV1ToolPhase, MothershipStreamV1ToolStatus, } from '@/lib/copilot/generated/mothership-stream-v1' -import { WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' +import { ApplyFileEdit, PrepareFileEdit } from '@/lib/copilot/generated/tool-catalog-v1' import type { PersistedStreamEventEnvelope } from '@/lib/copilot/request/session/contract' import { extractResourcesFromToolResult, @@ -77,7 +77,7 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode): void invalidateResourceQueries(deps.queryClient, deps.workspaceId, resource.type, resource.id) } - if ((name === 'edit_content' || name === WorkspaceFile.id) && isSuccess) { + if ((name === ApplyFileEdit.id || name === PrepareFileEdit.id) && isSuccess) { const out = output as Record | undefined const editData = out && typeof out.data === 'object' && out.data !== null @@ -100,17 +100,20 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode): void deps.onToolResultRef.current?.(name, isSuccess, output) const workspaceFileOperation = - name === WorkspaceFile.id && typeof params?.operation === 'string' + name === PrepareFileEdit.id && typeof params?.operation === 'string' ? params.operation : undefined const shouldKeepWorkspacePreviewOpen = - name === WorkspaceFile.id && + name === PrepareFileEdit.id && (workspaceFileOperation === 'append' || workspaceFileOperation === 'update' || workspaceFileOperation === 'patch') - if ((name === WorkspaceFile.id || name === 'edit_content') && !shouldKeepWorkspacePreviewOpen) { - if (name === WorkspaceFile.id) { + if ( + (name === PrepareFileEdit.id || name === ApplyFileEdit.id) && + !shouldKeepWorkspacePreviewOpen + ) { + if (name === PrepareFileEdit.id) { deps.removePreviewSessionImmediate(node.id) } const fileResource = extractedResources.find((r) => r.type === 'file') @@ -126,7 +129,7 @@ function runToolResultSideEffects(ctx: StreamLoopContext, node: ToolNode): void /** * Side effects for tool events. State (the tool node, its status, args, and the - * edit_content row merge) is owned by `reduceEvent`; this handler routes preview + * apply_file_edit row merge) is owned by `reduceEvent`; this handler routes preview * phases, fires client workflow tools, and runs result side effects, then * flushes the model-derived snapshot. */ diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts index 90e8d0f9ca..2abb9ef6a7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/stream-helpers.ts @@ -2,30 +2,30 @@ import { createLogger } from '@sim/logger' import { isRecordLike } from '@sim/utils/object' import { CallIntegrationTool, - CrawlWebsite, - CreateFile, + CreateEmptyFile, CreateWorkflow, - DeployApi, - DeployChat, - DeployMcp, + DeployAsApi, + DeployAsChat, + DeployAsMcp, EditWorkflow, - FunctionExecute, Glob, Grep, ManageCredential, ManageCustomTool, - ManageMcpTool, + ManageMcpConnection, ManageSkill, + PrepareFileEdit, + PrepareFileEditOperation, QueryLogs, Redeploy, Rm, RunFromBlock, + RunFunction, RunWorkflow, RunWorkflowUntilBlock, - ScrapePage, - SearchOnline, - WorkspaceFile, - WorkspaceFileOperation, + WebCrawl, + WebScrape, + WebSearch, } from '@/lib/copilot/generated/tool-catalog-v1' import { extractStreamingStringArgument } from '@/lib/copilot/tools/streaming-args' import { getToolDisplayTitle, mvDisplayVerb } from '@/lib/copilot/tools/tool-display' @@ -40,9 +40,9 @@ const logger = createLogger('StreamHelpers') export const FILE_SUBAGENT_ID = 'file' export const DEPLOY_TOOL_NAMES: Set = new Set([ - DeployApi.id, - DeployChat.id, - DeployMcp.id, + DeployAsApi.id, + DeployAsChat.id, + DeployAsMcp.id, Redeploy.id, ]) @@ -139,13 +139,13 @@ function resolveWorkspaceFileDisplayTitle( let verb = 'Writing' switch (operation) { - case WorkspaceFileOperation.append: + case PrepareFileEditOperation.append: verb = 'Adding' break - case WorkspaceFileOperation.patch: + case PrepareFileEditOperation.patch: verb = 'Editing' break - case WorkspaceFileOperation.update: + case PrepareFileEditOperation.update: verb = 'Writing' break } @@ -270,11 +270,11 @@ export function resolveStreamingToolDisplayTitle( name: string, streamingArgs: string ): string | undefined { - if (name === FunctionExecute.id) { + if (name === RunFunction.id) { return functionExecuteTitle(matchStreamingStringArg(streamingArgs, 'title')) } - if (name === WorkspaceFile.id) { + if (name === PrepareFileEdit.id) { return resolveWorkspaceFileDisplayTitle( matchStreamingStringArg(streamingArgs, 'operation'), matchStreamingStringArg(streamingArgs, 'title'), @@ -282,7 +282,7 @@ export function resolveStreamingToolDisplayTitle( ) } - if (name === CreateFile.id) { + if (name === CreateEmptyFile.id) { const target = matchStreamingStringArg(streamingArgs, 'path') ?? matchStreamingStringArg(streamingArgs, 'fileName') @@ -299,7 +299,7 @@ export function resolveStreamingToolDisplayTitle( return workflowId ? resolveToolDisplayTitle(name, { workflowId }) : undefined } - if (name === SearchOnline.id) { + if (name === WebSearch.id) { const toolTitle = matchStreamingStringArg(streamingArgs, 'toolTitle') return toolTitle ? `Searching online for ${toolTitle}` : undefined } @@ -349,12 +349,12 @@ export function resolveStreamingToolDisplayTitle( return toolTitle ? `Deleting ${toolTitle}` : undefined } - if (name === ScrapePage.id) { + if (name === WebScrape.id) { const url = matchStreamingStringArg(streamingArgs, 'url') return url ? `Scraping ${url}` : undefined } - if (name === CrawlWebsite.id) { + if (name === WebCrawl.id) { const url = matchStreamingStringArg(streamingArgs, 'url') return url ? `Crawling ${url}` : undefined } @@ -363,7 +363,7 @@ export function resolveStreamingToolDisplayTitle( return resolveStreamingManagedResourceTitle(name, streamingArgs, ['toolTitle', 'title', 'name']) } - if (name === ManageMcpTool.id) { + if (name === ManageMcpConnection.id) { return resolveStreamingManagedResourceTitle(name, streamingArgs, [ 'serverName', 'name', diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts index d4a5b69e3a..a764b32fa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model-serialize.test.ts @@ -68,7 +68,7 @@ describe('streaming resource titles', () => { }) // A main-agent file delegation: trigger tool (main lane), subagent span, inner -// workspace_file, span end, delegation result. +// prepare_file_edit, span end, delegation result. function fileDelegationEvents(): PersistedStreamEventEnvelope[] { const sub: Scope = { lane: 'subagent', @@ -89,13 +89,13 @@ function fileDelegationEvents(): PersistedStreamEventEnvelope[] { env( 4, 'tool', - { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { phase: 'call', toolCallId: 'wf-1', toolName: 'prepare_file_edit' }, { lane: 'subagent', spanId: 'S1' } ), env( 5, 'tool', - { phase: 'result', toolCallId: 'wf-1', toolName: 'workspace_file', success: true }, + { phase: 'result', toolCallId: 'wf-1', toolName: 'prepare_file_edit', success: true }, { lane: 'subagent', spanId: 'S1' } ), env( @@ -124,7 +124,7 @@ describe('modelToContentBlocks', () => { expect(trigger?.toolCall?.status).toBe('success') const innerTool = blocksByType(blocks, 'tool_call').find( - (b) => b.toolCall?.name === 'workspace_file' + (b) => b.toolCall?.name === 'prepare_file_edit' ) expect(innerTool?.spanId).toBe('S1') expect(innerTool?.toolCall?.calledBy).toBe('file') @@ -220,7 +220,7 @@ describe('modelToContentBlocks', () => { env( 4, 'tool', - { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { phase: 'call', toolCallId: 'wf-1', toolName: 'prepare_file_edit' }, { lane: 'subagent', spanId: 'S1' } ), env( @@ -233,7 +233,7 @@ describe('modelToContentBlocks', () => { ]) ) const types = blocks.map((b) => b.type) - const innerIdx = blocks.findIndex((b) => b.toolCall?.name === 'workspace_file') + const innerIdx = blocks.findIndex((b) => b.toolCall?.name === 'prepare_file_edit') const endIdx = types.indexOf('subagent_end') const afterIdx = blocks.findIndex((b) => b.type === 'text' && b.content === 'after') // subagent_end sits after the inner work and before the trailing main text — no sibling jumps. @@ -275,7 +275,7 @@ describe('modelToContentBlocks', () => { env( 3, 'tool', - { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { phase: 'call', toolCallId: 'wf-1', toolName: 'prepare_file_edit' }, { lane: 'subagent', spanId: 'S1' } ), ]) @@ -309,7 +309,7 @@ describe('modelToContentBlocks', () => { env(1, 'tool', { phase: 'call', toolCallId: 'wf', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', arguments: { operation: 'create', title: 'My Doc' }, }), ]) @@ -322,7 +322,7 @@ describe('modelToContentBlocks', () => { const blocks = modelToContentBlocks(build(fileDelegationEvents())) const startIdx = blocks.findIndex((b) => b.type === 'subagent') const innerIdx = blocks.findIndex( - (b) => b.type === 'tool_call' && b.toolCall?.name === 'workspace_file' + (b) => b.type === 'tool_call' && b.toolCall?.name === 'prepare_file_edit' ) const endIdx = blocks.findIndex((b) => b.type === 'subagent_end') expect(startIdx).toBeGreaterThanOrEqual(0) @@ -425,7 +425,7 @@ describe('contentBlocksToModel round-trip', () => { env( 3, 'tool', - { phase: 'call', toolCallId: 'wf-1', toolName: 'workspace_file' }, + { phase: 'call', toolCallId: 'wf-1', toolName: 'prepare_file_edit' }, { lane: 'subagent', spanId: 'S1' } ), ]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts index 4681ae8e40..971ecd57b8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.test.ts @@ -144,17 +144,17 @@ describe('reduceEvent — tool lifecycle', () => { it('accumulates streaming args across deltas', () => { const m = apply([ - toolCall(1, 'tc-1', 'workspace_file'), + toolCall(1, 'tc-1', 'prepare_file_edit'), envelope(2, 'tool', { phase: 'args_delta', toolCallId: 'tc-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', argumentsDelta: '{"a":', }), envelope(3, 'tool', { phase: 'args_delta', toolCallId: 'tc-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', argumentsDelta: '1}', }), ]) @@ -164,11 +164,11 @@ describe('reduceEvent — tool lifecycle', () => { it('clears streamingArgs once the result settles the tool', () => { const m = apply([ - toolCall(1, 'tc-1', 'workspace_file'), + toolCall(1, 'tc-1', 'prepare_file_edit'), envelope(2, 'tool', { phase: 'args_delta', toolCallId: 'tc-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', argumentsDelta: '{"operation":"create"', }), toolResult(3, 'tc-1', true), @@ -213,11 +213,11 @@ describe('reduceEvent — tool lifecycle', () => { it('ignores preview phases (decoupled from tool status)', () => { const m = apply([ - toolCall(1, 'tc-1', 'workspace_file'), + toolCall(1, 'tc-1', 'prepare_file_edit'), envelope(2, 'tool', { previewPhase: 'file_preview_content', toolCallId: 'tc-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', content: 'x', contentMode: 'delta', fileName: 'f', @@ -270,8 +270,8 @@ describe('reduceEvent — subagent lifecycle', () => { const m = apply([ spanStart(1, 'S1', 'file', 'tc-a'), spanStart(2, 'S2', 'file', 'tc-b'), - toolCall(3, 'wf-a', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), - toolCall(4, 'wf-b', 'workspace_file', { lane: 'subagent', spanId: 'S2' }), + toolCall(3, 'wf-a', 'prepare_file_edit', { lane: 'subagent', spanId: 'S1' }), + toolCall(4, 'wf-b', 'prepare_file_edit', { lane: 'subagent', spanId: 'S2' }), toolResult(5, 'wf-a', true), spanEnd(6, 'S1', 'file'), toolResult(7, 'wf-b', true), @@ -327,7 +327,7 @@ describe('reduceEvent — idempotency', () => { it('rebuilds the identical model when replayed into a fresh model', () => { const events = [ spanStart(1, 'S1', 'file', 'tc-file'), - toolCall(2, 'wf', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), + toolCall(2, 'wf', 'prepare_file_edit', { lane: 'subagent', spanId: 'S1' }), toolResult(3, 'wf', true), spanEnd(4, 'S1', 'file'), complete(5), @@ -340,42 +340,42 @@ describe('reduceEvent — idempotency', () => { }) }) -describe('reduceEvent — edit_content row merge', () => { - it('folds an edit_content write into its span workspace_file row', () => { +describe('reduceEvent — apply_file_edit row merge', () => { + it('folds an apply_file_edit write into its span prepare_file_edit row', () => { const sub: Scope = { lane: 'subagent', spanId: 'S1' } const m = apply([ spanStart(1, 'S1', 'file', 'tc-file'), - toolCall(2, 'wf-1', 'workspace_file', sub), + toolCall(2, 'wf-1', 'prepare_file_edit', sub), toolResult(3, 'wf-1', true, undefined, sub), - toolCall(4, 'ec-1', 'edit_content', sub), + toolCall(4, 'ec-1', 'apply_file_edit', sub), ]) - // No separate edit_content node; the workspace_file row reopened for the edit. + // No separate apply_file_edit node; the prepare_file_edit row reopened for the edit. expect(m.nodes.has('ec-1')).toBe(false) expect(tool(m, 'wf-1').status).toBe('running') expect(m.toolAlias.get('ec-1')).toBe('wf-1') }) - it('settles the merged row on the edit_content result', () => { + it('settles the merged row on the apply_file_edit result', () => { const sub: Scope = { lane: 'subagent', spanId: 'S1' } const m = apply([ spanStart(1, 'S1', 'file', 'tc-file'), - toolCall(2, 'wf-1', 'workspace_file', sub), - toolCall(3, 'ec-1', 'edit_content', sub), + toolCall(2, 'wf-1', 'prepare_file_edit', sub), + toolCall(3, 'ec-1', 'apply_file_edit', sub), toolResult(4, 'ec-1', true, undefined, sub), ]) expect(tool(m, 'wf-1').status).toBe('success') expect(m.nodes.has('ec-1')).toBe(false) }) - it('folds an edit_content result that raced ahead of its call into the merged row', () => { + it('folds an apply_file_edit result that raced ahead of its call into the merged row', () => { const sub: Scope = { lane: 'subagent', spanId: 'S1' } const m = apply([ spanStart(1, 'S1', 'file', 'tc-file'), - toolCall(2, 'wf-1', 'workspace_file', sub), - // Result for edit_content arrives BEFORE its call (buffered under ec-1)... + toolCall(2, 'wf-1', 'prepare_file_edit', sub), + // Result for apply_file_edit arrives BEFORE its call (buffered under ec-1)... toolResult(3, 'ec-1', true, undefined, sub), // ...then the call lands and aliases ec-1 -> wf-1, draining the buffer. - toolCall(4, 'ec-1', 'edit_content', sub), + toolCall(4, 'ec-1', 'apply_file_edit', sub), ]) expect(tool(m, 'wf-1').status).toBe('success') expect(tool(m, 'wf-1').result?.success).toBe(true) @@ -386,13 +386,13 @@ describe('reduceEvent — edit_content row merge', () => { const sub: Scope = { lane: 'subagent', spanId: 'S1' } const m = apply([ spanStart(1, 'S1', 'file', 'tc-file'), - // Section 1: the workspace_file row is reopened by its edit_content, but the + // Section 1: the prepare_file_edit row is reopened by its apply_file_edit, but the // edit's closing result is reordered/dropped — wf-1 is left running. - toolCall(2, 'wf-1', 'workspace_file', sub), + toolCall(2, 'wf-1', 'prepare_file_edit', sub), toolResult(3, 'wf-1', true, undefined, sub), - toolCall(4, 'ec-1', 'edit_content', sub), + toolCall(4, 'ec-1', 'apply_file_edit', sub), // Section 2 opens before section 1's edit result lands. - toolCall(5, 'wf-2', 'workspace_file', sub), + toolCall(5, 'wf-2', 'prepare_file_edit', sub), ]) // The previous section settles instead of spinning until the turn terminal... expect(tool(m, 'wf-1').status).toBe('success') @@ -498,7 +498,7 @@ describe('turn-terminal propagation', () => { // A file subagent opened but no span end arrived (mid-stream error/disconnect). const m = apply([ spanStart(1, 'S1', 'file', 'tc-file'), - toolCall(2, 'wf-1', 'workspace_file', { lane: 'subagent', spanId: 'S1' }), + toolCall(2, 'wf-1', 'prepare_file_edit', { lane: 'subagent', spanId: 'S1' }), ]) expect(agent(m, 'S1').endSeq).toBeUndefined() applyTurnTerminal(m, 'error') diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts index 6ac6b32e3f..d636bf8b47 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/stream/turn-model.ts @@ -121,7 +121,7 @@ export interface TurnModel { > /** * Maps a tool call id to another tool node it folds into. Used for the - * `edit_content` -> `workspace_file` row merge so the write streams into the + * `apply_file_edit` -> `prepare_file_edit` row merge so the write streams into the * single "writing" row rather than a second row. */ toolAlias: Map @@ -142,16 +142,16 @@ export function createTurnModel(): TurnModel { } } -const WORKSPACE_FILE_TOOL = 'workspace_file' -const EDIT_CONTENT_TOOL = 'edit_content' +const WORKSPACE_FILE_TOOL = 'prepare_file_edit' +const EDIT_CONTENT_TOOL = 'apply_file_edit' -/** Resolves a tool call id through the alias map (e.g. edit_content -> its workspace_file row). */ +/** Resolves a tool call id through the alias map (e.g. apply_file_edit -> its prepare_file_edit row). */ export function resolveToolId(model: TurnModel, id: string): string { return model.toolAlias.get(id) ?? id } /** - * Finds the most recent `workspace_file` tool node in a span so an `edit_content` + * Finds the most recent `prepare_file_edit` tool node in a span so an `apply_file_edit` * write folds into it (the single "writing" row). Co-location in the file * subagent's span is the link — no coupling to preview phases. The caller * reopens whatever this returns, including an already-settled row (an edit after @@ -169,11 +169,11 @@ function findWorkspaceFileNodeInSpan(model: TurnModel, spanId: string): ToolNode } /** - * The file agent writes a file as strictly sequential `workspace_file` + - * `edit_content` section pairs, waiting for each to finish before the next. So - * when a new section's `workspace_file` opens, any earlier `workspace_file` row + * The file agent writes a file as strictly sequential `prepare_file_edit` + + * `apply_file_edit` section pairs, waiting for each to finish before the next. So + * when a new section's `prepare_file_edit` opens, any earlier `prepare_file_edit` row * still `running` in the same span is a completed section whose closing - * `edit_content` result was reordered or dropped — finalize it as success so its + * `apply_file_edit` result was reordered or dropped — finalize it as success so its * "writing" spinner resolves when the next section starts, instead of lingering * until the turn-terminal sweep. A no-op on the happy path (prior rows already * settled on their own result). @@ -331,8 +331,8 @@ function appendText( /** * Applies a result that raced ahead of its tool `call` (buffered under `fromId`) * onto `node`, then clears the buffer. Used by the normal call path and by the - * edit_content -> workspace_file merge, where the buffer is keyed by the - * edit_content id but folds into the workspace_file row. + * apply_file_edit -> prepare_file_edit merge, where the buffer is keyed by the + * apply_file_edit id but folds into the prepare_file_edit row. */ function drainBufferedResult(model: TurnModel, fromId: string, node: ToolNode): void { const buffered = model.bufferedResults.get(fromId) @@ -473,12 +473,12 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve ensureSubagentLane(model, spanId, scope, seq, tsMs) const phase = payload.phase if (phase === MothershipStreamV1ToolPhase.call) { - // edit_content folds into its span's workspace_file row (the write + // apply_file_edit folds into its span's prepare_file_edit row (the write // continues in the single "writing" row), reopening it for the edit. if (toolName === EDIT_CONTENT_TOOL) { - // A re-emitted edit_content call (same tool call id — duplicate/replay) + // A re-emitted apply_file_edit call (same tool call id — duplicate/replay) // must keep its ORIGINAL target row. Re-running the span lookup can - // return a newer workspace_file, and folding into that would leave the + // return a newer prepare_file_edit, and folding into that would leave the // first (already reopened) row running with no result ever closing it — // a spinner stuck until the turn-terminal sweep. So once aliased, reuse. const aliasedId = model.toolAlias.get(rawToolCallId) @@ -492,7 +492,7 @@ export function reduceEvent(model: TurnModel, envelope: PersistedStreamEventEnve parent.status = 'running' parent.result = undefined // A result that raced ahead of this call was buffered under the - // edit_content id; fold it into the reopened workspace_file row. + // apply_file_edit id; fold it into the reopened prepare_file_edit row. drainBufferedResult(model, rawToolCallId, parent) break } diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index e6d21c2776..f29c7b8612 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -1,7 +1,7 @@ import type { ChatContext } from '@/stores/panel' import type { BrowserTextSelection, TerminalTextSelection } from '@/stores/panel/types' -const EDIT_CONTENT_TOOL_ID = 'edit_content' +const EDIT_CONTENT_TOOL_ID = 'apply_file_edit' const RUN_SUBAGENT_ID = 'run' export type { @@ -191,7 +191,10 @@ export const SUBAGENT_LABELS: Record = { search: 'Search Agent', superagent: 'Superagent', run: 'Run Agent', - agent: 'Tools Agent', + // The extensions subagent's wire/scope AgentID stays `agent` (pre-rename); + // `extensions` is its current model-facing trigger tool name. + agent: 'Extensions Agent', + extensions: 'Extensions Agent', // `job` retained as a backward-compat alias so historical transcripts still render a label. job: 'Job Agent', file: 'File Agent', diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 8db321108d..d9f04de5a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -115,6 +115,13 @@ interface TableProps { * context to resolve it — stays on today's Filter/Sort bar. */ viewsEnabled?: boolean + /** + * Saved view to adopt on first seed instead of the table's default — + * embedded mode only, set when the agent opened this table pinned to a + * view. Participates only in the one-time adoption branch, so it never + * fights a later user switch. + */ + initialViewId?: string } /** @@ -213,6 +220,7 @@ function isSameViewConfig(a: TableViewConfig, b: TableViewConfig): boolean { */ export function Table({ embedded, + initialViewId, workspaceId: propWorkspaceId, tableId: propTableId, tableLocksEnabled = false, @@ -546,7 +554,9 @@ export function Table({ !views.some((view) => view.id === activeViewId) if (activeViewId === null || inheritedParams) { - const defaultView = views.find((view) => view.isDefault) + const pinnedView = + embedded && initialViewId ? views.find((view) => view.id === initialViewId) : undefined + const defaultView = pinnedView ?? views.find((view) => view.isDefault) // `sort` rides the same host URL, so when the view id is inherited the // sort beside it is too — not local work, and it must not suppress the // default view's own sort. diff --git a/apps/sim/lib/api/contracts/custom-blocks.ts b/apps/sim/lib/api/contracts/custom-blocks.ts index 70f40a1132..99ec47a5f0 100644 --- a/apps/sim/lib/api/contracts/custom-blocks.ts +++ b/apps/sim/lib/api/contracts/custom-blocks.ts @@ -83,7 +83,7 @@ export const listCustomBlocksQuerySchema = z.object({ * Icon URLs are rendered as org-wide `` sources, so only https URLs and * internal file-serve paths (what the icon upload UI stores) are accepted — * never data:/blob:/other schemes an admin could smuggle into shared metadata. - * Shared with the copilot deploy_custom_block handler's pass-through branch. + * Shared with the copilot publish_custom_block handler's pass-through branch. */ export function isAllowedCustomBlockIconUrl(value: string): boolean { return value.startsWith('https://') || value.startsWith('/api/files/serve/') diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index 44345553e4..50c11d0ab2 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -702,7 +702,7 @@ export async function hasWorkspaceLiveSyncAccess(workspaceId: string): Promise { try { diff --git a/apps/sim/lib/copilot/application/table-commands.test.ts b/apps/sim/lib/copilot/application/table-commands.test.ts index 900f8b76df..f1d734dec1 100644 --- a/apps/sim/lib/copilot/application/table-commands.test.ts +++ b/apps/sim/lib/copilot/application/table-commands.test.ts @@ -12,7 +12,7 @@ const mocks = vi.hoisted(() => ({ createFromFile: { operation: { id: 'tables.imports.create_from_workspace_file' } }, createWorkflowGroup: { operation: { id: 'tables.groups.create' } }, deleteTables: { operation: { id: 'tables.delete' } }, - importFile: { operation: { id: 'tables.imports.workspace_file' } }, + importFile: { operation: { id: 'tables.imports.prepare_file_edit' } }, replaceProjectedRows: { operation: { id: 'tables.rows.replace' } }, updateWorkflowGroup: { operation: { id: 'tables.groups.update' } }, }, diff --git a/apps/sim/lib/copilot/async-runs/repository.test.ts b/apps/sim/lib/copilot/async-runs/repository.test.ts index e6098a8b85..81bdd06198 100644 --- a/apps/sim/lib/copilot/async-runs/repository.test.ts +++ b/apps/sim/lib/copilot/async-runs/repository.test.ts @@ -282,7 +282,7 @@ describe('async tool repository single-row semantics', () => { const existingRow = { runId: 'run-1', toolCallId: 'tool-1', - toolName: 'function_execute', + toolName: 'run_function', args: { language: 'javascript', code: 'return {{FIRST_SECRET}}' }, status, } @@ -291,7 +291,7 @@ describe('async tool repository single-row semantics', () => { const result = await upsertAsyncToolCall({ runId: 'run-1', toolCallId: 'tool-1', - toolName: 'function_execute', + toolName: 'run_function', args: { language: 'javascript', code: 'return {{SECOND_SECRET}}' }, status: 'pending', }) diff --git a/apps/sim/lib/copilot/chat/display-message.ts b/apps/sim/lib/copilot/chat/display-message.ts index 24df088f59..443d517a7b 100644 --- a/apps/sim/lib/copilot/chat/display-message.ts +++ b/apps/sim/lib/copilot/chat/display-message.ts @@ -138,17 +138,17 @@ function toDisplayContexts( })) } -const WORKSPACE_FILE_TOOL = 'workspace_file' -const EDIT_CONTENT_TOOL = 'edit_content' +const WORKSPACE_FILE_TOOL = 'prepare_file_edit' +const EDIT_CONTENT_TOOL = 'apply_file_edit' const MAIN_SPAN = 'main' /** - * Collapses an `edit_content` write into the most-recent `workspace_file` row in + * Collapses an `apply_file_edit` write into the most-recent `prepare_file_edit` row in * the same subagent span, mirroring the live turn-model fold. The live view * folds these in `reduceEvent`, but the persisted transcript stores them as two * separate tool blocks; without this a reloaded chat splits the file write into - * "workspace_file" + "edit_content" rows (and a refresh mid-write leaves the - * second row spinning). The reopened row inherits the edit_content's final + * "prepare_file_edit" + "apply_file_edit" rows (and a refresh mid-write leaves the + * second row spinning). The reopened row inherits the apply_file_edit's final * status/result, exactly as the live single "writing" row resolves. Every other * block is passed through untouched, so this only affects file writes. */ diff --git a/apps/sim/lib/copilot/chat/effective-transcript.test.ts b/apps/sim/lib/copilot/chat/effective-transcript.test.ts index 10c74da054..23910ed66c 100644 --- a/apps/sim/lib/copilot/chat/effective-transcript.test.ts +++ b/apps/sim/lib/copilot/chat/effective-transcript.test.ts @@ -242,7 +242,7 @@ describe('buildEffectiveChatTranscript', () => { payload: { phase: 'result', toolCallId: 'tool-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', executor: 'go', mode: 'sync', success: false, @@ -262,7 +262,7 @@ describe('buildEffectiveChatTranscript', () => { type: MothershipStreamV1EventType.tool, toolCall: expect.objectContaining({ id: 'tool-1', - name: 'workspace_file', + name: 'prepare_file_edit', state: MothershipStreamV1CompletionStatus.cancelled, }), }), diff --git a/apps/sim/lib/copilot/chat/payload.test.ts b/apps/sim/lib/copilot/chat/payload.test.ts index 2f061cf859..4ec333268b 100644 --- a/apps/sim/lib/copilot/chat/payload.test.ts +++ b/apps/sim/lib/copilot/chat/payload.test.ts @@ -368,7 +368,7 @@ describe('buildCopilotRequestPayload', () => { content: [ 'File "payroll.xlsx" (application/octet-stream, 1 bytes) uploaded.', 'Read with: read("uploads/payroll.xlsx")', - 'To save permanently: materialize_file(fileName: "payroll.xlsx")', + 'To save permanently: save_upload(fileName: "payroll.xlsx")', ].join('\n'), }, ]) @@ -410,7 +410,7 @@ describe('buildCopilotRequestPayload', () => { content: [ 'File "photo.png" (image/png, 10 bytes) uploaded.', 'Read with: read("uploads/photo.png")', - 'To save permanently: materialize_file(fileName: "photo.png")', + 'To save permanently: save_upload(fileName: "photo.png")', ].join('\n'), }, ]) diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index ef1ea48d15..5f185b683f 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -362,7 +362,7 @@ export async function buildCopilotRequestPayload( userMessageId ) // Encode the read path per the percent-encoded VFS convention (matches - // files/ and the uploads glob output). The materialize_file `fileName` + // files/ and the uploads glob output). The save_upload `fileName` // arg stays the raw display name — the upload resolver accepts both. let encodedUploadName = displayName try { @@ -382,11 +382,11 @@ export async function buildCopilotRequestPayload( lines = [ `File "${displayName}" (${mediaType}, ${f.size} bytes) uploaded.`, `Read with: read("uploads/${encodedUploadName}")`, - `To save permanently: materialize_file(fileName: "${displayName}")`, + `To save permanently: save_upload(fileName: "${displayName}")`, ] if (displayName.endsWith('.json')) { lines.push( - `To import as a workflow: materialize_file(fileName: "${displayName}", operation: "import")` + `To import as a workflow: save_upload(fileName: "${displayName}", operation: "import")` ) } } diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 3d522d7035..82ec12ead2 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -7,7 +7,7 @@ export interface ToolCatalogEntry { clientExecutable?: boolean hidden?: boolean id: - | 'agent' + | 'apply_file_edit' | 'auth' | 'browser' | 'browser_click' @@ -32,26 +32,21 @@ export interface ToolCatalogEntry { | 'browser_type' | 'browser_wait_for' | 'call_integration_tool' - | 'check_deployment_status' | 'cp' - | 'crawl_website' - | 'create_file' + | 'create_empty_file' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_workspace_mcp_server' | 'deploy' - | 'deploy_api' - | 'deploy_chat' - | 'deploy_custom_block' - | 'deploy_mcp' + | 'deploy_as_api' + | 'deploy_as_chat' + | 'deploy_as_mcp' | 'diff_workflows' - | 'download_to_workspace_file' - | 'edit_content' + | 'download_file' | 'edit_workflow' - | 'enrichment_run' + | 'extensions' | 'ffmpeg' | 'file' - | 'function_execute' | 'generate_api_key' | 'generate_audio' | 'generate_image' @@ -59,15 +54,14 @@ export interface ToolCatalogEntry { | 'get_block_outputs' | 'get_block_upstream_references' | 'get_deployed_workflow_state' - | 'get_deployment_log' - | 'get_page_contents' - | 'get_platform_actions' + | 'get_deployment_status' + | 'get_ui_reference' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' | 'grep' | 'knowledge' - | 'knowledge_base' + | 'list_deployment_versions' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -76,17 +70,19 @@ export interface ToolCatalogEntry { | 'load_skill' | 'manage_credential' | 'manage_custom_tool' - | 'manage_mcp_tool' + | 'manage_knowledge_base' + | 'manage_mcp_connection' | 'manage_sandbox' | 'manage_skill' - | 'materialize_file' | 'media' | 'mkdir' | 'mv' | 'oauth_get_auth_link' | 'oauth_request_access' | 'open_resource' + | 'prepare_file_edit' | 'promote_to_live' + | 'publish_custom_block' | 'query_logs' | 'query_user_table' | 'read' @@ -97,17 +93,17 @@ export interface ToolCatalogEntry { | 'run' | 'run_block' | 'run_code' + | 'run_enrichment' | 'run_from_block' + | 'run_function' | 'run_workflow' | 'run_workflow_until_block' - | 'scrape_page' + | 'save_upload' | 'search' - | 'search_documentation' | 'search_integration_tools' | 'search_knowledge_base' | 'search_library_docs' - | 'search_online' - | 'search_patterns' + | 'search_sim_docs' | 'set_block_enabled' | 'set_environment_variables' | 'set_global_workflow_variables' @@ -118,17 +114,21 @@ export interface ToolCatalogEntry { | 'table_enrichments' | 'table_manage' | 'table_rows' + | 'table_views' | 'terminal' | 'update_deployment_version' | 'update_workspace_mcp_server' | 'user_table' | 'wait' + | 'web_crawl' + | 'web_fetch' + | 'web_scrape' + | 'web_search' | 'workflow' - | 'workspace_file' internal?: boolean mode: 'async' | 'sync' name: - | 'agent' + | 'apply_file_edit' | 'auth' | 'browser' | 'browser_click' @@ -153,26 +153,21 @@ export interface ToolCatalogEntry { | 'browser_type' | 'browser_wait_for' | 'call_integration_tool' - | 'check_deployment_status' | 'cp' - | 'crawl_website' - | 'create_file' + | 'create_empty_file' | 'create_workflow' | 'create_workspace_mcp_server' | 'delete_workspace_mcp_server' | 'deploy' - | 'deploy_api' - | 'deploy_chat' - | 'deploy_custom_block' - | 'deploy_mcp' + | 'deploy_as_api' + | 'deploy_as_chat' + | 'deploy_as_mcp' | 'diff_workflows' - | 'download_to_workspace_file' - | 'edit_content' + | 'download_file' | 'edit_workflow' - | 'enrichment_run' + | 'extensions' | 'ffmpeg' | 'file' - | 'function_execute' | 'generate_api_key' | 'generate_audio' | 'generate_image' @@ -180,15 +175,14 @@ export interface ToolCatalogEntry { | 'get_block_outputs' | 'get_block_upstream_references' | 'get_deployed_workflow_state' - | 'get_deployment_log' - | 'get_page_contents' - | 'get_platform_actions' + | 'get_deployment_status' + | 'get_ui_reference' | 'get_workflow_data' | 'get_workflow_run_options' | 'glob' | 'grep' | 'knowledge' - | 'knowledge_base' + | 'list_deployment_versions' | 'list_integration_tools' | 'list_user_workspaces' | 'list_workspace_mcp_servers' @@ -197,17 +191,19 @@ export interface ToolCatalogEntry { | 'load_skill' | 'manage_credential' | 'manage_custom_tool' - | 'manage_mcp_tool' + | 'manage_knowledge_base' + | 'manage_mcp_connection' | 'manage_sandbox' | 'manage_skill' - | 'materialize_file' | 'media' | 'mkdir' | 'mv' | 'oauth_get_auth_link' | 'oauth_request_access' | 'open_resource' + | 'prepare_file_edit' | 'promote_to_live' + | 'publish_custom_block' | 'query_logs' | 'query_user_table' | 'read' @@ -218,17 +214,17 @@ export interface ToolCatalogEntry { | 'run' | 'run_block' | 'run_code' + | 'run_enrichment' | 'run_from_block' + | 'run_function' | 'run_workflow' | 'run_workflow_until_block' - | 'scrape_page' + | 'save_upload' | 'search' - | 'search_documentation' | 'search_integration_tools' | 'search_knowledge_base' | 'search_library_docs' - | 'search_online' - | 'search_patterns' + | 'search_sim_docs' | 'set_block_enabled' | 'set_environment_variables' | 'set_global_workflow_variables' @@ -239,13 +235,17 @@ export interface ToolCatalogEntry { | 'table_enrichments' | 'table_manage' | 'table_rows' + | 'table_views' | 'terminal' | 'update_deployment_version' | 'update_workspace_mcp_server' | 'user_table' | 'wait' + | 'web_crawl' + | 'web_fetch' + | 'web_scrape' + | 'web_search' | 'workflow' - | 'workspace_file' parameters: unknown requiredPermission?: 'admin' | 'write' requiresApproval?: boolean @@ -265,20 +265,35 @@ export interface ToolCatalogEntry { | 'workflow' } -export const Agent: ToolCatalogEntry = { - id: 'agent', - name: 'agent', - route: 'subagent', +export const ApplyFileEdit: ToolCatalogEntry = { + id: 'apply_file_edit', + name: 'apply_file_edit', + route: 'sim', mode: 'async', parameters: { - properties: { - request: { description: 'What tool/skill/MCP action is needed.', type: 'string' }, - }, - required: ['request'], 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'], }, - subagentId: 'agent', - internal: true, requiredPermission: 'write', } @@ -1244,22 +1259,6 @@ export const CallIntegrationTool: ToolCatalogEntry = { requiresApproval: true, } -export const CheckDeploymentStatus: ToolCatalogEntry = { - id: 'check_deployment_status', - name: 'check_deployment_status', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'Workflow ID to check (defaults to current workflow)', - }, - }, - }, -} - export const Cp: ToolCatalogEntry = { id: 'cp', name: 'cp', @@ -1290,35 +1289,9 @@ export const Cp: ToolCatalogEntry = { requiredPermission: 'write', } -export const CrawlWebsite: ToolCatalogEntry = { - id: 'crawl_website', - name: 'crawl_website', - route: 'go', - mode: 'sync', - parameters: { - type: 'object', - properties: { - exclude_paths: { - type: 'array', - description: 'Skip URLs matching these patterns', - items: { type: 'string' }, - }, - include_paths: { - type: 'array', - description: 'Only crawl URLs matching these patterns', - items: { type: 'string' }, - }, - limit: { type: 'number', description: 'Maximum pages to crawl (default 10, max 50)' }, - max_depth: { type: 'number', description: 'How deep to follow links (default 2)' }, - url: { type: 'string', description: 'Starting URL to crawl from' }, - }, - required: ['url'], - }, -} - -export const CreateFile: ToolCatalogEntry = { - id: 'create_file', - name: 'create_file', +export const CreateEmptyFile: ToolCatalogEntry = { + id: 'create_empty_file', + name: 'create_empty_file', route: 'sim', mode: 'async', parameters: { @@ -1470,9 +1443,9 @@ export const Deploy: ToolCatalogEntry = { internal: true, } -export const DeployApi: ToolCatalogEntry = { - id: 'deploy_api', - name: 'deploy_api', +export const DeployAsApi: ToolCatalogEntry = { + id: 'deploy_as_api', + name: 'deploy_as_api', route: 'sim', mode: 'async', parameters: { @@ -1521,7 +1494,7 @@ export const DeployApi: ToolCatalogEntry = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_api and redeploy this is always "api".', + 'Deployment surface this result describes. For deploy_as_api and redeploy this is always "api".', }, examples: { type: 'object', @@ -1551,9 +1524,9 @@ export const DeployApi: ToolCatalogEntry = { requiresApproval: true, } -export const DeployChat: ToolCatalogEntry = { - id: 'deploy_chat', - name: 'deploy_chat', +export const DeployAsChat: ToolCatalogEntry = { + id: 'deploy_as_chat', + name: 'deploy_as_chat', route: 'sim', mode: 'async', parameters: { @@ -1652,7 +1625,7 @@ export const DeployChat: ToolCatalogEntry = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_chat this is always "chat".', + 'Deployment surface this result describes. For deploy_as_chat this is always "chat".', }, examples: { type: 'object', @@ -1670,7 +1643,7 @@ export const DeployChat: ToolCatalogEntry = { }, success: { type: 'boolean', - description: 'Whether the deploy_chat action completed successfully.', + description: 'Whether the deploy_as_chat action completed successfully.', }, version: { type: 'number', @@ -1697,121 +1670,9 @@ export const DeployChat: ToolCatalogEntry = { requiresApproval: true, } -export const DeployCustomBlock: ToolCatalogEntry = { - id: 'deploy_custom_block', - name: 'deploy_custom_block', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - action: { - type: 'string', - description: 'Whether to publish (deploy) or unpublish (undeploy) the custom block', - enum: ['deploy', 'undeploy'], - default: 'deploy', - }, - description: { - type: 'string', - description: 'Short description shown in the block picker, max 280 characters', - }, - exposedOutputs: { - type: 'array', - description: - "Outputs the block exposes, each mapping a child block output path to a friendly name (use get_block_outputs for valid paths). Omit to expose the terminal block's whole result", - items: { - type: 'object', - properties: { - blockId: { type: 'string', description: 'Block UUID inside the workflow' }, - name: { type: 'string', description: 'Friendly output name shown on the block' }, - path: { - type: 'string', - description: - "Dot-path into that block's output (from get_block_outputs relativeOutputs)", - }, - }, - required: ['blockId', 'path', 'name'], - }, - }, - iconUrl: { - type: 'string', - description: - 'Optional icon image for the block: a workspace file VFS path (e.g. "files/icon.png", copied into public icon storage at publish) or an https image URL. Omit to use the organization\'s default icon', - }, - inputs: { - type: 'array', - description: - "Optional per-input placeholder overrides. Input names and types are derived from the workflow's input trigger and cannot be changed here", - items: { - type: 'object', - properties: { - id: { type: 'string', description: 'Stable id of the input trigger field' }, - placeholder: { - type: 'string', - description: "Placeholder text shown in the block's input field", - }, - }, - required: ['id'], - }, - }, - name: { - type: 'string', - description: - 'Display name for the block, max 60 characters. REQUIRED the first time a workflow is published. When republishing an existing block, omit it to keep the current name or pass a new one to rename. Ignored for undeploy.', - }, - workflowId: { type: 'string', description: 'Workflow ID (defaults to active workflow)' }, - }, - }, - resultSchema: { - type: 'object', - properties: { - action: { - type: 'string', - description: 'Action performed by the tool, such as "deploy" or "undeploy".', - }, - blockId: { type: 'string', description: 'Custom block record ID.' }, - blockType: { - type: 'string', - description: 'Stable block type slug (custom_block_*) used in workflow state.', - }, - deploymentConfig: { - type: 'object', - description: - "Structured deployment configuration keyed by surface name. Includes the block's type, name, description, icon, derived input fields, and exposed outputs.", - }, - deploymentStatus: { - type: 'object', - description: - 'Structured per-surface deployment status keyed by surface name, including customBlock and the underlying api surface when applicable.', - }, - deploymentType: { - type: 'string', - description: - 'Deployment surface this result describes. For deploy_custom_block this is always "custom_block".', - }, - isDeployed: { - type: 'boolean', - description: 'Whether the custom block is published after this tool call.', - }, - name: { type: 'string', description: 'Display name of the custom block.' }, - removed: { - type: 'boolean', - description: 'Whether the custom block was unpublished during an undeploy action.', - }, - updated: { - type: 'boolean', - description: 'Whether an existing custom block was updated instead of created.', - }, - workflowId: { type: 'string', description: 'Workflow ID the custom block is bound to.' }, - }, - required: ['deploymentType', 'deploymentStatus'], - }, - requiredPermission: 'admin', -} - -export const DeployMcp: ToolCatalogEntry = { - id: 'deploy_mcp', - name: 'deploy_mcp', +export const DeployAsMcp: ToolCatalogEntry = { + id: 'deploy_as_mcp', + name: 'deploy_as_mcp', route: 'sim', mode: 'async', parameters: { @@ -1873,7 +1734,7 @@ export const DeployMcp: ToolCatalogEntry = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_mcp this is always "mcp".', + 'Deployment surface this result describes. For deploy_as_mcp this is always "mcp".', }, examples: { type: 'object', @@ -1935,9 +1796,9 @@ export const DiffWorkflows: ToolCatalogEntry = { }, } -export const DownloadToWorkspaceFile: ToolCatalogEntry = { - id: 'download_to_workspace_file', - name: 'download_to_workspace_file', +export const DownloadFile: ToolCatalogEntry = { + id: 'download_file', + name: 'download_file', route: 'sim', mode: 'async', parameters: { @@ -1990,38 +1851,6 @@ export const DownloadToWorkspaceFile: ToolCatalogEntry = { capabilities: ['file_output'], } -export const EditContent: ToolCatalogEntry = { - id: 'edit_content', - name: 'edit_content', - route: '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', @@ -2066,53 +1895,20 @@ export const EditWorkflow: ToolCatalogEntry = { requiredPermission: 'write', } -export const EnrichmentRun: ToolCatalogEntry = { - id: 'enrichment_run', - name: 'enrichment_run', - route: 'sim', +export const Extensions: ToolCatalogEntry = { + id: 'extensions', + name: 'extensions', + route: 'subagent', mode: 'async', parameters: { - type: 'object', properties: { - enrichmentId: { - type: 'string', - description: - "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.", - enum: [ - 'work-email', - 'phone-number', - 'company-domain', - 'company-info', - 'email-verification', - ], - }, - inputs: { - type: 'object', - description: - 'Map of the enrichment\'s input id → value, e.g. { "fullName": "Jane Doe", "companyDomain": "acme.com" }. Provide a value for every required input.', - }, + request: { description: 'What tool/skill/MCP action is needed.', type: 'string' }, }, - required: ['enrichmentId', 'inputs'], - }, - resultSchema: { + required: ['request'], type: 'object', - properties: { - matched: { - type: 'boolean', - description: 'True when a provider returned a non-empty result.', - }, - provider: { - type: ['string', 'null'], - description: - 'Internal label of the provider that produced the result (billing/diagnostics only — do NOT surface it to the user), or null on no match.', - }, - result: { - type: 'object', - description: 'Mapped output values from the winning provider (empty object on no match).', - }, - }, - required: ['matched', 'result'], }, + subagentId: 'agent', + internal: true, requiredPermission: 'write', } @@ -2302,157 +2098,6 @@ export const File: ToolCatalogEntry = { internal: true, } -export const FunctionExecute: ToolCatalogEntry = { - id: 'function_execute', - name: 'function_execute', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - code: { - type: 'string', - description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', - }, - inputs: { - type: 'object', - description: - 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', - properties: { - directories: { - type: 'array', - description: - 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', - items: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', - }, - sandboxPath: { - type: 'string', - description: - 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', - }, - }, - required: ['path'], - }, - }, - files: { - type: 'array', - description: 'Workspace files to mount into the sandbox.', - items: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', - }, - sandboxPath: { - type: 'string', - description: - 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', - }, - }, - required: ['path'], - }, - }, - tables: { - type: 'array', - description: 'Workspace tables to mount as CSV files.', - items: { - type: 'object', - properties: { - path: { type: 'string', description: 'Canonical VFS table path when available.' }, - sandboxPath: { - type: 'string', - description: 'Optional full sandbox path for the mounted CSV.', - }, - tableId: { type: 'string', description: 'Workspace table ID.' }, - }, - }, - }, - }, - }, - language: { - type: 'string', - description: 'Execution language.', - enum: ['javascript', 'python', 'shell'], - }, - outputTable: { - type: 'string', - description: - 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', - }, - outputs: { - type: 'object', - description: - 'Workspace files to create or overwrite from returned code results or sandbox-created files.', - properties: { - files: { - type: 'array', - description: - 'File outputs. Missing parent folders are created automatically for create mode.', - items: { - type: 'object', - properties: { - format: { - type: 'string', - description: 'Optional serialization format for returned values.', - enum: ['json', 'csv', 'txt', 'md', 'html'], - }, - mimeType: { - type: 'string', - description: 'Optional MIME type override when inference is not enough.', - }, - mode: { - type: 'string', - description: 'Create a new file or overwrite an existing file at path.', - enum: ['create', 'overwrite'], - }, - path: { - type: 'string', - description: 'Canonical destination VFS path, e.g. "files/Reports/chart.png".', - }, - sandboxPath: { - type: 'string', - description: - 'Optional full path to a file created inside the sandbox. Omit to save the code return value.', - }, - }, - required: ['path', 'mode'], - }, - }, - }, - }, - sandboxId: { - type: 'string', - description: - 'Optional Sim sandbox id from agent/sandboxes/{name}.json. DEFAULT-FIRST: omit this whenever the documented default function_execute environment can do the job. Select a ready existing Sim sandbox only when a required third-party dependency, Debian system package, or managed CLI is known to be absent, or a default attempt failed specifically because it was missing. Never guess an id.', - }, - timeout: { - type: 'number', - description: - 'Maximum execution time in SECONDS (Sim converts to milliseconds). The sandbox stops execution and returns a timeout error after this duration. Defaults to 10 seconds and is capped at 300 seconds regardless of plan.', - default: 10, - }, - title: { - type: 'string', - description: - 'Short user-visible label for this execution, e.g. "Clean customer CSV", "Revenue chart", or "Query GitHub issues".', - }, - }, - required: ['code'], - }, - requiredPermission: 'write', - requiresApproval: true, - capabilities: ['file_input', 'directory_input', 'file_output', 'table_input', 'table_output'], -} - export const GenerateApiKey: ToolCatalogEntry = { id: 'generate_api_key', name: 'generate_api_key', @@ -2983,9 +2628,9 @@ export const GetDeployedWorkflowState: ToolCatalogEntry = { }, } -export const GetDeploymentLog: ToolCatalogEntry = { - id: 'get_deployment_log', - name: 'get_deployment_log', +export const GetDeploymentStatus: ToolCatalogEntry = { + id: 'get_deployment_status', + name: 'get_deployment_status', route: 'sim', mode: 'async', parameters: { @@ -2993,42 +2638,15 @@ export const GetDeploymentLog: ToolCatalogEntry = { properties: { workflowId: { type: 'string', - description: 'Optional workflow ID. If not provided, uses the current workflow in context.', + description: 'Workflow ID to check (defaults to current workflow)', }, }, }, } -export const GetPageContents: ToolCatalogEntry = { - id: 'get_page_contents', - name: 'get_page_contents', - route: 'go', - mode: 'sync', - parameters: { - type: 'object', - properties: { - include_highlights: { - type: 'boolean', - description: 'Include key highlights (default false)', - }, - include_summary: { - type: 'boolean', - description: 'Include AI-generated summary (default false)', - }, - include_text: { type: 'boolean', description: 'Include full page text (default true)' }, - urls: { - type: 'array', - description: 'URLs to get content from (max 10)', - items: { type: 'string' }, - }, - }, - required: ['urls'], - }, -} - -export const GetPlatformActions: ToolCatalogEntry = { - id: 'get_platform_actions', - name: 'get_platform_actions', +export const GetUiReference: ToolCatalogEntry = { + id: 'get_ui_reference', + name: 'get_ui_reference', route: 'sim', mode: 'async', parameters: { type: 'object', properties: {} }, @@ -3160,203 +2778,19 @@ export const Knowledge: ToolCatalogEntry = { internal: true, } -export const KnowledgeBase: ToolCatalogEntry = { - id: 'knowledge_base', - name: 'knowledge_base', +export const ListDeploymentVersions: ToolCatalogEntry = { + id: 'list_deployment_versions', + name: 'list_deployment_versions', route: 'sim', mode: 'async', parameters: { type: 'object', properties: { - args: { - type: 'object', - description: 'Arguments for the operation', - properties: { - apiKey: { - type: 'string', - description: - 'API key for API-key-based connectors (required when connector auth mode is apiKey)', - }, - chunkingConfig: { - type: 'object', - description: "Chunking configuration (optional for 'create')", - properties: { - maxSize: { - type: 'number', - description: 'Maximum chunk size (100-4000, default: 1024)', - default: 1024, - }, - minSize: { - type: 'number', - description: 'Minimum chunk size (1-2000, default: 1)', - default: 1, - }, - overlap: { - type: 'number', - description: 'Overlap between chunks (0-500, default: 200)', - default: 200, - }, - }, - }, - connectorId: { - type: 'string', - description: - 'Connector ID (required for update_connector, delete_connector, sync_connector)', - }, - connectorStatus: { - type: 'string', - description: 'Connector status (optional for update_connector)', - enum: ['active', 'paused'], - }, - connectorType: { - type: 'string', - description: - "Connector type from registry, e.g. 'confluence', 'google_drive', 'notion' (required for add_connector). Read knowledgebases/connectors/{type}.json for the config schema.", - }, - credentialId: { - type: 'string', - description: - 'OAuth credential ID from environment/credentials.json (required for OAuth connectors)', - }, - description: { - type: 'string', - description: "Description of the knowledge base (optional for 'create')", - }, - disabledTagIds: { - type: 'array', - description: - 'Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema.', - }, - documentId: { type: 'string', description: 'Document ID (required for update_document)' }, - documentIds: { - type: 'array', - description: 'Document IDs (for batch delete_document)', - items: { type: 'string' }, - }, - enabled: { - type: 'boolean', - description: 'Enable/disable a document (optional for update_document)', - }, - filePaths: { - type: 'array', - description: - 'Canonical workspace file VFS paths to add as documents (for add_file), e.g. ["files/Docs/handbook.pdf"].', - items: { type: 'string' }, - }, - filename: { - type: 'string', - description: 'New filename for a document (optional for update_document)', - }, - knowledgeBaseId: { - type: 'string', - description: - 'Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)', - }, - knowledgeBaseIds: { - type: 'array', - description: 'Knowledge base IDs (for batch delete)', - items: { type: 'string' }, - }, - name: { - type: 'string', - description: "Name of the knowledge base (required for 'create')", - }, - query: { type: 'string', description: "Search query text (required for 'query')" }, - sourceConfig: { - type: 'object', - description: - 'Connector-specific configuration matching the configFields in knowledgebases/connectors/{type}.json', - }, - syncIntervalMinutes: { - type: 'number', - description: - 'Sync interval in minutes. Accepted values: 60 (hourly), 360 (6h), 1440 (daily), 10080 (weekly), 0 (manual only). Default: 1440', - default: 1440, - }, - tagDefinitionId: { - type: 'string', - description: 'Tag definition ID (required for update_tag, delete_tag)', - }, - tagDisplayName: { - type: 'string', - description: - 'Display name for the tag (required for create_tag, optional for update_tag)', - }, - tagFieldType: { - type: 'string', - description: - 'Field type: text, number, date, boolean (optional for create_tag, defaults to text)', - enum: ['text', 'number', 'date', 'boolean'], - }, - tagValues: { - type: 'array', - description: - 'Typed tag values to set on this document (optional for update_document). Resolve tagDefinitionId with list_tags first. Use null to clear a value.', - items: { - type: 'object', - properties: { - tagDefinitionId: { - type: 'string', - description: 'Tag definition ID returned by list_tags.', - }, - value: { - type: ['string', 'number', 'boolean', 'null'], - description: - "Value matching the tag definition's field type: string for text, number for number, YYYY-MM-DD string for date, boolean for boolean, or null to clear.", - }, - }, - required: ['tagDefinitionId', 'value'], - }, - }, - topK: { - type: 'number', - description: 'Number of results to return (1-50, default: 5)', - default: 5, - }, - workspaceId: { - type: 'string', - description: - "Workspace ID. Required for 'create' when there is no workspace in context; otherwise the current workspace context is used.", - }, - }, - }, - operation: { + workflowId: { type: 'string', - description: 'The operation to perform', - enum: [ - 'create', - 'get', - 'query', - 'add_file', - 'update', - 'delete_document', - 'update_document', - 'list_tags', - 'create_tag', - 'update_tag', - 'delete_tag', - 'get_tag_usage', - 'add_connector', - 'update_connector', - 'delete_connector', - 'sync_connector', - ], + description: 'Optional workflow ID. If not provided, uses the current workflow in context.', }, }, - required: ['operation'], - }, - resultSchema: { - type: 'object', - properties: { - data: { - type: ['object', 'array'], - description: - 'Operation-specific result payload. An object for most operations; list_tags and get_tag_usage return an array of tag definitions.', - }, - message: { type: 'string', description: 'Human-readable outcome summary.' }, - success: { type: 'boolean', description: 'Whether the operation succeeded.' }, - }, - required: ['success', 'message'], }, } @@ -3558,9 +2992,209 @@ export const ManageCustomTool: ToolCatalogEntry = { requiredPermission: 'write', } -export const ManageMcpTool: ToolCatalogEntry = { - id: 'manage_mcp_tool', - name: 'manage_mcp_tool', +export const ManageKnowledgeBase: ToolCatalogEntry = { + id: 'manage_knowledge_base', + name: 'manage_knowledge_base', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + apiKey: { + type: 'string', + description: + 'API key for API-key-based connectors (required when connector auth mode is apiKey)', + }, + chunkingConfig: { + type: 'object', + description: "Chunking configuration (optional for 'create')", + properties: { + maxSize: { + type: 'number', + description: 'Maximum chunk size (100-4000, default: 1024)', + default: 1024, + }, + minSize: { + type: 'number', + description: 'Minimum chunk size (1-2000, default: 1)', + default: 1, + }, + overlap: { + type: 'number', + description: 'Overlap between chunks (0-500, default: 200)', + default: 200, + }, + }, + }, + connectorId: { + type: 'string', + description: + 'Connector ID (required for update_connector, delete_connector, sync_connector)', + }, + connectorStatus: { + type: 'string', + description: 'Connector status (optional for update_connector)', + enum: ['active', 'paused'], + }, + connectorType: { + type: 'string', + description: + "Connector type from registry, e.g. 'confluence', 'google_drive', 'notion' (required for add_connector). Read knowledgebases/connectors/{type}.json for the config schema.", + }, + credentialId: { + type: 'string', + description: + 'OAuth credential ID from environment/credentials.json (required for OAuth connectors)', + }, + description: { + type: 'string', + description: "Description of the knowledge base (optional for 'create')", + }, + disabledTagIds: { + type: 'array', + description: + 'Tag definition IDs to opt out of (optional for add_connector). See tagDefinitions in the connector schema.', + }, + documentId: { type: 'string', description: 'Document ID (required for update_document)' }, + documentIds: { + type: 'array', + description: 'Document IDs (for batch delete_document)', + items: { type: 'string' }, + }, + enabled: { + type: 'boolean', + description: 'Enable/disable a document (optional for update_document)', + }, + filePaths: { + type: 'array', + description: + 'Canonical workspace file VFS paths to add as documents (for add_file), e.g. ["files/Docs/handbook.pdf"].', + items: { type: 'string' }, + }, + filename: { + type: 'string', + description: 'New filename for a document (optional for update_document)', + }, + knowledgeBaseId: { + type: 'string', + description: + 'Knowledge base ID (required for get, query, add_file, list_tags, create_tag, get_tag_usage)', + }, + knowledgeBaseIds: { + type: 'array', + description: 'Knowledge base IDs (for batch delete)', + items: { type: 'string' }, + }, + name: { + type: 'string', + description: "Name of the knowledge base (required for 'create')", + }, + query: { type: 'string', description: "Search query text (required for 'query')" }, + sourceConfig: { + type: 'object', + description: + 'Connector-specific configuration matching the configFields in knowledgebases/connectors/{type}.json', + }, + syncIntervalMinutes: { + type: 'number', + description: + 'Sync interval in minutes. Accepted values: 60 (hourly), 360 (6h), 1440 (daily), 10080 (weekly), 0 (manual only). Default: 1440', + default: 1440, + }, + tagDefinitionId: { + type: 'string', + description: 'Tag definition ID (required for update_tag, delete_tag)', + }, + tagDisplayName: { + type: 'string', + description: + 'Display name for the tag (required for create_tag, optional for update_tag)', + }, + tagFieldType: { + type: 'string', + description: + 'Field type: text, number, date, boolean (optional for create_tag, defaults to text)', + enum: ['text', 'number', 'date', 'boolean'], + }, + tagValues: { + type: 'array', + description: + 'Typed tag values to set on this document (optional for update_document). Resolve tagDefinitionId with list_tags first. Use null to clear a value.', + items: { + type: 'object', + properties: { + tagDefinitionId: { + type: 'string', + description: 'Tag definition ID returned by list_tags.', + }, + value: { + type: ['string', 'number', 'boolean', 'null'], + description: + "Value matching the tag definition's field type: string for text, number for number, YYYY-MM-DD string for date, boolean for boolean, or null to clear.", + }, + }, + required: ['tagDefinitionId', 'value'], + }, + }, + topK: { + type: 'number', + description: 'Number of results to return (1-50, default: 5)', + default: 5, + }, + workspaceId: { + type: 'string', + description: + "Workspace ID. Required for 'create' when there is no workspace in context; otherwise the current workspace context is used.", + }, + }, + }, + operation: { + type: 'string', + description: 'The operation to perform', + enum: [ + 'create', + 'get', + 'query', + 'add_file', + 'update', + 'delete_document', + 'update_document', + 'list_tags', + 'create_tag', + 'update_tag', + 'delete_tag', + 'get_tag_usage', + 'add_connector', + 'update_connector', + 'delete_connector', + 'sync_connector', + ], + }, + }, + required: ['operation'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: ['object', 'array'], + description: + 'Operation-specific result payload. An object for most operations; list_tags and get_tag_usage return an array of tag definitions.', + }, + message: { type: 'string', description: 'Human-readable outcome summary.' }, + success: { type: 'boolean', description: 'Whether the operation succeeded.' }, + }, + required: ['success', 'message'], + }, +} + +export const ManageMcpConnection: ToolCatalogEntry = { + id: 'manage_mcp_connection', + name: 'manage_mcp_connection', route: 'sim', mode: 'async', parameters: { @@ -3700,33 +3334,6 @@ export const ManageSkill: ToolCatalogEntry = { requiredPermission: 'write', } -export const MaterializeFile: ToolCatalogEntry = { - id: 'materialize_file', - name: 'materialize_file', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - fileNames: { - type: 'array', - description: - 'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])', - items: { type: 'string' }, - }, - operation: { - type: 'string', - description: - 'What to do with the file. "save" promotes it to a permanent files/ path. "import" imports a workflow JSON as a workspace workflow. "extract" decompresses a .zip upload into files//. Defaults to "save".', - enum: ['save', 'import', 'extract'], - default: 'save', - }, - }, - required: ['fileNames'], - }, - requiredPermission: 'write', -} - export const Media: ToolCatalogEntry = { id: 'media', name: 'media', @@ -3868,6 +3475,11 @@ export const OpenResource: ToolCatalogEntry = { description: 'The resource type.', enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], }, + view: { + type: 'string', + description: + 'Saved table view to open pinned (type "table" only): a view id or exact view name from the table\'s views.json. The panel opens the table with that view\'s filter/sort active. Omit to open the table on its default view.', + }, }, required: ['type'], }, @@ -3877,6 +3489,129 @@ export const OpenResource: ToolCatalogEntry = { }, } +export const PrepareFileEdit: ToolCatalogEntry = { + id: 'prepare_file_edit', + name: 'prepare_file_edit', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + operation: { + type: 'string', + description: 'The file operation to perform.', + enum: ['append', 'update', 'patch'], + }, + target: { + type: 'object', + description: 'Explicit file target. Use kind=path + path for existing files.', + properties: { + kind: { + type: 'string', + description: 'How the file target is identified.', + enum: ['path'], + }, + path: { + type: 'string', + description: + 'Canonical existing workspace file VFS path, e.g. "files/Reports/report.md". Required when target.kind=path.', + }, + }, + required: ['kind'], + }, + title: { + type: 'string', + description: + 'Required short UI label for this content unit, e.g. "Chapter 1", "Slide 3", or "Fix footer spacing".', + }, + contentType: { + type: 'string', + description: + 'Optional MIME type override. Usually omit and let the system infer from the target file extension.', + enum: [ + 'text/markdown', + 'text/html', + 'text/plain', + 'application/json', + 'text/csv', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/pdf', + ], + }, + edit: { + type: 'object', + 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 apply_file_edit tool call.', + properties: { + after_anchor: { + type: 'string', + description: + 'Boundary line kept after inserted replacement content. Required for mode=replace_between.', + }, + anchor: { + type: 'string', + description: + 'Anchor line after which new content is inserted. Required for mode=insert_after.', + }, + before_anchor: { + type: 'string', + 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'], + }, + }, + }, + }, + required: ['operation', 'target', 'title'], + }, + 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 file operation succeeded.' }, + }, + required: ['success', 'message'], + }, + requiredPermission: 'write', +} + export const PromoteToLive: ToolCatalogEntry = { id: 'promote_to_live', name: 'promote_to_live', @@ -3901,6 +3636,118 @@ export const PromoteToLive: ToolCatalogEntry = { requiresApproval: true, } +export const PublishCustomBlock: ToolCatalogEntry = { + id: 'publish_custom_block', + name: 'publish_custom_block', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + action: { + type: 'string', + description: 'Whether to publish (deploy) or unpublish (undeploy) the custom block', + enum: ['deploy', 'undeploy'], + default: 'deploy', + }, + description: { + type: 'string', + description: 'Short description shown in the block picker, max 280 characters', + }, + exposedOutputs: { + type: 'array', + description: + "Outputs the block exposes, each mapping a child block output path to a friendly name (use get_block_outputs for valid paths). Omit to expose the terminal block's whole result", + items: { + type: 'object', + properties: { + blockId: { type: 'string', description: 'Block UUID inside the workflow' }, + name: { type: 'string', description: 'Friendly output name shown on the block' }, + path: { + type: 'string', + description: + "Dot-path into that block's output (from get_block_outputs relativeOutputs)", + }, + }, + required: ['blockId', 'path', 'name'], + }, + }, + iconUrl: { + type: 'string', + description: + 'Optional icon image for the block: a workspace file VFS path (e.g. "files/icon.png", copied into public icon storage at publish) or an https image URL. Omit to use the organization\'s default icon', + }, + inputs: { + type: 'array', + description: + "Optional per-input placeholder overrides. Input names and types are derived from the workflow's input trigger and cannot be changed here", + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Stable id of the input trigger field' }, + placeholder: { + type: 'string', + description: "Placeholder text shown in the block's input field", + }, + }, + required: ['id'], + }, + }, + name: { + type: 'string', + description: + 'Display name for the block, max 60 characters. REQUIRED the first time a workflow is published. When republishing an existing block, omit it to keep the current name or pass a new one to rename. Ignored for undeploy.', + }, + workflowId: { type: 'string', description: 'Workflow ID (defaults to active workflow)' }, + }, + }, + resultSchema: { + type: 'object', + properties: { + action: { + type: 'string', + description: 'Action performed by the tool, such as "deploy" or "undeploy".', + }, + blockId: { type: 'string', description: 'Custom block record ID.' }, + blockType: { + type: 'string', + description: 'Stable block type slug (custom_block_*) used in workflow state.', + }, + deploymentConfig: { + type: 'object', + description: + "Structured deployment configuration keyed by surface name. Includes the block's type, name, description, icon, derived input fields, and exposed outputs.", + }, + deploymentStatus: { + type: 'object', + description: + 'Structured per-surface deployment status keyed by surface name, including customBlock and the underlying api surface when applicable.', + }, + deploymentType: { + type: 'string', + description: + 'Deployment surface this result describes. For publish_custom_block this is always "custom_block".', + }, + isDeployed: { + type: 'boolean', + description: 'Whether the custom block is published after this tool call.', + }, + name: { type: 'string', description: 'Display name of the custom block.' }, + removed: { + type: 'boolean', + description: 'Whether the custom block was unpublished during an undeploy action.', + }, + updated: { + type: 'boolean', + description: 'Whether an existing custom block was updated instead of created.', + }, + workflowId: { type: 'string', description: 'Workflow ID the custom block is bound to.' }, + }, + required: ['deploymentType', 'deploymentStatus'], + }, + requiredPermission: 'admin', +} + export const QueryLogs: ToolCatalogEntry = { id: 'query_logs', name: 'query_logs', @@ -4040,6 +3887,11 @@ export const QueryUserTable: ToolCatalogEntry = { }, rowId: { type: 'string', description: 'Row ID (required for get_row)' }, tableId: { type: 'string', description: 'Table ID (required for all operations)' }, + view: { + type: 'string', + description: + "Saved view to query through (query_rows only): a view id or exact view name from the table's views.json. The view's saved filter ANDs with any filter you pass (query-within-the-view); its saved sort applies only when you pass no order. Layout fields (hidden columns, widths) are ignored — full rows come back. Manage views via the table agent.", + }, }, }, operation: { @@ -4132,7 +3984,7 @@ export const Redeploy: ToolCatalogEntry = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_api and redeploy this is always "api".', + 'Deployment surface this result describes. For deploy_as_api and redeploy this is always "api".', }, examples: { type: 'object', @@ -4384,6 +4236,56 @@ export const RunCode: ToolCatalogEntry = { capabilities: ['file_input', 'directory_input', 'table_input'], } +export const RunEnrichment: ToolCatalogEntry = { + id: 'run_enrichment', + name: 'run_enrichment', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + enrichmentId: { + type: 'string', + description: + "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.", + enum: [ + 'work-email', + 'phone-number', + 'company-domain', + 'company-info', + 'email-verification', + ], + }, + inputs: { + type: 'object', + description: + 'Map of the enrichment\'s input id → value, e.g. { "fullName": "Jane Doe", "companyDomain": "acme.com" }. Provide a value for every required input.', + }, + }, + required: ['enrichmentId', 'inputs'], + }, + resultSchema: { + type: 'object', + properties: { + matched: { + type: 'boolean', + description: 'True when a provider returned a non-empty result.', + }, + provider: { + type: ['string', 'null'], + description: + 'Internal label of the provider that produced the result (billing/diagnostics only — do NOT surface it to the user), or null on no match.', + }, + result: { + type: 'object', + description: 'Mapped output values from the winning provider (empty object on no match).', + }, + }, + required: ['matched', 'result'], + }, + requiredPermission: 'write', +} + export const RunFromBlock: ToolCatalogEntry = { id: 'run_from_block', name: 'run_from_block', @@ -4418,6 +4320,157 @@ export const RunFromBlock: ToolCatalogEntry = { clientExecutable: true, } +export const RunFunction: ToolCatalogEntry = { + id: 'run_function', + name: 'run_function', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', + }, + inputs: { + type: 'object', + description: + 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', + properties: { + directories: { + type: 'array', + description: + 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', + }, + }, + required: ['path'], + }, + }, + files: { + type: 'array', + description: 'Workspace files to mount into the sandbox.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', + }, + }, + required: ['path'], + }, + }, + tables: { + type: 'array', + description: 'Workspace tables to mount as CSV files.', + items: { + type: 'object', + properties: { + path: { type: 'string', description: 'Canonical VFS table path when available.' }, + sandboxPath: { + type: 'string', + description: 'Optional full sandbox path for the mounted CSV.', + }, + tableId: { type: 'string', description: 'Workspace table ID.' }, + }, + }, + }, + }, + }, + language: { + type: 'string', + description: 'Execution language.', + enum: ['javascript', 'python', 'shell'], + }, + outputTable: { + type: 'string', + description: + 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', + }, + outputs: { + type: 'object', + description: + 'Workspace files to create or overwrite from returned code results or sandbox-created files.', + properties: { + files: { + type: 'array', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', + items: { + type: 'object', + properties: { + format: { + type: 'string', + description: 'Optional serialization format for returned values.', + enum: ['json', 'csv', 'txt', 'md', 'html'], + }, + mimeType: { + type: 'string', + description: 'Optional MIME type override when inference is not enough.', + }, + mode: { + type: 'string', + description: 'Create a new file or overwrite an existing file at path.', + enum: ['create', 'overwrite'], + }, + path: { + type: 'string', + description: 'Canonical destination VFS path, e.g. "files/Reports/chart.png".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full path to a file created inside the sandbox. Omit to save the code return value.', + }, + }, + required: ['path', 'mode'], + }, + }, + }, + }, + sandboxId: { + type: 'string', + description: + 'Optional Sim sandbox id from agent/sandboxes/{name}.json. DEFAULT-FIRST: omit this whenever the documented default run_function environment can do the job. Select a ready existing Sim sandbox only when a required third-party dependency, Debian system package, or managed CLI is known to be absent, or a default attempt failed specifically because it was missing. Never guess an id.', + }, + timeout: { + type: 'number', + description: + 'Maximum execution time in SECONDS (Sim converts to milliseconds). The sandbox stops execution and returns a timeout error after this duration. Defaults to 10 seconds and is capped at 300 seconds regardless of plan.', + default: 10, + }, + title: { + type: 'string', + description: + 'Short user-visible label for this execution, e.g. "Clean customer CSV", "Revenue chart", or "Query GitHub issues".', + }, + }, + required: ['code'], + }, + requiredPermission: 'write', + requiresApproval: true, + capabilities: ['file_input', 'directory_input', 'file_output', 'table_input', 'table_output'], +} + export const RunWorkflow: ToolCatalogEntry = { id: 'run_workflow', name: 'run_workflow', @@ -4517,26 +4570,31 @@ export const RunWorkflowUntilBlock: ToolCatalogEntry = { requiresApproval: true, } -export const ScrapePage: ToolCatalogEntry = { - id: 'scrape_page', - name: 'scrape_page', - route: 'go', - mode: 'sync', +export const SaveUpload: ToolCatalogEntry = { + id: 'save_upload', + name: 'save_upload', + route: 'sim', + mode: 'async', parameters: { type: 'object', properties: { - include_links: { - type: 'boolean', - description: 'Extract all links from the page (default false)', + fileNames: { + type: 'array', + description: + 'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])', + items: { type: 'string' }, }, - url: { type: 'string', description: 'The URL to scrape (must include https://)' }, - wait_for: { + operation: { type: 'string', - description: 'CSS selector to wait for before scraping (for JS-heavy pages)', + description: + 'What to do with the file. "save" promotes it to a permanent files/ path. "import" imports a workflow JSON as a workspace workflow. "extract" decompresses a .zip upload into files//. Defaults to "save".', + enum: ['save', 'import', 'extract'], + default: 'save', }, }, - required: ['url'], + required: ['fileNames'], }, + requiredPermission: 'write', } export const Search: ToolCatalogEntry = { @@ -4559,26 +4617,6 @@ export const Search: ToolCatalogEntry = { internal: true, } -export const SearchDocumentation: ToolCatalogEntry = { - id: 'search_documentation', - name: 'search_documentation', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - query: { type: 'string', description: 'The search query' }, - topK: { - type: 'number', - description: - 'Number of results to return (default 10). Not clamped — keep it small, since each result is a full doc chunk.', - default: 10, - }, - }, - required: ['query'], - }, -} - export const SearchIntegrationTools: ToolCatalogEntry = { id: 'search_integration_tools', name: 'search_integration_tools', @@ -4680,64 +4718,23 @@ export const SearchLibraryDocs: ToolCatalogEntry = { }, } -export const SearchOnline: ToolCatalogEntry = { - id: 'search_online', - name: 'search_online', - route: 'go', - mode: 'sync', +export const SearchSimDocs: ToolCatalogEntry = { + id: 'search_sim_docs', + name: 'search_sim_docs', + route: 'sim', + mode: 'async', parameters: { type: 'object', properties: { - category: { - type: 'string', - description: 'Filter by category', - enum: [ - 'news', - 'tweet', - 'github', - 'company', - 'research paper', - 'linkedin profile', - 'pdf', - 'personal site', - ], - }, - 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', + query: { type: 'string', description: 'The search query' }, + topK: { + type: 'number', description: - "Required short UI label fragment (e.g. 'Slack integrations'), not a full sentence.", + 'Number of results to return (default 10). Not clamped — keep it small, since each result is a full doc chunk.', + default: 10, }, }, - required: ['query', 'toolTitle'], - }, -} - -export const SearchPatterns: ToolCatalogEntry = { - id: 'search_patterns', - name: 'search_patterns', - route: 'go', - mode: 'sync', - parameters: { - type: 'object', - properties: { - limit: { - type: 'integer', - description: 'Maximum number of pattern examples to return per query (defaults to 3).', - }, - queries: { - type: 'array', - description: - 'Up to 3 descriptive strings explaining the workflow pattern(s) you need. Focus on intent and desired outcomes.', - items: { - type: 'string', - description: 'Example: "how to automate wealthbox meeting notes into follow-up tasks"', - }, - }, - }, - required: ['queries'], + required: ['query'], }, } @@ -5409,6 +5406,79 @@ export const TableRows: ToolCatalogEntry = { }, } +export const TableViews: ToolCatalogEntry = { + id: 'table_views', + name: 'table_views', + route: 'sim', + mode: 'async', + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + filter: { + type: 'object', + description: + 'Saved row predicate, same grammar as query_rows filters: {"all":[...]} / {"any":[...]} of {field, op, value} leaves with exact column NAMES. Omit or null for an unfiltered view.', + }, + hiddenColumns: { + type: 'array', + description: + 'Column names to hide in the UI when this view is active. Display-only — queries through the view still return every column.', + items: { type: 'string' }, + }, + isDefault: { + type: 'boolean', + description: + "Make this view the table's default (at most one per table; setting it clears the previous default).", + }, + name: { + type: 'string', + description: + "View display name (required for create_view; optional rename on update_view). Free-form label, need not be unique — prefer distinct names so query_user_table's view argument can use them unambiguously.", + }, + sort: { + type: 'array', + description: + 'Saved ordered sort spec, e.g. [{"field":"due","direction":"asc"}], column NAMES. Omit or null for default ordering.', + }, + tableId: { type: 'string', description: 'Table ID (required for every operation)' }, + viewId: { + type: 'string', + description: + 'View ID (required for get_view, update_view, delete_view, set_default_view)', + }, + }, + required: ['tableId'], + }, + operation: { + type: 'string', + description: 'The view operation to perform', + enum: [ + 'list_views', + 'get_view', + 'create_view', + 'update_view', + 'delete_view', + 'set_default_view', + ], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { type: 'object', description: 'Operation-specific result payload.' }, + message: { type: 'string', description: 'Human-readable outcome summary.' }, + success: { type: 'boolean', description: 'Whether the operation succeeded.' }, + }, + required: ['success', 'message'], + }, +} + export const Terminal: ToolCatalogEntry = { id: 'terminal', name: 'terminal', @@ -5548,7 +5618,7 @@ export const UpdateDeploymentVersion: ToolCatalogEntry = { version: { type: 'number', description: - 'The numeric deployment version number to update (use get_deployment_log to find it).', + 'The numeric deployment version number to update (use list_deployment_versions to find it).', }, workflowId: { type: 'string', @@ -5942,6 +6012,116 @@ export const Wait: ToolCatalogEntry = { }, } +export const WebCrawl: ToolCatalogEntry = { + id: 'web_crawl', + name: 'web_crawl', + route: 'go', + mode: 'sync', + parameters: { + type: 'object', + properties: { + exclude_paths: { + type: 'array', + description: 'Skip URLs matching these patterns', + items: { type: 'string' }, + }, + include_paths: { + type: 'array', + description: 'Only crawl URLs matching these patterns', + items: { type: 'string' }, + }, + limit: { type: 'number', description: 'Maximum pages to crawl (default 10, max 50)' }, + max_depth: { type: 'number', description: 'How deep to follow links (default 2)' }, + url: { type: 'string', description: 'Starting URL to crawl from' }, + }, + required: ['url'], + }, +} + +export const WebFetch: ToolCatalogEntry = { + id: 'web_fetch', + name: 'web_fetch', + route: 'go', + mode: 'sync', + parameters: { + type: 'object', + properties: { + include_highlights: { + type: 'boolean', + description: 'Include key highlights (default false)', + }, + include_summary: { + type: 'boolean', + description: 'Include AI-generated summary (default false)', + }, + include_text: { type: 'boolean', description: 'Include full page text (default true)' }, + urls: { + type: 'array', + description: 'URLs to get content from (max 10)', + items: { type: 'string' }, + }, + }, + required: ['urls'], + }, +} + +export const WebScrape: ToolCatalogEntry = { + id: 'web_scrape', + name: 'web_scrape', + route: 'go', + mode: 'sync', + parameters: { + type: 'object', + properties: { + include_links: { + type: 'boolean', + description: 'Extract all links from the page (default false)', + }, + url: { type: 'string', description: 'The URL to scrape (must include https://)' }, + wait_for: { + type: 'string', + description: 'CSS selector to wait for before scraping (for JS-heavy pages)', + }, + }, + required: ['url'], + }, +} + +export const WebSearch: ToolCatalogEntry = { + id: 'web_search', + name: 'web_search', + route: 'go', + mode: 'sync', + parameters: { + type: 'object', + properties: { + category: { + type: 'string', + description: 'Filter by category', + enum: [ + 'news', + 'tweet', + 'github', + 'company', + 'research paper', + 'linkedin profile', + 'pdf', + 'personal site', + ], + }, + 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: + "Required short UI label fragment (e.g. 'Slack integrations'), not a full sentence.", + }, + }, + required: ['query', 'toolTitle'], + }, +} + export const Workflow: ToolCatalogEntry = { id: 'workflow', name: 'workflow', @@ -5974,129 +6154,6 @@ export const Workflow: ToolCatalogEntry = { internal: true, } -export const WorkspaceFile: ToolCatalogEntry = { - id: 'workspace_file', - name: 'workspace_file', - route: 'sim', - mode: 'async', - parameters: { - type: 'object', - properties: { - operation: { - type: 'string', - description: 'The file operation to perform.', - enum: ['append', 'update', 'patch'], - }, - target: { - type: 'object', - description: 'Explicit file target. Use kind=path + path for existing files.', - properties: { - kind: { - type: 'string', - description: 'How the file target is identified.', - enum: ['path'], - }, - path: { - type: 'string', - description: - 'Canonical existing workspace file VFS path, e.g. "files/Reports/report.md". Required when target.kind=path.', - }, - }, - required: ['kind'], - }, - title: { - type: 'string', - description: - 'Required short UI label for this content unit, e.g. "Chapter 1", "Slide 3", or "Fix footer spacing".', - }, - contentType: { - type: 'string', - description: - 'Optional MIME type override. Usually omit and let the system infer from the target file extension.', - enum: [ - 'text/markdown', - 'text/html', - 'text/plain', - 'application/json', - 'text/csv', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/pdf', - ], - }, - edit: { - type: 'object', - 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: { - after_anchor: { - type: 'string', - description: - 'Boundary line kept after inserted replacement content. Required for mode=replace_between.', - }, - anchor: { - type: 'string', - description: - 'Anchor line after which new content is inserted. Required for mode=insert_after.', - }, - before_anchor: { - type: 'string', - 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'], - }, - }, - }, - }, - required: ['operation', 'target', 'title'], - }, - 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 file operation succeeded.' }, - }, - required: ['success', 'message'], - }, - requiredPermission: 'write', -} - export const FfmpegOperation = { overlayAudio: 'overlay_audio', mixAudio: 'mix_audio', @@ -6129,47 +6186,6 @@ export const FfmpegOperationValues = [ FfmpegOperation.probe, ] as const -export const KnowledgeBaseOperation = { - create: 'create', - get: 'get', - query: 'query', - addFile: 'add_file', - update: 'update', - 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.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', @@ -6200,21 +6216,62 @@ export const ManageCustomToolOperationValues = [ ManageCustomToolOperation.list, ] as const -export const ManageMcpToolOperation = { +export const ManageKnowledgeBaseOperation = { + create: 'create', + get: 'get', + query: 'query', + addFile: 'add_file', + update: 'update', + 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 ManageKnowledgeBaseOperation = + (typeof ManageKnowledgeBaseOperation)[keyof typeof ManageKnowledgeBaseOperation] + +export const ManageKnowledgeBaseOperationValues = [ + ManageKnowledgeBaseOperation.create, + ManageKnowledgeBaseOperation.get, + ManageKnowledgeBaseOperation.query, + ManageKnowledgeBaseOperation.addFile, + ManageKnowledgeBaseOperation.update, + ManageKnowledgeBaseOperation.deleteDocument, + ManageKnowledgeBaseOperation.updateDocument, + ManageKnowledgeBaseOperation.listTags, + ManageKnowledgeBaseOperation.createTag, + ManageKnowledgeBaseOperation.updateTag, + ManageKnowledgeBaseOperation.deleteTag, + ManageKnowledgeBaseOperation.getTagUsage, + ManageKnowledgeBaseOperation.addConnector, + ManageKnowledgeBaseOperation.updateConnector, + ManageKnowledgeBaseOperation.deleteConnector, + ManageKnowledgeBaseOperation.syncConnector, +] as const + +export const ManageMcpConnectionOperation = { add: 'add', edit: 'edit', delete: 'delete', list: 'list', } as const -export type ManageMcpToolOperation = - (typeof ManageMcpToolOperation)[keyof typeof ManageMcpToolOperation] +export type ManageMcpConnectionOperation = + (typeof ManageMcpConnectionOperation)[keyof typeof ManageMcpConnectionOperation] -export const ManageMcpToolOperationValues = [ - ManageMcpToolOperation.add, - ManageMcpToolOperation.edit, - ManageMcpToolOperation.delete, - ManageMcpToolOperation.list, +export const ManageMcpConnectionOperationValues = [ + ManageMcpConnectionOperation.add, + ManageMcpConnectionOperation.edit, + ManageMcpConnectionOperation.delete, + ManageMcpConnectionOperation.list, ] as const export const ManageSandboxOperation = { @@ -6250,19 +6307,19 @@ export const ManageSkillOperationValues = [ ManageSkillOperation.list, ] as const -export const MaterializeFileOperation = { - save: 'save', - import: 'import', - extract: 'extract', +export const PrepareFileEditOperation = { + append: 'append', + update: 'update', + patch: 'patch', } as const -export type MaterializeFileOperation = - (typeof MaterializeFileOperation)[keyof typeof MaterializeFileOperation] +export type PrepareFileEditOperation = + (typeof PrepareFileEditOperation)[keyof typeof PrepareFileEditOperation] -export const MaterializeFileOperationValues = [ - MaterializeFileOperation.save, - MaterializeFileOperation.import, - MaterializeFileOperation.extract, +export const PrepareFileEditOperationValues = [ + PrepareFileEditOperation.append, + PrepareFileEditOperation.update, + PrepareFileEditOperation.patch, ] as const export const QueryUserTableOperation = { @@ -6282,6 +6339,20 @@ export const QueryUserTableOperationValues = [ QueryUserTableOperation.queryRows, ] as const +export const SaveUploadOperation = { + save: 'save', + import: 'import', + extract: 'extract', +} as const + +export type SaveUploadOperation = (typeof SaveUploadOperation)[keyof typeof SaveUploadOperation] + +export const SaveUploadOperationValues = [ + SaveUploadOperation.save, + SaveUploadOperation.import, + SaveUploadOperation.extract, +] as const + export const SearchKnowledgeBaseOperation = { get: 'get', query: 'query', @@ -6392,6 +6463,26 @@ export const TableRowsOperationValues = [ TableRowsOperation.deleteRowsByFilter, ] as const +export const TableViewsOperation = { + listViews: 'list_views', + getView: 'get_view', + createView: 'create_view', + updateView: 'update_view', + deleteView: 'delete_view', + setDefaultView: 'set_default_view', +} as const + +export type TableViewsOperation = (typeof TableViewsOperation)[keyof typeof TableViewsOperation] + +export const TableViewsOperationValues = [ + TableViewsOperation.listViews, + TableViewsOperation.getView, + TableViewsOperation.createView, + TableViewsOperation.updateView, + TableViewsOperation.deleteView, + TableViewsOperation.setDefaultView, +] as const + export const TerminalOperation = { run: 'run', read: 'read', @@ -6490,23 +6581,8 @@ export const UserTableOperationValues = [ UserTableOperation.addEnrichment, ] 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, + [ApplyFileEdit.id]: ApplyFileEdit, [Auth.id]: Auth, [Browser.id]: Browser, [BrowserClick.id]: BrowserClick, @@ -6531,26 +6607,21 @@ export const TOOL_CATALOG: Record = { [BrowserType.id]: BrowserType, [BrowserWaitFor.id]: BrowserWaitFor, [CallIntegrationTool.id]: CallIntegrationTool, - [CheckDeploymentStatus.id]: CheckDeploymentStatus, [Cp.id]: Cp, - [CrawlWebsite.id]: CrawlWebsite, - [CreateFile.id]: CreateFile, + [CreateEmptyFile.id]: CreateEmptyFile, [CreateWorkflow.id]: CreateWorkflow, [CreateWorkspaceMcpServer.id]: CreateWorkspaceMcpServer, [DeleteWorkspaceMcpServer.id]: DeleteWorkspaceMcpServer, [Deploy.id]: Deploy, - [DeployApi.id]: DeployApi, - [DeployChat.id]: DeployChat, - [DeployCustomBlock.id]: DeployCustomBlock, - [DeployMcp.id]: DeployMcp, + [DeployAsApi.id]: DeployAsApi, + [DeployAsChat.id]: DeployAsChat, + [DeployAsMcp.id]: DeployAsMcp, [DiffWorkflows.id]: DiffWorkflows, - [DownloadToWorkspaceFile.id]: DownloadToWorkspaceFile, - [EditContent.id]: EditContent, + [DownloadFile.id]: DownloadFile, [EditWorkflow.id]: EditWorkflow, - [EnrichmentRun.id]: EnrichmentRun, + [Extensions.id]: Extensions, [Ffmpeg.id]: Ffmpeg, [File.id]: File, - [FunctionExecute.id]: FunctionExecute, [GenerateApiKey.id]: GenerateApiKey, [GenerateAudio.id]: GenerateAudio, [GenerateImage.id]: GenerateImage, @@ -6558,15 +6629,14 @@ export const TOOL_CATALOG: Record = { [GetBlockOutputs.id]: GetBlockOutputs, [GetBlockUpstreamReferences.id]: GetBlockUpstreamReferences, [GetDeployedWorkflowState.id]: GetDeployedWorkflowState, - [GetDeploymentLog.id]: GetDeploymentLog, - [GetPageContents.id]: GetPageContents, - [GetPlatformActions.id]: GetPlatformActions, + [GetDeploymentStatus.id]: GetDeploymentStatus, + [GetUiReference.id]: GetUiReference, [GetWorkflowData.id]: GetWorkflowData, [GetWorkflowRunOptions.id]: GetWorkflowRunOptions, [Glob.id]: Glob, [Grep.id]: Grep, [Knowledge.id]: Knowledge, - [KnowledgeBase.id]: KnowledgeBase, + [ListDeploymentVersions.id]: ListDeploymentVersions, [ListIntegrationTools.id]: ListIntegrationTools, [ListUserWorkspaces.id]: ListUserWorkspaces, [ListWorkspaceMcpServers.id]: ListWorkspaceMcpServers, @@ -6575,17 +6645,19 @@ export const TOOL_CATALOG: Record = { [LoadSkill.id]: LoadSkill, [ManageCredential.id]: ManageCredential, [ManageCustomTool.id]: ManageCustomTool, - [ManageMcpTool.id]: ManageMcpTool, + [ManageKnowledgeBase.id]: ManageKnowledgeBase, + [ManageMcpConnection.id]: ManageMcpConnection, [ManageSandbox.id]: ManageSandbox, [ManageSkill.id]: ManageSkill, - [MaterializeFile.id]: MaterializeFile, [Media.id]: Media, [Mkdir.id]: Mkdir, [Mv.id]: Mv, [OauthGetAuthLink.id]: OauthGetAuthLink, [OauthRequestAccess.id]: OauthRequestAccess, [OpenResource.id]: OpenResource, + [PrepareFileEdit.id]: PrepareFileEdit, [PromoteToLive.id]: PromoteToLive, + [PublishCustomBlock.id]: PublishCustomBlock, [QueryLogs.id]: QueryLogs, [QueryUserTable.id]: QueryUserTable, [Read.id]: Read, @@ -6596,17 +6668,17 @@ export const TOOL_CATALOG: Record = { [Run.id]: Run, [RunBlock.id]: RunBlock, [RunCode.id]: RunCode, + [RunEnrichment.id]: RunEnrichment, [RunFromBlock.id]: RunFromBlock, + [RunFunction.id]: RunFunction, [RunWorkflow.id]: RunWorkflow, [RunWorkflowUntilBlock.id]: RunWorkflowUntilBlock, - [ScrapePage.id]: ScrapePage, + [SaveUpload.id]: SaveUpload, [Search.id]: Search, - [SearchDocumentation.id]: SearchDocumentation, [SearchIntegrationTools.id]: SearchIntegrationTools, [SearchKnowledgeBase.id]: SearchKnowledgeBase, [SearchLibraryDocs.id]: SearchLibraryDocs, - [SearchOnline.id]: SearchOnline, - [SearchPatterns.id]: SearchPatterns, + [SearchSimDocs.id]: SearchSimDocs, [SetBlockEnabled.id]: SetBlockEnabled, [SetEnvironmentVariables.id]: SetEnvironmentVariables, [SetGlobalWorkflowVariables.id]: SetGlobalWorkflowVariables, @@ -6617,11 +6689,15 @@ export const TOOL_CATALOG: Record = { [TableEnrichments.id]: TableEnrichments, [TableManage.id]: TableManage, [TableRows.id]: TableRows, + [TableViews.id]: TableViews, [Terminal.id]: Terminal, [UpdateDeploymentVersion.id]: UpdateDeploymentVersion, [UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer, [UserTable.id]: UserTable, [Wait.id]: Wait, + [WebCrawl.id]: WebCrawl, + [WebFetch.id]: WebFetch, + [WebScrape.id]: WebScrape, + [WebSearch.id]: WebSearch, [Workflow.id]: Workflow, - [WorkspaceFile.id]: WorkspaceFile, } diff --git a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts index a3fd31e1c8..50d9fe521a 100644 --- a/apps/sim/lib/copilot/generated/tool-schemas-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-schemas-v1.ts @@ -10,18 +10,37 @@ export interface ToolRuntimeSchemaEntry { } export const TOOL_RUNTIME_SCHEMAS: Record = { - agent: { + apply_file_edit: { parameters: { + type: 'object', properties: { - request: { - description: 'What tool/skill/MCP action is needed.', + 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: ['request'], + 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'], }, - resultSchema: undefined, }, auth: { parameters: { @@ -1106,18 +1125,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - check_deployment_status: { - parameters: { - type: 'object', - properties: { - workflowId: { - type: 'string', - description: 'Workflow ID to check (defaults to current workflow)', - }, - }, - }, - resultSchema: undefined, - }, cp: { parameters: { type: 'object', @@ -1145,42 +1152,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - crawl_website: { - parameters: { - type: 'object', - properties: { - exclude_paths: { - type: 'array', - description: 'Skip URLs matching these patterns', - items: { - type: 'string', - }, - }, - include_paths: { - type: 'array', - description: 'Only crawl URLs matching these patterns', - items: { - type: 'string', - }, - }, - limit: { - type: 'number', - description: 'Maximum pages to crawl (default 10, max 50)', - }, - max_depth: { - type: 'number', - description: 'How deep to follow links (default 2)', - }, - url: { - type: 'string', - description: 'Starting URL to crawl from', - }, - }, - required: ['url'], - }, - resultSchema: undefined, - }, - create_file: { + create_empty_file: { parameters: { type: 'object', properties: { @@ -1328,7 +1300,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - deploy_api: { + deploy_as_api: { parameters: { type: 'object', properties: { @@ -1382,7 +1354,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_api and redeploy this is always "api".', + 'Deployment surface this result describes. For deploy_as_api and redeploy this is always "api".', }, examples: { type: 'object', @@ -1412,7 +1384,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - deploy_chat: { + deploy_as_chat: { parameters: { type: 'object', properties: { @@ -1526,7 +1498,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_chat this is always "chat".', + 'Deployment surface this result describes. For deploy_as_chat this is always "chat".', }, examples: { type: 'object', @@ -1547,7 +1519,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, success: { type: 'boolean', - description: 'Whether the deploy_chat action completed successfully.', + description: 'Whether the deploy_as_chat action completed successfully.', }, version: { type: 'number', @@ -1571,134 +1543,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { ], }, }, - deploy_custom_block: { - parameters: { - type: 'object', - properties: { - action: { - type: 'string', - description: 'Whether to publish (deploy) or unpublish (undeploy) the custom block', - enum: ['deploy', 'undeploy'], - default: 'deploy', - }, - description: { - type: 'string', - description: 'Short description shown in the block picker, max 280 characters', - }, - exposedOutputs: { - type: 'array', - description: - "Outputs the block exposes, each mapping a child block output path to a friendly name (use get_block_outputs for valid paths). Omit to expose the terminal block's whole result", - items: { - type: 'object', - properties: { - blockId: { - type: 'string', - description: 'Block UUID inside the workflow', - }, - name: { - type: 'string', - description: 'Friendly output name shown on the block', - }, - path: { - type: 'string', - description: - "Dot-path into that block's output (from get_block_outputs relativeOutputs)", - }, - }, - required: ['blockId', 'path', 'name'], - }, - }, - iconUrl: { - type: 'string', - description: - 'Optional icon image for the block: a workspace file VFS path (e.g. "files/icon.png", copied into public icon storage at publish) or an https image URL. Omit to use the organization\'s default icon', - }, - inputs: { - type: 'array', - description: - "Optional per-input placeholder overrides. Input names and types are derived from the workflow's input trigger and cannot be changed here", - items: { - type: 'object', - properties: { - id: { - type: 'string', - description: 'Stable id of the input trigger field', - }, - placeholder: { - type: 'string', - description: "Placeholder text shown in the block's input field", - }, - }, - required: ['id'], - }, - }, - name: { - type: 'string', - description: - 'Display name for the block, max 60 characters. REQUIRED the first time a workflow is published. When republishing an existing block, omit it to keep the current name or pass a new one to rename. Ignored for undeploy.', - }, - workflowId: { - type: 'string', - description: 'Workflow ID (defaults to active workflow)', - }, - }, - }, - resultSchema: { - type: 'object', - properties: { - action: { - type: 'string', - description: 'Action performed by the tool, such as "deploy" or "undeploy".', - }, - blockId: { - type: 'string', - description: 'Custom block record ID.', - }, - blockType: { - type: 'string', - description: 'Stable block type slug (custom_block_*) used in workflow state.', - }, - deploymentConfig: { - type: 'object', - description: - "Structured deployment configuration keyed by surface name. Includes the block's type, name, description, icon, derived input fields, and exposed outputs.", - }, - deploymentStatus: { - type: 'object', - description: - 'Structured per-surface deployment status keyed by surface name, including customBlock and the underlying api surface when applicable.', - }, - deploymentType: { - type: 'string', - description: - 'Deployment surface this result describes. For deploy_custom_block this is always "custom_block".', - }, - isDeployed: { - type: 'boolean', - description: 'Whether the custom block is published after this tool call.', - }, - name: { - type: 'string', - description: 'Display name of the custom block.', - }, - removed: { - type: 'boolean', - description: 'Whether the custom block was unpublished during an undeploy action.', - }, - updated: { - type: 'boolean', - description: 'Whether an existing custom block was updated instead of created.', - }, - workflowId: { - type: 'string', - description: 'Workflow ID the custom block is bound to.', - }, - }, - required: ['deploymentType', 'deploymentStatus'], - }, - }, - deploy_mcp: { + deploy_as_mcp: { parameters: { type: 'object', properties: { @@ -1773,7 +1618,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_mcp this is always "mcp".', + 'Deployment surface this result describes. For deploy_as_mcp this is always "mcp".', }, examples: { type: 'object', @@ -1844,7 +1689,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - download_to_workspace_file: { + download_file: { parameters: { type: 'object', properties: { @@ -1893,38 +1738,6 @@ 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', @@ -1964,49 +1777,18 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - enrichment_run: { + extensions: { parameters: { - type: 'object', properties: { - enrichmentId: { + request: { + description: 'What tool/skill/MCP action is needed.', type: 'string', - description: - "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.", - enum: [ - 'work-email', - 'phone-number', - 'company-domain', - 'company-info', - 'email-verification', - ], - }, - inputs: { - type: 'object', - description: - 'Map of the enrichment\'s input id → value, e.g. { "fullName": "Jane Doe", "companyDomain": "acme.com" }. Provide a value for every required input.', }, }, - required: ['enrichmentId', 'inputs'], - }, - resultSchema: { + required: ['request'], type: 'object', - properties: { - matched: { - type: 'boolean', - description: 'True when a provider returned a non-empty result.', - }, - provider: { - type: ['string', 'null'], - description: - 'Internal label of the provider that produced the result (billing/diagnostics only — do NOT surface it to the user), or null on no match.', - }, - result: { - type: 'object', - description: 'Mapped output values from the winning provider (empty object on no match).', - }, - }, - required: ['matched', 'result'], }, + resultSchema: undefined, }, ffmpeg: { parameters: { @@ -2203,156 +1985,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - function_execute: { - parameters: { - type: 'object', - properties: { - code: { - type: 'string', - description: - 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', - }, - inputs: { - type: 'object', - description: - 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', - properties: { - directories: { - type: 'array', - description: - 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', - items: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', - }, - sandboxPath: { - type: 'string', - description: - 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', - }, - }, - required: ['path'], - }, - }, - files: { - type: 'array', - description: 'Workspace files to mount into the sandbox.', - items: { - type: 'object', - properties: { - path: { - type: 'string', - description: - 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', - }, - sandboxPath: { - type: 'string', - description: - 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', - }, - }, - required: ['path'], - }, - }, - tables: { - type: 'array', - description: 'Workspace tables to mount as CSV files.', - items: { - type: 'object', - properties: { - path: { - type: 'string', - description: 'Canonical VFS table path when available.', - }, - sandboxPath: { - type: 'string', - description: 'Optional full sandbox path for the mounted CSV.', - }, - tableId: { - type: 'string', - description: 'Workspace table ID.', - }, - }, - }, - }, - }, - }, - language: { - type: 'string', - description: 'Execution language.', - enum: ['javascript', 'python', 'shell'], - }, - outputTable: { - type: 'string', - description: - 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', - }, - outputs: { - type: 'object', - description: - 'Workspace files to create or overwrite from returned code results or sandbox-created files.', - properties: { - files: { - type: 'array', - description: - 'File outputs. Missing parent folders are created automatically for create mode.', - items: { - type: 'object', - properties: { - format: { - type: 'string', - description: 'Optional serialization format for returned values.', - enum: ['json', 'csv', 'txt', 'md', 'html'], - }, - mimeType: { - type: 'string', - description: 'Optional MIME type override when inference is not enough.', - }, - mode: { - type: 'string', - description: 'Create a new file or overwrite an existing file at path.', - enum: ['create', 'overwrite'], - }, - path: { - type: 'string', - description: 'Canonical destination VFS path, e.g. "files/Reports/chart.png".', - }, - sandboxPath: { - type: 'string', - description: - 'Optional full path to a file created inside the sandbox. Omit to save the code return value.', - }, - }, - required: ['path', 'mode'], - }, - }, - }, - }, - sandboxId: { - type: 'string', - description: - 'Optional Sim sandbox id from agent/sandboxes/{name}.json. DEFAULT-FIRST: omit this whenever the documented default function_execute environment can do the job. Select a ready existing Sim sandbox only when a required third-party dependency, Debian system package, or managed CLI is known to be absent, or a default attempt failed specifically because it was missing. Never guess an id.', - }, - timeout: { - type: 'number', - description: - 'Maximum execution time in SECONDS (Sim converts to milliseconds). The sandbox stops execution and returns a timeout error after this duration. Defaults to 10 seconds and is capped at 300 seconds regardless of plan.', - default: 10, - }, - title: { - type: 'string', - description: - 'Short user-visible label for this execution, e.g. "Clean customer CSV", "Revenue chart", or "Query GitHub issues".', - }, - }, - required: ['code'], - }, - resultSchema: undefined, - }, generate_api_key: { parameters: { type: 'object', @@ -2877,48 +2509,19 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - get_deployment_log: { + get_deployment_status: { parameters: { type: 'object', properties: { workflowId: { type: 'string', - description: - 'Optional workflow ID. If not provided, uses the current workflow in context.', + description: 'Workflow ID to check (defaults to current workflow)', }, }, }, resultSchema: undefined, }, - get_page_contents: { - parameters: { - type: 'object', - properties: { - include_highlights: { - type: 'boolean', - description: 'Include key highlights (default false)', - }, - include_summary: { - type: 'boolean', - description: 'Include AI-generated summary (default false)', - }, - include_text: { - type: 'boolean', - description: 'Include full page text (default true)', - }, - urls: { - type: 'array', - description: 'URLs to get content from (max 10)', - items: { - type: 'string', - }, - }, - }, - required: ['urls'], - }, - resultSchema: undefined, - }, - get_platform_actions: { + get_ui_reference: { parameters: { type: 'object', properties: {}, @@ -3037,7 +2640,213 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - knowledge_base: { + list_deployment_versions: { + parameters: { + type: 'object', + properties: { + workflowId: { + type: 'string', + description: + 'Optional workflow ID. If not provided, uses the current workflow in context.', + }, + }, + }, + resultSchema: undefined, + }, + list_integration_tools: { + parameters: { + properties: { + integration: { + description: + 'The integration service name — the folder under components/integrations/ (e.g. "slack", "gmail", "google_sheets"). Returns every operation\'s id, name, and description for that service.', + type: 'string', + }, + }, + required: ['integration'], + type: 'object', + }, + resultSchema: undefined, + }, + list_user_workspaces: { + parameters: { + type: 'object', + properties: {}, + }, + resultSchema: undefined, + }, + list_workspace_mcp_servers: { + parameters: { + type: 'object', + properties: { + workspaceId: { + type: 'string', + description: + 'Workspace ID. Required when no current workspace context is available, such as headless MCP calls.', + }, + }, + }, + resultSchema: undefined, + }, + load_deployment: { + parameters: { + type: 'object', + properties: { + version: { + type: 'string', + description: + 'A string: a deployment version number (e.g. "5"), or "live" for the active deployment. (Unlike promote_to_live, which takes a numeric version, "live" is accepted here.)', + }, + workflowId: { + type: 'string', + description: + 'Optional workflow ID. If not provided, uses the current workflow in context.', + }, + }, + required: ['version'], + }, + resultSchema: undefined, + }, + load_integration_tool: { + parameters: { + properties: { + tool_ids: { + description: + 'Exact integration tool ids to load before calling them, e.g. ["gmail_send_v2"]. Copy the "id" field verbatim from components/integrations/{service}/{operation}.json (including any version suffix).', + items: { + type: 'string', + }, + type: 'array', + }, + }, + required: ['tool_ids'], + type: 'object', + }, + resultSchema: undefined, + }, + load_skill: { + parameters: { + type: 'object', + properties: { + name: { + type: 'string', + description: + "Skill name exactly as it appears in the Loadable Skills index (e.g. 'pptx-writing').", + }, + }, + required: ['name'], + }, + resultSchema: undefined, + }, + manage_credential: { + parameters: { + type: 'object', + properties: { + credentialId: { + type: 'string', + description: 'The credential ID (required for rename)', + }, + credentialIds: { + type: 'array', + description: 'Array of credential IDs (for batch delete)', + items: { + type: 'string', + }, + }, + displayName: { + type: 'string', + description: 'New display name (required for rename)', + }, + operation: { + type: 'string', + description: 'The operation to perform', + enum: ['rename', 'delete'], + }, + }, + required: ['operation'], + }, + resultSchema: undefined, + }, + manage_custom_tool: { + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'The JavaScript code that executes when the tool is called (required for add). Parameters from schema are available as variables. Function body only - no signature or wrapping braces.', + }, + operation: { + type: 'string', + description: + "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", + enum: ['add', 'edit', 'delete', 'list'], + }, + schema: { + type: 'object', + description: 'The tool schema in OpenAI function calling format (required for add).', + properties: { + function: { + type: 'object', + description: 'The function definition', + properties: { + description: { + type: 'string', + description: 'What the function does', + }, + name: { + type: 'string', + description: 'The function name (camelCase)', + }, + parameters: { + type: 'object', + description: 'The function parameters schema', + properties: { + properties: { + type: 'object', + description: 'Parameter definitions as key-value pairs', + }, + required: { + type: 'array', + description: 'Array of required parameter names', + items: { + type: 'string', + }, + }, + type: { + type: 'string', + description: "Must be 'object'", + }, + }, + required: ['type', 'properties'], + }, + }, + required: ['name', 'parameters'], + }, + type: { + type: 'string', + description: "Must be 'function'", + }, + }, + required: ['type', 'function'], + }, + toolId: { + type: 'string', + description: + "The ID of the custom tool. Get it from the `list` operation or the `id` field inside the tool's VFS file (agent/custom-tools/{name}.json — the filename is the display name, not the id); get_workflow_data also returns it where that tool is available. Do not guess or construct it. Required for edit and delete; omit for add and list.", + }, + toolIds: { + type: 'array', + description: 'Array of custom tool IDs (for batch delete)', + items: { + type: 'string', + }, + }, + }, + required: ['operation'], + }, + resultSchema: undefined, + }, + manage_knowledge_base: { parameters: { type: 'object', properties: { @@ -3250,200 +3059,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, - list_integration_tools: { - parameters: { - properties: { - integration: { - description: - 'The integration service name — the folder under components/integrations/ (e.g. "slack", "gmail", "google_sheets"). Returns every operation\'s id, name, and description for that service.', - type: 'string', - }, - }, - required: ['integration'], - type: 'object', - }, - resultSchema: undefined, - }, - list_user_workspaces: { - parameters: { - type: 'object', - properties: {}, - }, - resultSchema: undefined, - }, - list_workspace_mcp_servers: { - parameters: { - type: 'object', - properties: { - workspaceId: { - type: 'string', - description: - 'Workspace ID. Required when no current workspace context is available, such as headless MCP calls.', - }, - }, - }, - resultSchema: undefined, - }, - load_deployment: { - parameters: { - type: 'object', - properties: { - version: { - type: 'string', - description: - 'A string: a deployment version number (e.g. "5"), or "live" for the active deployment. (Unlike promote_to_live, which takes a numeric version, "live" is accepted here.)', - }, - workflowId: { - type: 'string', - description: - 'Optional workflow ID. If not provided, uses the current workflow in context.', - }, - }, - required: ['version'], - }, - resultSchema: undefined, - }, - load_integration_tool: { - parameters: { - properties: { - tool_ids: { - description: - 'Exact integration tool ids to load before calling them, e.g. ["gmail_send_v2"]. Copy the "id" field verbatim from components/integrations/{service}/{operation}.json (including any version suffix).', - items: { - type: 'string', - }, - type: 'array', - }, - }, - required: ['tool_ids'], - type: 'object', - }, - resultSchema: undefined, - }, - load_skill: { - parameters: { - type: 'object', - properties: { - name: { - type: 'string', - description: - "Skill name exactly as it appears in the Loadable Skills index (e.g. 'pptx-writing').", - }, - }, - required: ['name'], - }, - resultSchema: undefined, - }, - manage_credential: { - parameters: { - type: 'object', - properties: { - credentialId: { - type: 'string', - description: 'The credential ID (required for rename)', - }, - credentialIds: { - type: 'array', - description: 'Array of credential IDs (for batch delete)', - items: { - type: 'string', - }, - }, - displayName: { - type: 'string', - description: 'New display name (required for rename)', - }, - operation: { - type: 'string', - description: 'The operation to perform', - enum: ['rename', 'delete'], - }, - }, - required: ['operation'], - }, - resultSchema: undefined, - }, - manage_custom_tool: { - parameters: { - type: 'object', - properties: { - code: { - type: 'string', - description: - 'The JavaScript code that executes when the tool is called (required for add). Parameters from schema are available as variables. Function body only - no signature or wrapping braces.', - }, - operation: { - type: 'string', - description: - "The operation to perform: 'add', 'edit', 'list', or 'delete'. These verbs are tool-specific — other manage_* tools may use create/update instead of add/edit.", - enum: ['add', 'edit', 'delete', 'list'], - }, - schema: { - type: 'object', - description: 'The tool schema in OpenAI function calling format (required for add).', - properties: { - function: { - type: 'object', - description: 'The function definition', - properties: { - description: { - type: 'string', - description: 'What the function does', - }, - name: { - type: 'string', - description: 'The function name (camelCase)', - }, - parameters: { - type: 'object', - description: 'The function parameters schema', - properties: { - properties: { - type: 'object', - description: 'Parameter definitions as key-value pairs', - }, - required: { - type: 'array', - description: 'Array of required parameter names', - items: { - type: 'string', - }, - }, - type: { - type: 'string', - description: "Must be 'object'", - }, - }, - required: ['type', 'properties'], - }, - }, - required: ['name', 'parameters'], - }, - type: { - type: 'string', - description: "Must be 'function'", - }, - }, - required: ['type', 'function'], - }, - toolId: { - type: 'string', - description: - "The ID of the custom tool. Get it from the `list` operation or the `id` field inside the tool's VFS file (agent/custom-tools/{name}.json — the filename is the display name, not the id); get_workflow_data also returns it where that tool is available. Do not guess or construct it. Required for edit and delete; omit for add and list.", - }, - toolIds: { - type: 'array', - description: 'Array of custom tool IDs (for batch delete)', - items: { - type: 'string', - }, - }, - }, - required: ['operation'], - }, - resultSchema: undefined, - }, - manage_mcp_tool: { + manage_mcp_connection: { parameters: { type: 'object', properties: { @@ -3582,30 +3198,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - materialize_file: { - parameters: { - type: 'object', - properties: { - fileNames: { - type: 'array', - description: - 'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])', - items: { - type: 'string', - }, - }, - operation: { - type: 'string', - description: - 'What to do with the file. "save" promotes it to a permanent files/ path. "import" imports a workflow JSON as a workspace workflow. "extract" decompresses a .zip upload into files//. Defaults to "save".', - enum: ['save', 'import', 'extract'], - default: 'save', - }, - }, - required: ['fileNames'], - }, - resultSchema: undefined, - }, media: { parameters: { properties: { @@ -3726,6 +3318,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { description: 'The resource type.', enum: ['workflow', 'table', 'knowledgebase', 'file', 'log'], }, + view: { + type: 'string', + description: + 'Saved table view to open pinned (type "table" only): a view id or exact view name from the table\'s views.json. The panel opens the table with that view\'s filter/sort active. Omit to open the table on its default view.', + }, }, required: ['type'], }, @@ -3735,6 +3332,129 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + prepare_file_edit: { + parameters: { + type: 'object', + properties: { + operation: { + type: 'string', + description: 'The file operation to perform.', + enum: ['append', 'update', 'patch'], + }, + target: { + type: 'object', + description: 'Explicit file target. Use kind=path + path for existing files.', + properties: { + kind: { + type: 'string', + description: 'How the file target is identified.', + enum: ['path'], + }, + path: { + type: 'string', + description: + 'Canonical existing workspace file VFS path, e.g. "files/Reports/report.md". Required when target.kind=path.', + }, + }, + required: ['kind'], + }, + title: { + type: 'string', + description: + 'Required short UI label for this content unit, e.g. "Chapter 1", "Slide 3", or "Fix footer spacing".', + }, + contentType: { + type: 'string', + description: + 'Optional MIME type override. Usually omit and let the system infer from the target file extension.', + enum: [ + 'text/markdown', + 'text/html', + 'text/plain', + 'application/json', + 'text/csv', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/pdf', + ], + }, + edit: { + type: 'object', + 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 apply_file_edit tool call.', + properties: { + after_anchor: { + type: 'string', + description: + 'Boundary line kept after inserted replacement content. Required for mode=replace_between.', + }, + anchor: { + type: 'string', + description: + 'Anchor line after which new content is inserted. Required for mode=insert_after.', + }, + before_anchor: { + type: 'string', + 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'], + }, + }, + }, + }, + required: ['operation', 'target', 'title'], + }, + 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 file operation succeeded.', + }, + }, + required: ['success', 'message'], + }, + }, promote_to_live: { parameters: { type: 'object', @@ -3754,6 +3474,133 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + publish_custom_block: { + parameters: { + type: 'object', + properties: { + action: { + type: 'string', + description: 'Whether to publish (deploy) or unpublish (undeploy) the custom block', + enum: ['deploy', 'undeploy'], + default: 'deploy', + }, + description: { + type: 'string', + description: 'Short description shown in the block picker, max 280 characters', + }, + exposedOutputs: { + type: 'array', + description: + "Outputs the block exposes, each mapping a child block output path to a friendly name (use get_block_outputs for valid paths). Omit to expose the terminal block's whole result", + items: { + type: 'object', + properties: { + blockId: { + type: 'string', + description: 'Block UUID inside the workflow', + }, + name: { + type: 'string', + description: 'Friendly output name shown on the block', + }, + path: { + type: 'string', + description: + "Dot-path into that block's output (from get_block_outputs relativeOutputs)", + }, + }, + required: ['blockId', 'path', 'name'], + }, + }, + iconUrl: { + type: 'string', + description: + 'Optional icon image for the block: a workspace file VFS path (e.g. "files/icon.png", copied into public icon storage at publish) or an https image URL. Omit to use the organization\'s default icon', + }, + inputs: { + type: 'array', + description: + "Optional per-input placeholder overrides. Input names and types are derived from the workflow's input trigger and cannot be changed here", + items: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'Stable id of the input trigger field', + }, + placeholder: { + type: 'string', + description: "Placeholder text shown in the block's input field", + }, + }, + required: ['id'], + }, + }, + name: { + type: 'string', + description: + 'Display name for the block, max 60 characters. REQUIRED the first time a workflow is published. When republishing an existing block, omit it to keep the current name or pass a new one to rename. Ignored for undeploy.', + }, + workflowId: { + type: 'string', + description: 'Workflow ID (defaults to active workflow)', + }, + }, + }, + resultSchema: { + type: 'object', + properties: { + action: { + type: 'string', + description: 'Action performed by the tool, such as "deploy" or "undeploy".', + }, + blockId: { + type: 'string', + description: 'Custom block record ID.', + }, + blockType: { + type: 'string', + description: 'Stable block type slug (custom_block_*) used in workflow state.', + }, + deploymentConfig: { + type: 'object', + description: + "Structured deployment configuration keyed by surface name. Includes the block's type, name, description, icon, derived input fields, and exposed outputs.", + }, + deploymentStatus: { + type: 'object', + description: + 'Structured per-surface deployment status keyed by surface name, including customBlock and the underlying api surface when applicable.', + }, + deploymentType: { + type: 'string', + description: + 'Deployment surface this result describes. For publish_custom_block this is always "custom_block".', + }, + isDeployed: { + type: 'boolean', + description: 'Whether the custom block is published after this tool call.', + }, + name: { + type: 'string', + description: 'Display name of the custom block.', + }, + removed: { + type: 'boolean', + description: 'Whether the custom block was unpublished during an undeploy action.', + }, + updated: { + type: 'boolean', + description: 'Whether an existing custom block was updated instead of created.', + }, + workflowId: { + type: 'string', + description: 'Workflow ID the custom block is bound to.', + }, + }, + required: ['deploymentType', 'deploymentStatus'], + }, + }, query_logs: { parameters: { type: 'object', @@ -3901,6 +3748,11 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { type: 'string', description: 'Table ID (required for all operations)', }, + view: { + type: 'string', + description: + "Saved view to query through (query_rows only): a view id or exact view name from the table's views.json. The view's saved filter ANDs with any filter you pass (query-within-the-view); its saved sort applies only when you pass no order. Layout fields (hidden columns, widths) are ignored — full rows come back. Manage views via the table agent.", + }, }, }, operation: { @@ -4006,7 +3858,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { deploymentType: { type: 'string', description: - 'Deployment surface this result describes. For deploy_api and redeploy this is always "api".', + 'Deployment surface this result describes. For deploy_as_api and redeploy this is always "api".', }, examples: { type: 'object', @@ -4249,6 +4101,50 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + run_enrichment: { + parameters: { + type: 'object', + properties: { + enrichmentId: { + type: 'string', + description: + "Which enrichment to run. Discover the full set and each one's inputs/outputs via table_enrichments.list_enrichments.", + enum: [ + 'work-email', + 'phone-number', + 'company-domain', + 'company-info', + 'email-verification', + ], + }, + inputs: { + type: 'object', + description: + 'Map of the enrichment\'s input id → value, e.g. { "fullName": "Jane Doe", "companyDomain": "acme.com" }. Provide a value for every required input.', + }, + }, + required: ['enrichmentId', 'inputs'], + }, + resultSchema: { + type: 'object', + properties: { + matched: { + type: 'boolean', + description: 'True when a provider returned a non-empty result.', + }, + provider: { + type: ['string', 'null'], + description: + 'Internal label of the provider that produced the result (billing/diagnostics only — do NOT surface it to the user), or null on no match.', + }, + result: { + type: 'object', + description: 'Mapped output values from the winning provider (empty object on no match).', + }, + }, + required: ['matched', 'result'], + }, + }, run_from_block: { parameters: { type: 'object', @@ -4281,6 +4177,156 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + run_function: { + parameters: { + type: 'object', + properties: { + code: { + type: 'string', + description: + 'Code to execute. For JS: raw statements auto-wrapped in async context. For Python: full script. For shell: bash script with pre-installed CLI tools. Use each needed secret as {{VAR_NAME}}; the reference resolves to the value exactly as stored.', + }, + inputs: { + type: 'object', + description: + 'Workspace resources to mount into the sandbox. Copy paths verbatim from glob/read/grep output — they are percent-encoded per segment (spaces are %20, an in-name slash is %2F; parentheses and dots stay literal). Both the encoded path and the plain name resolve, so copy the returned path exactly rather than retyping or decoding it.', + properties: { + directories: { + type: 'array', + description: + 'Workspace folders to mount recursively into the sandbox, including nested files and empty folders.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS folder path, e.g. "files/Reports". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full sandbox directory path override. Omit to mount at /home/user/{path}.', + }, + }, + required: ['path'], + }, + }, + files: { + type: 'array', + description: 'Workspace files to mount into the sandbox.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: + 'Canonical VFS file path, e.g. "files/Reports/sales.csv". By default this mounts at "/home/user/{path}".', + }, + sandboxPath: { + type: 'string', + description: + 'Full sandbox path to mount at, e.g. /home/user/inputs/data.csv. STRONGLY RECOMMENDED whenever the file name has spaces or special characters: the default mount path is the percent-ENCODED canonical path (e.g. /home/user/files/Q4%20Sales%20(Final).csv), which code using the human-readable name will not find. Set a simple sandboxPath and read exactly that.', + }, + }, + required: ['path'], + }, + }, + tables: { + type: 'array', + description: 'Workspace tables to mount as CSV files.', + items: { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Canonical VFS table path when available.', + }, + sandboxPath: { + type: 'string', + description: 'Optional full sandbox path for the mounted CSV.', + }, + tableId: { + type: 'string', + description: 'Workspace table ID.', + }, + }, + }, + }, + }, + }, + language: { + type: 'string', + description: 'Execution language.', + enum: ['javascript', 'python', 'shell'], + }, + outputTable: { + type: 'string', + description: + 'Table ID to overwrite with the code\'s return value. Code MUST return an array of objects where keys match column names. All existing rows are replaced. Example: "tbl_abc123"', + }, + outputs: { + type: 'object', + description: + 'Workspace files to create or overwrite from returned code results or sandbox-created files.', + properties: { + files: { + type: 'array', + description: + 'File outputs. Missing parent folders are created automatically for create mode.', + items: { + type: 'object', + properties: { + format: { + type: 'string', + description: 'Optional serialization format for returned values.', + enum: ['json', 'csv', 'txt', 'md', 'html'], + }, + mimeType: { + type: 'string', + description: 'Optional MIME type override when inference is not enough.', + }, + mode: { + type: 'string', + description: 'Create a new file or overwrite an existing file at path.', + enum: ['create', 'overwrite'], + }, + path: { + type: 'string', + description: 'Canonical destination VFS path, e.g. "files/Reports/chart.png".', + }, + sandboxPath: { + type: 'string', + description: + 'Optional full path to a file created inside the sandbox. Omit to save the code return value.', + }, + }, + required: ['path', 'mode'], + }, + }, + }, + }, + sandboxId: { + type: 'string', + description: + 'Optional Sim sandbox id from agent/sandboxes/{name}.json. DEFAULT-FIRST: omit this whenever the documented default run_function environment can do the job. Select a ready existing Sim sandbox only when a required third-party dependency, Debian system package, or managed CLI is known to be absent, or a default attempt failed specifically because it was missing. Never guess an id.', + }, + timeout: { + type: 'number', + description: + 'Maximum execution time in SECONDS (Sim converts to milliseconds). The sandbox stops execution and returns a timeout error after this duration. Defaults to 10 seconds and is capped at 300 seconds regardless of plan.', + default: 10, + }, + title: { + type: 'string', + description: + 'Short user-visible label for this execution, e.g. "Clean customer CSV", "Revenue chart", or "Query GitHub issues".', + }, + }, + required: ['code'], + }, + resultSchema: undefined, + }, run_workflow: { parameters: { type: 'object', @@ -4368,24 +4414,27 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - scrape_page: { + save_upload: { parameters: { type: 'object', properties: { - include_links: { - type: 'boolean', - description: 'Extract all links from the page (default false)', + fileNames: { + type: 'array', + description: + 'The names of the uploaded files to materialize (e.g. ["report.pdf", "data.csv"])', + items: { + type: 'string', + }, }, - url: { + operation: { type: 'string', - description: 'The URL to scrape (must include https://)', - }, - wait_for: { - type: 'string', - description: 'CSS selector to wait for before scraping (for JS-heavy pages)', + description: + 'What to do with the file. "save" promotes it to a permanent files/ path. "import" imports a workflow JSON as a workspace workflow. "extract" decompresses a .zip upload into files//. Defaults to "save".', + enum: ['save', 'import', 'extract'], + default: 'save', }, }, - required: ['url'], + required: ['fileNames'], }, resultSchema: undefined, }, @@ -4403,25 +4452,6 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - search_documentation: { - parameters: { - type: 'object', - properties: { - query: { - type: 'string', - description: 'The search query', - }, - topK: { - type: 'number', - description: - 'Number of results to return (default 10). Not clamped — keep it small, since each result is a full doc chunk.', - default: 10, - }, - }, - required: ['query'], - }, - resultSchema: undefined, - }, search_integration_tools: { parameters: { properties: { @@ -4519,65 +4549,22 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - search_online: { + search_sim_docs: { parameters: { type: 'object', properties: { - category: { - type: 'string', - description: 'Filter by category', - enum: [ - 'news', - 'tweet', - 'github', - 'company', - 'research paper', - 'linkedin profile', - 'pdf', - 'personal site', - ], - }, - 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', + description: 'The search query', }, - toolTitle: { - type: 'string', + topK: { + type: 'number', description: - "Required short UI label fragment (e.g. 'Slack integrations'), not a full sentence.", + 'Number of results to return (default 10). Not clamped — keep it small, since each result is a full doc chunk.', + default: 10, }, }, - required: ['query', 'toolTitle'], - }, - resultSchema: undefined, - }, - search_patterns: { - parameters: { - type: 'object', - properties: { - limit: { - type: 'integer', - description: 'Maximum number of pattern examples to return per query (defaults to 3).', - }, - queries: { - type: 'array', - description: - 'Up to 3 descriptive strings explaining the workflow pattern(s) you need. Focus on intent and desired outcomes.', - items: { - type: 'string', - description: 'Example: "how to automate wealthbox meeting notes into follow-up tasks"', - }, - }, - }, - required: ['queries'], + required: ['query'], }, resultSchema: undefined, }, @@ -5322,6 +5309,88 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { required: ['success', 'message'], }, }, + table_views: { + parameters: { + type: 'object', + properties: { + args: { + type: 'object', + description: 'Arguments for the operation', + properties: { + filter: { + type: 'object', + description: + 'Saved row predicate, same grammar as query_rows filters: {"all":[...]} / {"any":[...]} of {field, op, value} leaves with exact column NAMES. Omit or null for an unfiltered view.', + }, + hiddenColumns: { + type: 'array', + description: + 'Column names to hide in the UI when this view is active. Display-only — queries through the view still return every column.', + items: { + type: 'string', + }, + }, + isDefault: { + type: 'boolean', + description: + "Make this view the table's default (at most one per table; setting it clears the previous default).", + }, + name: { + type: 'string', + description: + "View display name (required for create_view; optional rename on update_view). Free-form label, need not be unique — prefer distinct names so query_user_table's view argument can use them unambiguously.", + }, + sort: { + type: 'array', + description: + 'Saved ordered sort spec, e.g. [{"field":"due","direction":"asc"}], column NAMES. Omit or null for default ordering.', + }, + tableId: { + type: 'string', + description: 'Table ID (required for every operation)', + }, + viewId: { + type: 'string', + description: + 'View ID (required for get_view, update_view, delete_view, set_default_view)', + }, + }, + required: ['tableId'], + }, + operation: { + type: 'string', + description: 'The view operation to perform', + enum: [ + 'list_views', + 'get_view', + 'create_view', + 'update_view', + 'delete_view', + 'set_default_view', + ], + }, + }, + required: ['operation', 'args'], + }, + resultSchema: { + type: 'object', + properties: { + data: { + type: 'object', + description: 'Operation-specific result payload.', + }, + message: { + type: 'string', + description: 'Human-readable outcome summary.', + }, + success: { + type: 'boolean', + description: 'Whether the operation succeeded.', + }, + }, + required: ['success', 'message'], + }, + }, terminal: { parameters: { type: 'object', @@ -5453,7 +5522,7 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { version: { type: 'number', description: - 'The numeric deployment version number to update (use get_deployment_log to find it).', + 'The numeric deployment version number to update (use list_deployment_versions to find it).', }, workflowId: { type: 'string', @@ -5883,6 +5952,130 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, + web_crawl: { + parameters: { + type: 'object', + properties: { + exclude_paths: { + type: 'array', + description: 'Skip URLs matching these patterns', + items: { + type: 'string', + }, + }, + include_paths: { + type: 'array', + description: 'Only crawl URLs matching these patterns', + items: { + type: 'string', + }, + }, + limit: { + type: 'number', + description: 'Maximum pages to crawl (default 10, max 50)', + }, + max_depth: { + type: 'number', + description: 'How deep to follow links (default 2)', + }, + url: { + type: 'string', + description: 'Starting URL to crawl from', + }, + }, + required: ['url'], + }, + resultSchema: undefined, + }, + web_fetch: { + parameters: { + type: 'object', + properties: { + include_highlights: { + type: 'boolean', + description: 'Include key highlights (default false)', + }, + include_summary: { + type: 'boolean', + description: 'Include AI-generated summary (default false)', + }, + include_text: { + type: 'boolean', + description: 'Include full page text (default true)', + }, + urls: { + type: 'array', + description: 'URLs to get content from (max 10)', + items: { + type: 'string', + }, + }, + }, + required: ['urls'], + }, + resultSchema: undefined, + }, + web_scrape: { + parameters: { + type: 'object', + properties: { + include_links: { + type: 'boolean', + description: 'Extract all links from the page (default false)', + }, + url: { + type: 'string', + description: 'The URL to scrape (must include https://)', + }, + wait_for: { + type: 'string', + description: 'CSS selector to wait for before scraping (for JS-heavy pages)', + }, + }, + required: ['url'], + }, + resultSchema: undefined, + }, + web_search: { + parameters: { + type: 'object', + properties: { + category: { + type: 'string', + description: 'Filter by category', + enum: [ + 'news', + 'tweet', + 'github', + 'company', + 'research paper', + 'linkedin profile', + 'pdf', + 'personal site', + ], + }, + 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: + "Required short UI label fragment (e.g. 'Slack integrations'), not a full sentence.", + }, + }, + required: ['query', 'toolTitle'], + }, + resultSchema: undefined, + }, workflow: { parameters: { properties: { @@ -5909,127 +6102,4 @@ export const TOOL_RUNTIME_SCHEMAS: Record = { }, resultSchema: undefined, }, - workspace_file: { - parameters: { - type: 'object', - properties: { - operation: { - type: 'string', - description: 'The file operation to perform.', - enum: ['append', 'update', 'patch'], - }, - target: { - type: 'object', - description: 'Explicit file target. Use kind=path + path for existing files.', - properties: { - kind: { - type: 'string', - description: 'How the file target is identified.', - enum: ['path'], - }, - path: { - type: 'string', - description: - 'Canonical existing workspace file VFS path, e.g. "files/Reports/report.md". Required when target.kind=path.', - }, - }, - required: ['kind'], - }, - title: { - type: 'string', - description: - 'Required short UI label for this content unit, e.g. "Chapter 1", "Slide 3", or "Fix footer spacing".', - }, - contentType: { - type: 'string', - description: - 'Optional MIME type override. Usually omit and let the system infer from the target file extension.', - enum: [ - 'text/markdown', - 'text/html', - 'text/plain', - 'application/json', - 'text/csv', - 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/pdf', - ], - }, - edit: { - type: 'object', - 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: { - after_anchor: { - type: 'string', - description: - 'Boundary line kept after inserted replacement content. Required for mode=replace_between.', - }, - anchor: { - type: 'string', - description: - 'Anchor line after which new content is inserted. Required for mode=insert_after.', - }, - before_anchor: { - type: 'string', - 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'], - }, - }, - }, - }, - required: ['operation', 'target', 'title'], - }, - 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 file operation succeeded.', - }, - }, - required: ['success', 'message'], - }, - }, } diff --git a/apps/sim/lib/copilot/request/context/result.test.ts b/apps/sim/lib/copilot/request/context/result.test.ts index 1947b63551..b8659cb2d6 100644 --- a/apps/sim/lib/copilot/request/context/result.test.ts +++ b/apps/sim/lib/copilot/request/context/result.test.ts @@ -3,7 +3,7 @@ */ import { describe, expect, it } from 'vitest' import { MothershipStreamV1ToolOutcome } from '@/lib/copilot/generated/mothership-stream-v1' -import { FunctionExecute } from '@/lib/copilot/generated/tool-catalog-v1' +import { RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { buildToolCallSummaries } from '@/lib/copilot/request/context/result' import { TraceCollector } from '@/lib/copilot/request/trace' import type { StreamingContext } from '@/lib/copilot/request/types' @@ -44,7 +44,7 @@ describe('buildToolCallSummaries', () => { const context = makeContext() context.toolCalls.set('tool-1', { id: 'tool-1', - name: 'download_to_workspace_file', + name: 'download_file', status: 'pending', startTime: 1, }) @@ -59,7 +59,7 @@ describe('buildToolCallSummaries', () => { const context = makeContext() context.toolCalls.set('tool-2', { id: 'tool-2', - name: FunctionExecute.id, + name: RunFunction.id, status: 'executing', startTime: 1, }) @@ -76,7 +76,7 @@ describe('buildToolCallSummaries', () => { const context = makeContext() context.toolCalls.set('tool-3', { id: 'tool-3', - name: 'download_to_workspace_file', + name: 'download_file', status: MothershipStreamV1ToolOutcome.cancelled, result: { success: false }, error: 'Stopped by user', @@ -89,7 +89,7 @@ describe('buildToolCallSummaries', () => { expect(summaries).toHaveLength(1) expect(summaries[0]).toEqual({ id: 'tool-3', - name: 'download_to_workspace_file', + name: 'download_file', status: MothershipStreamV1ToolOutcome.cancelled, params: undefined, result: undefined, diff --git a/apps/sim/lib/copilot/request/go/file-preview-adapter.test.ts b/apps/sim/lib/copilot/request/go/file-preview-adapter.test.ts index 27851eabba..3e14e0bd0e 100644 --- a/apps/sim/lib/copilot/request/go/file-preview-adapter.test.ts +++ b/apps/sim/lib/copilot/request/go/file-preview-adapter.test.ts @@ -54,21 +54,21 @@ function toolEvent(payload: Record): StreamEvent { ) } -/** One args_delta chunk of the streamed `edit_content` JSON, as a driveable StreamEvent. */ +/** One args_delta chunk of the streamed `apply_file_edit` JSON, as a driveable StreamEvent. */ function editContentDelta(argumentsDelta: string): StreamEvent { return toolEvent({ toolCallId: EDIT_TOOL_CALL_ID, - toolName: 'edit_content', + toolName: 'apply_file_edit', phase: MothershipStreamV1ToolPhase.args_delta, argumentsDelta, }) } -/** The authoritative `workspace_file` call frame for a path-targeted update. */ +/** The authoritative `prepare_file_edit` call frame for a path-targeted update. */ function workspaceFileCall(): StreamEvent { return toolEvent({ toolCallId: WORKSPACE_FILE_TOOL_CALL_ID, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', phase: MothershipStreamV1ToolPhase.call, arguments: { operation: 'update', diff --git a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts index b79254a211..d347b01fd6 100644 --- a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts +++ b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts @@ -382,7 +382,7 @@ export async function processFilePreviewStreamEvent(input: { // Scope the in-flight intent to the invoking file subagent's channel (its // outer tool_use id) so two file agents streaming concurrently never read or - // overwrite each other's intent. workspace_file and edit_content from the same + // overwrite each other's intent. prepare_file_edit and apply_file_edit from the same // file agent share this channel id, so they pair up; siblings stay isolated. const channelId = streamEvent.scope?.parentToolCallId ?? '' const getIntent = (): FileIntent | null => context.activeFileIntents.get(channelId) ?? null @@ -393,7 +393,7 @@ export async function processFilePreviewStreamEvent(input: { context.activeFileIntents.delete(channelId) } - if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'workspace_file') { + if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'prepare_file_edit') { const toolCallId = streamEvent.payload.toolCallId const parsedArgs = parseWorkspaceFileArgs(streamEvent.payload.arguments) if (toolCallId && parsedArgs) { @@ -408,7 +408,7 @@ export async function processFilePreviewStreamEvent(input: { const { fileId, fileName } = target const isContentOp = isContentOperation(operation) - // Per-channel: a re-declared workspace_file just overwrites THIS channel's + // Per-channel: a re-declared prepare_file_edit just overwrites THIS channel's // slot. No cross-message intent clearing — that would wipe a concurrent // sibling file agent's pending intent. const intent: FileIntent = { @@ -451,12 +451,12 @@ export async function processFilePreviewStreamEvent(input: { await emitPreviewEvent(streamEvent, options, { toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_start', }) await emitPreviewEvent(streamEvent, options, { toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_target', operation, target: { @@ -469,7 +469,7 @@ export async function processFilePreviewStreamEvent(input: { if (edit) { await emitPreviewEvent(streamEvent, options, { toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_edit_meta', edit, }) @@ -481,7 +481,7 @@ export async function processFilePreviewStreamEvent(input: { const workspaceResultIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && - streamEvent.payload.toolName === 'workspace_file' && + streamEvent.payload.toolName === 'prepare_file_edit' && workspaceResultIntent && isContentOperation(workspaceResultIntent.operation) ) { @@ -526,12 +526,12 @@ export async function processFilePreviewStreamEvent(input: { await emitPreviewEvent(streamEvent, options, { toolCallId: intent.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_start', }) await emitPreviewEvent(streamEvent, options, { toolCallId: intent.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_target', operation: intent.operation, target: { @@ -544,7 +544,7 @@ export async function processFilePreviewStreamEvent(input: { if (intent.edit) { await emitPreviewEvent(streamEvent, options, { toolCallId: intent.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_edit_meta', edit: intent.edit, }) @@ -555,7 +555,7 @@ export async function processFilePreviewStreamEvent(input: { const patchDeleteIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && - streamEvent.payload.toolName === 'workspace_file' && + streamEvent.payload.toolName === 'prepare_file_edit' && patchDeleteIntent && isContentOperation(patchDeleteIntent.operation) && patchDeleteIntent.operation === 'patch' && @@ -594,7 +594,7 @@ export async function processFilePreviewStreamEvent(input: { await persistFilePreviewSession(nextSession) await emitPreviewEvent(streamEvent, options, { toolCallId: nextSession.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_content', content: previewText, contentMode: 'snapshot', @@ -608,7 +608,10 @@ export async function processFilePreviewStreamEvent(input: { } } - if (isToolArgsDeltaStreamEvent(streamEvent) && streamEvent.payload.toolName === 'edit_content') { + if ( + isToolArgsDeltaStreamEvent(streamEvent) && + streamEvent.payload.toolName === 'apply_file_edit' + ) { const toolCallId = streamEvent.payload.toolCallId const delta = streamEvent.payload.argumentsDelta const stateForTool = editContentState.get(toolCallId) ?? { raw: '' } @@ -685,7 +688,7 @@ export async function processFilePreviewStreamEvent(input: { // collaborative editor for this file is open, that client applies the stream to the shared // doc as minimal CRDT diffs (see `applyStreamedMarkdownToLiveDoc` in the editor), which // renders smoothly locally AND broadcasts to every peer — so a server-side streaming merge - // would double-write the shared doc. The final `edit_content` durable write still reconciles + // would double-write the shared doc. The final `apply_file_edit` durable write still reconciles // the file and seeds any late joiner. if ( @@ -714,7 +717,7 @@ export async function processFilePreviewStreamEvent(input: { await emitPreviewEvent(streamEvent, options, { toolCallId: nextSession.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_content', content: previewUpdate.content, contentMode: previewUpdate.contentMode, @@ -739,7 +742,7 @@ export async function processFilePreviewStreamEvent(input: { editContentState.set(toolCallId, stateForTool) } - if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'edit_content') { + if (isToolCallStreamEvent(streamEvent) && streamEvent.payload.toolName === 'apply_file_edit') { const toolCallId = streamEvent.payload.toolCallId if (toolCallId) { editContentState.delete(toolCallId) @@ -749,7 +752,7 @@ export async function processFilePreviewStreamEvent(input: { const editResultIntent = getIntent() if ( isToolResultStreamEvent(streamEvent) && - streamEvent.payload.toolName === 'edit_content' && + streamEvent.payload.toolName === 'apply_file_edit' && editResultIntent ) { const currentPreview = filePreviewState.get(editResultIntent.toolCallId) @@ -767,7 +770,7 @@ export async function processFilePreviewStreamEvent(input: { }) await emitPreviewEvent(streamEvent, options, { toolCallId: currentPreview.session.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_content', content: currentPreview.session.previewText, contentMode: 'snapshot', @@ -801,7 +804,7 @@ export async function processFilePreviewStreamEvent(input: { await emitPreviewEvent(streamEvent, options, { toolCallId: editResultIntent.toolCallId, - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_complete', fileId: editResultIntent.target.fileId, output: streamEvent.payload.output, diff --git a/apps/sim/lib/copilot/request/go/stream.test.ts b/apps/sim/lib/copilot/request/go/stream.test.ts index 5921c6e89b..25f550a3a0 100644 --- a/apps/sim/lib/copilot/request/go/stream.test.ts +++ b/apps/sim/lib/copilot/request/go/stream.test.ts @@ -181,7 +181,7 @@ describe('copilot go stream helpers', () => { expect(decodeJsonStringPrefix('partial \\u26')).toBe('partial ') }) - it('extracts the streamed edit_content prefix from partial JSON', () => { + it('extracts the streamed apply_file_edit prefix from partial JSON', () => { expect(extractEditContent('{"content":"hello\\nwor')).toBe('hello\nwor') expect(extractEditContent('{"content":"tab\\tvalue"}')).toBe('tab\tvalue') }) @@ -216,7 +216,7 @@ describe('copilot go stream helpers', () => { }) }) - it('hydrates path-based workspace_file edits into file preview events before edit_content streams', async () => { + it('hydrates path-based prepare_file_edit edits into file preview events before apply_file_edit streams', async () => { listAllWorkspaceFilesMock.mockResolvedValue({ files: [{ id: 'file-1', name: 'notes.md', folderPath: null }], }) @@ -229,7 +229,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'workspace-file-path-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.call, @@ -248,7 +248,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'workspace-file-path-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.result, @@ -267,7 +267,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'edit-content-path-1', - toolName: 'edit_content', + toolName: 'apply_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.args_delta, @@ -282,7 +282,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'edit-content-path-1', - toolName: 'edit_content', + toolName: 'apply_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.result, @@ -378,7 +378,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'workspace-file-alias-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.call, @@ -397,7 +397,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'edit-content-alias-1', - toolName: 'edit_content', + toolName: 'apply_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.args_delta, @@ -412,7 +412,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'edit-content-alias-1', - toolName: 'edit_content', + toolName: 'apply_file_edit', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.result, @@ -492,7 +492,7 @@ describe('copilot go stream helpers', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'tool-result-dedupe', - toolName: 'search_online', + toolName: 'web_search', executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, phase: MothershipStreamV1ToolPhase.result, @@ -541,7 +541,7 @@ describe('copilot go stream helpers', () => { expect(context.toolCalls.get('tool-result-dedupe')).toEqual( expect.objectContaining({ id: 'tool-result-dedupe', - name: 'search_online', + name: 'web_search', status: MothershipStreamV1ToolOutcome.success, result: { success: true, output: { value: 'ok' } }, }) diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index 32562b8293..82762e5d46 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -87,7 +87,7 @@ import { MothershipStreamV1ToolOutcome, MothershipStreamV1ToolPhase, } from '@/lib/copilot/generated/mothership-stream-v1' -import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' +import { Read as ReadTool, RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { prePersistClientExecutableToolCall, sseHandlers, @@ -244,7 +244,7 @@ describe('sse-handlers tool lifecycle', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'deploy-1', - toolName: 'deploy_api', + toolName: 'deploy_as_api', arguments: { versionName: 'v2' }, executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, @@ -259,7 +259,7 @@ describe('sse-handlers tool lifecycle', () => { expect(upsertAsyncToolCall).toHaveBeenCalledWith({ runId: 'run-1', toolCallId: 'deploy-1', - toolName: 'deploy_api', + toolName: 'deploy_as_api', args: { versionName: 'v2' }, status: MothershipStreamV1AsyncToolRecordStatus.pending, }) @@ -330,14 +330,14 @@ describe('sse-handlers tool lifecycle', () => { context.runId = 'run-1' context.toolPermissions = { enabled: true, - autoAllowed: new Set(['deploy_api']), + autoAllowed: new Set(['deploy_as_api']), } const event = { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'deploy-2', - toolName: 'deploy_api', + toolName: 'deploy_as_api', arguments: {}, executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, @@ -562,7 +562,7 @@ describe('sse-handlers tool lifecycle', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'tool-function', - toolName: FunctionExecute.id, + toolName: RunFunction.id, arguments: { code: 'return {{SECRET}}' }, executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, @@ -1097,7 +1097,7 @@ describe('sse-handlers tool lifecycle', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'function-finalized-args', - toolName: FunctionExecute.id, + toolName: RunFunction.id, arguments: { language: 'javascript', code: 'return {{STALE_SECRET}}' }, executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, @@ -1115,7 +1115,7 @@ describe('sse-handlers tool lifecycle', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'function-finalized-args', - toolName: FunctionExecute.id, + toolName: RunFunction.id, arguments: { language: 'javascript', code: 'return 1' }, executor: MothershipStreamV1ToolExecutor.sim, mode: MothershipStreamV1ToolMode.async, @@ -1130,7 +1130,7 @@ describe('sse-handlers tool lifecycle', () => { await sleep(0) expect(executeTool).toHaveBeenCalledWith( - FunctionExecute.id, + RunFunction.id, { language: 'javascript', code: 'return 1' }, expect.any(Object) ) diff --git a/apps/sim/lib/copilot/request/handlers/tool.ts b/apps/sim/lib/copilot/request/handlers/tool.ts index c7e340b013..54bf70d780 100644 --- a/apps/sim/lib/copilot/request/handlers/tool.ts +++ b/apps/sim/lib/copilot/request/handlers/tool.ts @@ -501,8 +501,8 @@ async function handleCallPhase( if (!toolCall) return // Capture the invoking subagent's channel id so the executor can thread it - // into the server tool context — this is what scopes the workspace_file -> - // edit_content intent handoff to one file subagent under concurrency. + // into the server tool context — this is what scopes the prepare_file_edit -> + // apply_file_edit intent handoff to one file subagent under concurrency. if (parentToolCallId) toolCall.parentToolCallId = parentToolCallId const readPath = typeof args?.path === 'string' ? args.path : undefined diff --git a/apps/sim/lib/copilot/request/session/contract.test.ts b/apps/sim/lib/copilot/request/session/contract.test.ts index 06661e275d..86dcbc17fb 100644 --- a/apps/sim/lib/copilot/request/session/contract.test.ts +++ b/apps/sim/lib/copilot/request/session/contract.test.ts @@ -156,7 +156,7 @@ describe('stream session contract parser', () => { type: 'tool' as const, payload: { toolCallId: 'preview-1', - toolName: 'workspace_file' as const, + toolName: 'prepare_file_edit' as const, previewPhase: 'file_preview_content' as const, content: 'draft body', contentMode: 'snapshot' as const, diff --git a/apps/sim/lib/copilot/request/session/contract.ts b/apps/sim/lib/copilot/request/session/contract.ts index dde683b966..e7b6eb9920 100644 --- a/apps/sim/lib/copilot/request/session/contract.ts +++ b/apps/sim/lib/copilot/request/session/contract.ts @@ -47,7 +47,7 @@ export interface SyntheticFilePreviewTarget { export interface SyntheticFilePreviewStartPayload { previewPhase: typeof FILE_PREVIEW_PHASE.start toolCallId: string - toolName: 'workspace_file' + toolName: 'prepare_file_edit' } export interface SyntheticFilePreviewTargetPayload { @@ -56,14 +56,14 @@ export interface SyntheticFilePreviewTargetPayload { target: SyntheticFilePreviewTarget title?: string toolCallId: string - toolName: 'workspace_file' + toolName: 'prepare_file_edit' } export interface SyntheticFilePreviewEditMetaPayload { edit: JsonRecord previewPhase: typeof FILE_PREVIEW_PHASE.editMeta toolCallId: string - toolName: 'workspace_file' + toolName: 'prepare_file_edit' } export interface SyntheticFilePreviewContentPayload { @@ -77,7 +77,7 @@ export interface SyntheticFilePreviewContentPayload { previewVersion: number targetKind?: string toolCallId: string - toolName: 'workspace_file' + toolName: 'prepare_file_edit' } export interface SyntheticFilePreviewCompletePayload { @@ -86,7 +86,7 @@ export interface SyntheticFilePreviewCompletePayload { previewPhase: typeof FILE_PREVIEW_PHASE.complete previewVersion?: number toolCallId: string - toolName: 'workspace_file' + toolName: 'prepare_file_edit' } export type SyntheticFilePreviewPayload = @@ -360,7 +360,7 @@ function isSyntheticFilePreviewPayload(value: unknown): value is SyntheticFilePr return false } - if (typeof value.toolCallId !== 'string' || value.toolName !== 'workspace_file') { + if (typeof value.toolCallId !== 'string' || value.toolName !== 'prepare_file_edit') { return false } diff --git a/apps/sim/lib/copilot/request/session/event.test.ts b/apps/sim/lib/copilot/request/session/event.test.ts index 0f0573a24f..29d86146c1 100644 --- a/apps/sim/lib/copilot/request/session/event.test.ts +++ b/apps/sim/lib/copilot/request/session/event.test.ts @@ -43,7 +43,7 @@ describe('createEvent', () => { payload: { previewPhase: 'file_preview_start', toolCallId: 'preview-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', }, }) @@ -56,7 +56,7 @@ describe('createEvent', () => { payload: { previewPhase: 'file_preview_start', toolCallId: 'preview-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', }, }) }) diff --git a/apps/sim/lib/copilot/request/session/writer.test.ts b/apps/sim/lib/copilot/request/session/writer.test.ts index aa3eb5384e..8ff64276df 100644 --- a/apps/sim/lib/copilot/request/session/writer.test.ts +++ b/apps/sim/lib/copilot/request/session/writer.test.ts @@ -168,7 +168,7 @@ describe('StreamWriter', () => { type: MothershipStreamV1EventType.tool, payload: { toolCallId: 'preview-1', - toolName: 'workspace_file', + toolName: 'prepare_file_edit', previewPhase: 'file_preview_start', }, } satisfies StreamEvent) diff --git a/apps/sim/lib/copilot/request/sse-utils.test.ts b/apps/sim/lib/copilot/request/sse-utils.test.ts index 65b5b4319c..d8da8edc0c 100644 --- a/apps/sim/lib/copilot/request/sse-utils.test.ts +++ b/apps/sim/lib/copilot/request/sse-utils.test.ts @@ -36,7 +36,7 @@ describe('shouldSkipToolCallEvent', () => { it('keeps non-vfs generating placeholders visible', () => { expect( shouldSkipToolCallEvent( - toolCallEvent('search-generating-placeholder', 'search_online', undefined, true) + toolCallEvent('search-generating-placeholder', 'web_search', undefined, true) ) ).toBe(false) }) diff --git a/apps/sim/lib/copilot/request/tools/executor.test.ts b/apps/sim/lib/copilot/request/tools/executor.test.ts index b40964ab42..4bc032fae6 100644 --- a/apps/sim/lib/copilot/request/tools/executor.test.ts +++ b/apps/sim/lib/copilot/request/tools/executor.test.ts @@ -115,7 +115,7 @@ describe('toolWatchdogTimeoutMs', () => { expect(toolWatchdogTimeoutMs('read')).toBe(TOOL_WATCHDOG_DEFAULT_MS) }) - it.each(['deploy_api', 'deploy_chat', 'deploy_mcp', 'redeploy', 'promote_to_live'])( + it.each(['deploy_as_api', 'deploy_as_chat', 'deploy_as_mcp', 'redeploy', 'promote_to_live'])( 'does not undercut deployment tool %s with the default watchdog', (toolName) => { expect(toolWatchdogTimeoutMs(toolName)).toBe(TOOL_WATCHDOG_LONG_RUNNING_MS) diff --git a/apps/sim/lib/copilot/request/tools/executor.ts b/apps/sim/lib/copilot/request/tools/executor.ts index 0184583015..f4712a6fff 100644 --- a/apps/sim/lib/copilot/request/tools/executor.ts +++ b/apps/sim/lib/copilot/request/tools/executor.ts @@ -20,36 +20,36 @@ import { MothershipStreamV1ToolPhase, } from '@/lib/copilot/generated/mothership-stream-v1' import { + ApplyFileEdit, BrowserRequestTakeover, - CrawlWebsite, - CreateFile, + CreateEmptyFile, CreateWorkflow, - DeployApi, - DeployChat, - DeployCustomBlock, - DeployMcp, - DownloadToWorkspaceFile, - EditContent, + DeployAsApi, + DeployAsChat, + DeployAsMcp, + DownloadFile, Ffmpeg, - FunctionExecute, GenerateApiKey, GenerateAudio, GenerateImage, GenerateVideo, - KnowledgeBase, LoadDeployment, - MaterializeFile, + ManageKnowledgeBase, Media, + PrepareFileEdit, PromoteToLive, + PublishCustomBlock, Redeploy, Run, RunBlock, RunCode, RunFromBlock, + RunFunction, RunWorkflow, RunWorkflowUntilBlock, + SaveUpload, Search, - WorkspaceFile, + WebCrawl, } from '@/lib/copilot/generated/tool-catalog-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { publishToolConfirmation } from '@/lib/copilot/persistence/tool-confirm' @@ -217,7 +217,7 @@ const LONG_RUNNING_TOOL_IDS: ReadonlySet = new Set([ RunFromBlock.id, RunWorkflow.id, RunWorkflowUntilBlock.id, - FunctionExecute.id, + RunFunction.id, RunCode.id, GenerateImage.id, GenerateAudio.id, @@ -225,17 +225,17 @@ const LONG_RUNNING_TOOL_IDS: ReadonlySet = new Set([ Ffmpeg.id, Media.id, Search.id, - CrawlWebsite.id, - KnowledgeBase.id, - DownloadToWorkspaceFile.id, - CreateFile.id, - EditContent.id, - MaterializeFile.id, - WorkspaceFile.id, - DeployApi.id, - DeployChat.id, - DeployCustomBlock.id, - DeployMcp.id, + WebCrawl.id, + ManageKnowledgeBase.id, + DownloadFile.id, + CreateEmptyFile.id, + ApplyFileEdit.id, + SaveUpload.id, + PrepareFileEdit.id, + DeployAsApi.id, + DeployAsChat.id, + PublishCustomBlock.id, + DeployAsMcp.id, Redeploy.id, LoadDeployment.id, PromoteToLive.id, diff --git a/apps/sim/lib/copilot/request/tools/files.test.ts b/apps/sim/lib/copilot/request/tools/files.test.ts index 19c04e383e..7a6b94dbe8 100644 --- a/apps/sim/lib/copilot/request/tools/files.test.ts +++ b/apps/sim/lib/copilot/request/tools/files.test.ts @@ -24,7 +24,7 @@ vi.mock('@/lib/copilot/request/otel', () => ({ ) => fn({ setAttribute: vi.fn(), setAttributes: vi.fn(), addEvent: vi.fn() }), })) -import { FunctionExecute } from '@/lib/copilot/generated/tool-catalog-v1' +import { RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { extractTabularData, maybeWriteOutputToFile, @@ -37,7 +37,7 @@ import { MAX_INLINE_MATERIALIZATION_BYTES } from '@/lib/execution/payloads/limit import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry' describe('unwrapFunctionExecuteOutput', () => { - it('unwraps the function_execute envelope { result, stdout }', () => { + it('unwraps the run_function envelope { result, stdout }', () => { expect(unwrapFunctionExecuteOutput({ result: 'name,age\nAlice,30', stdout: '' })).toBe( 'name,age\nAlice,30' ) @@ -56,7 +56,7 @@ describe('unwrapFunctionExecuteOutput', () => { }) describe('serializeOutputForFile (csv)', () => { - it('returns raw CSV text when function_execute result is already a CSV string', () => { + it('returns raw CSV text when run_function result is already a CSV string', () => { const output = { result: 'name,age\nAlice,30\nBob,40', stdout: '(2 rows)', @@ -139,7 +139,7 @@ describe('maybeWriteOutputToFile', () => { it('denies a read-only principal without writing the file', async () => { const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: 'name,age\nAlice,30', stdout: '' } }, buildContext({ userPermission: 'read' }) @@ -152,7 +152,7 @@ describe('maybeWriteOutputToFile', () => { it('does not deny a read-only principal when no workspace write occurs (sandbox export active)', async () => { const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: { files: [{ path: 'report.csv' }] }, stdout: '' } }, buildContext({ userPermission: 'read' }) @@ -164,7 +164,7 @@ describe('maybeWriteOutputToFile', () => { it('writes the output file for a write principal', async () => { const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: 'name,age\nAlice,30', stdout: '' } }, buildContext() @@ -201,7 +201,7 @@ describe('maybeWriteOutputToFile', () => { })) const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.json', mode: 'overwrite' }] } }, { success: true, output: { result: rows, stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -239,7 +239,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('TOKEN', 'secret-value') const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.txt', mode: 'overwrite' }] } }, { success: true, @@ -280,7 +280,7 @@ describe('maybeWriteOutputToFile', () => { } const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.json', mode: 'overwrite' }] } }, { success: true, output: runtimeOutput }, buildContext({ resolvedSecretTraceRegistry: toolRegistry }) @@ -317,7 +317,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('CSV_SECRET', secret) const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: [{ value: secret }], stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -356,7 +356,7 @@ describe('maybeWriteOutputToFile', () => { const rows = Array.from({ length: 10_000 }, () => ({ first: secret, second: secret })) const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: rows, stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -377,7 +377,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('CSV_SECRET', secret) const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [ @@ -402,7 +402,7 @@ describe('maybeWriteOutputToFile', () => { })) const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files } }, { success: true, output: { result: 'content', stdout: '' } }, buildContext() @@ -420,7 +420,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('API_KEY', 'secret-value') const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.txt', mode: 'overwrite' }] } }, { success: true, output: { result: '__var_API_KEY', stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -457,7 +457,7 @@ describe('maybeWriteOutputToFile', () => { } as unknown as ResolvedSecretTraceRegistry const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.txt', mode: 'overwrite' }] } }, { success: true, output: { result: 'anonymous-secret', stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -495,7 +495,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('OUTPUT_SECRET', 'secret-value') const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.txt', mode: 'overwrite' }] } }, { success: true, output: { result: 'secret-value', stdout: '' } }, buildContext({ userId: 'billing-actor', resolvedSecretTraceRegistry: registry }) @@ -534,7 +534,7 @@ describe('maybeWriteOutputToFile', () => { registry.recordResolved('OUTPUT_SECRET', 'secret-value') const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.txt', mode: 'overwrite' }] } }, { success: true, output: { result: 'secret-value', stdout: '' } }, buildContext({ userId: 'billing-actor', resolvedSecretTraceRegistry: registry }) @@ -557,7 +557,7 @@ describe('maybeWriteOutputToFile', () => { } as unknown as ResolvedSecretTraceRegistry const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [ @@ -580,7 +580,7 @@ describe('maybeWriteOutputToFile', () => { it('preserves legacy writes without a registry and marks their provenance unknown', async () => { const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.json', mode: 'overwrite' }] } }, { success: true, output: { result: { token: 'unknown' }, stdout: '' } }, buildContext({ resolvedSecretTraceRegistry: undefined }) @@ -595,7 +595,7 @@ describe('maybeWriteOutputToFile', () => { it('fails loudly instead of silently skipping declared outputs when workspace context is missing', async () => { const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, { outputs: { files: [{ path: 'files/report.csv', mode: 'overwrite' }] } }, { success: true, output: { result: 'name,age\nAlice,30', stdout: '' } }, buildContext({ workspaceId: undefined }) @@ -611,7 +611,7 @@ describe('maybeWriteOutputToFile', () => { it('still passes results through untouched when no outputs are declared, even without workspace context', async () => { const original = { success: true, output: { result: 42, stdout: '' } } const result = await maybeWriteOutputToFile( - FunctionExecute.id, + RunFunction.id, {}, original, buildContext({ workspaceId: undefined }) @@ -627,7 +627,7 @@ describe('extractTabularData', () => { expect(extractTabularData([{ a: 1 }, { a: 2 }])).toEqual([{ a: 1 }, { a: 2 }]) }) - it('does NOT unwrap function_execute envelopes on its own (callers must pre-unwrap)', () => { + it('does NOT unwrap run_function envelopes on its own (callers must pre-unwrap)', () => { // Caller is responsible for unwrapping { result, stdout } envelopes first. // Keeping that concern out of this function prevents a double unwrap when // the user's payload itself happens to have matching keys. diff --git a/apps/sim/lib/copilot/request/tools/files.ts b/apps/sim/lib/copilot/request/tools/files.ts index 035ca0b85d..22f1a641f9 100644 --- a/apps/sim/lib/copilot/request/tools/files.ts +++ b/apps/sim/lib/copilot/request/tools/files.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { FunctionExecute, UserTable } from '@/lib/copilot/generated/tool-catalog-v1' +import { RunFunction, UserTable } from '@/lib/copilot/generated/tool-catalog-v1' import { CopilotOutputFileOutcome } from '@/lib/copilot/generated/trace-attribute-values-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1' @@ -26,7 +26,7 @@ import type { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secr const logger = createLogger('CopilotToolResultFiles') const MAX_OUTPUT_FILE_PROVENANCE_REPRESENTATIONS = 10_000 -export const OUTPUT_PATH_TOOLS: Set = new Set([FunctionExecute.id, UserTable.id]) +export const OUTPUT_PATH_TOOLS: Set = new Set([RunFunction.id, UserTable.id]) export type OutputFormat = 'json' | 'csv' | 'txt' | 'md' | 'html' @@ -47,12 +47,12 @@ export const FORMAT_TO_CONTENT_TYPE: Record = { } /** - * Unwraps the `function_execute` response envelope `{ result, stdout }` so the + * Unwraps the `run_function` response envelope `{ result, stdout }` so the * rest of the serialization code works on the user's actual payload (a string, * array, object, etc.) instead of JSON-stringifying the envelope itself. * * Only unwraps when both keys are present — that's the unique shape of - * `function_execute` (see `apps/sim/tools/function/types.ts` `CodeExecutionOutput`). + * `run_function` (see `apps/sim/tools/function/types.ts` `CodeExecutionOutput`). * `user_table` returns `{ data, message, success }` which is left alone. */ export function unwrapFunctionExecuteOutput(output: unknown): unknown { @@ -66,7 +66,7 @@ export function unwrapFunctionExecuteOutput(output: unknown): unknown { /** * Try to pull a flat array of row-objects out of an already-unwrapped tool - * payload. Callers are responsible for stripping any `function_execute` + * payload. Callers are responsible for stripping any `run_function` * envelope first (via {@link unwrapFunctionExecuteOutput}) — this function * does not re-unwrap, so a user payload that coincidentally has `result` and * `stdout` keys is not mistaken for another envelope. diff --git a/apps/sim/lib/copilot/request/tools/permission.test.ts b/apps/sim/lib/copilot/request/tools/permission.test.ts index 5c75b64573..58a6e6c811 100644 --- a/apps/sim/lib/copilot/request/tools/permission.test.ts +++ b/apps/sim/lib/copilot/request/tools/permission.test.ts @@ -94,7 +94,7 @@ describe('toolCallNeedsApproval', () => { expect(toolCallNeedsApproval('terminal', context, {}, false, runCall)).toBe(false) }) - it.each(['deploy_api', 'deploy_chat', 'deploy_mcp'])( + it.each(['deploy_as_api', 'deploy_as_chat', 'deploy_as_mcp'])( 'honors the saved permission for a %s undeploy', (toolName) => { const context = makeContext() @@ -108,10 +108,10 @@ describe('toolCallNeedsApproval', () => { it('applies the normal saved permission to code with a secret reference', () => { const context = makeContext() - context.toolPermissions.autoAllowed.add('function_execute') + context.toolPermissions.autoAllowed.add('run_function') expect( - toolCallNeedsApproval('function_execute', context, {}, false, { + toolCallNeedsApproval('run_function', context, {}, false, { language: 'javascript', code: 'return {{API_KEY}}', }) @@ -135,7 +135,7 @@ describe('toolCallNeedsApproval', () => { context.toolPermissions.enabled = false expect( - toolCallNeedsApproval('function_execute', context, {}, false, { + toolCallNeedsApproval('run_function', context, {}, false, { language: 'javascript', code: 'return {{API_KEY}}', }) @@ -214,13 +214,13 @@ describe('gated tools are askable', () => { ).toEqual([ 'call_integration_tool', 'delete_workspace_mcp_server', - 'deploy_api', - 'deploy_chat', - 'deploy_mcp', - 'function_execute', + 'deploy_as_api', + 'deploy_as_chat', + 'deploy_as_mcp', 'promote_to_live', 'redeploy', 'run_code', + 'run_function', 'run_workflow', 'run_workflow_until_block', 'terminal', @@ -323,7 +323,7 @@ describe('runGatedToolExecution', () => { it('accepts the normal chat-level decision for code with a secret reference', async () => { const context = makeContext() const toolCall = makeToolCall() - toolCall.name = 'function_execute' + toolCall.name = 'run_function' toolCall.params = { language: 'javascript', code: 'return {{API_KEY}}' } const execute = vi.fn().mockResolvedValue({ status: 'success' }) waitForToolPermissionDecision.mockResolvedValue({ @@ -334,7 +334,7 @@ describe('runGatedToolExecution', () => { await gate(context, toolCall, execute, []) expect(execute).toHaveBeenCalledTimes(1) - expect(context.toolPermissions.autoAllowed.has('function_execute')).toBe(true) + expect(context.toolPermissions.autoAllowed.has('run_function')).toBe(true) }) it('does not suppress later prompts for a one-off allow', async () => { diff --git a/apps/sim/lib/copilot/request/tools/permissions.ts b/apps/sim/lib/copilot/request/tools/permissions.ts index 2d12c12ab1..fcd1e8d934 100644 --- a/apps/sim/lib/copilot/request/tools/permissions.ts +++ b/apps/sim/lib/copilot/request/tools/permissions.ts @@ -4,7 +4,7 @@ import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/typ /** * Guards a post-tool output-redirection sink against read-only principals. * - * `function_execute`, `user_table`, and `read` are read-allowed for execution + * `run_function`, `user_table`, and `read` are read-allowed for execution * (they don't mutate the workspace themselves), so the router's `WRITE_ACTIONS` * gate in `tools/server/router.ts` lets read-only collaborators run them. But * their output-redirection declarations (`outputs.files`, `outputTable`) @@ -12,7 +12,7 @@ import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/typ * Those writes must satisfy the same write gate as the dedicated mutation tools. * * Returns a denial `ToolCallResult` when the caller lacks write access (so the - * agent surfaces the same `Permission denied` outcome it gets from `create_file` + * agent surfaces the same `Permission denied` outcome it gets from `create_empty_file` * / `user_table` writes), or `null` when the write may proceed. */ export function denyOutputWriteWithoutWritePermission( diff --git a/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts b/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts index 26bff98ec4..536ac1adf8 100644 --- a/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts +++ b/apps/sim/lib/copilot/request/tools/resolved-secret-result.test.ts @@ -2,7 +2,7 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { FunctionExecute, RunCode } from '@/lib/copilot/generated/tool-catalog-v1' +import { RunCode, RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { projectToolResultForCopilot, TOOL_RESULT_UNAVAILABLE_ERROR, @@ -20,7 +20,7 @@ function createRegistry(): ResolvedSecretTraceRegistry { } describe('projectToolResultForCopilot', () => { - it.each([FunctionExecute.id, RunCode.id])( + it.each([RunFunction.id, RunCode.id])( 'projects active exact and embedded secrets for %s without mutating runtime output', (toolName) => { const registry = createRegistry() diff --git a/apps/sim/lib/copilot/request/tools/tables.test.ts b/apps/sim/lib/copilot/request/tools/tables.test.ts index 57347739a4..ba68f10f7c 100644 --- a/apps/sim/lib/copilot/request/tools/tables.test.ts +++ b/apps/sim/lib/copilot/request/tools/tables.test.ts @@ -30,7 +30,7 @@ vi.mock('@/lib/table/application/rows', () => ({ ProjectedWireRowsValidationError: class ProjectedWireRowsValidationError extends Error {}, })) -import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' +import { Read as ReadTool, RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { projectToolResultForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result' import { maybeWriteOutputToTable, @@ -94,7 +94,7 @@ describe('automatic Copilot tool-output table persistence', () => { ] const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: rows } }, context @@ -129,7 +129,7 @@ describe('automatic Copilot tool-output table persistence', () => { const runtimeRows = [{ name: 'secret-value', status: 'literal' }] const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: runtimeRows } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -159,7 +159,7 @@ describe('automatic Copilot tool-output table persistence', () => { registry.markIncomplete('unspecified') await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: [{ name: 'unknown' }] } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -179,7 +179,7 @@ describe('automatic Copilot tool-output table persistence', () => { ) const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: [{ wrong: true }] } }, buildContext() @@ -199,7 +199,7 @@ describe('automatic Copilot tool-output table persistence', () => { mocks.executeReplace.mockRejectedValueOnce(new Error('database duplicate: secret-value')) const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: [{ name: 'secret-value' }] } }, buildContext({ resolvedSecretTraceRegistry: registry }) @@ -217,7 +217,7 @@ describe('automatic Copilot tool-output table persistence', () => { it('rejects read-only Copilot execution before any application command', async () => { const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: [{ name: 'Ada' }] } }, buildContext({ userPermission: 'read' }) @@ -232,7 +232,7 @@ describe('automatic Copilot tool-output table persistence', () => { mocks.executeReplace.mockResolvedValueOnce({ table, deletedCount: 1, insertedCount: 1 }) const result = await maybeWriteOutputToTable( - FunctionExecute.id, + RunFunction.id, { outputTable: 'table-1' }, { success: true, output: { result: [{ name: 'Ada' }, { name: 'Grace' }] } }, buildContext() diff --git a/apps/sim/lib/copilot/request/tools/tables.ts b/apps/sim/lib/copilot/request/tools/tables.ts index 46d4d598e6..36eecdc220 100644 --- a/apps/sim/lib/copilot/request/tools/tables.ts +++ b/apps/sim/lib/copilot/request/tools/tables.ts @@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger' import { parse as csvParse } from 'csv-parse/sync' import { executeCopilotReplaceProjectedWireRows } from '@/lib/copilot/application/table-commands' import { messageForCopilotTableError } from '@/lib/copilot/auth/table-delegation' -import { FunctionExecute, Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1' +import { Read as ReadTool, RunFunction } from '@/lib/copilot/generated/tool-catalog-v1' import { CopilotTableOutcome } from '@/lib/copilot/generated/trace-attribute-values-v1' import { TraceAttr } from '@/lib/copilot/generated/trace-attributes-v1' import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1' @@ -68,7 +68,7 @@ export async function maybeWriteOutputToTable( result: ToolCallResult, context: ExecutionContext ): Promise { - if (toolName !== FunctionExecute.id) return result + if (toolName !== RunFunction.id) return result if (!result.success || !result.output) return result const outputTable = params?.outputTable as string | undefined if (!outputTable) return result diff --git a/apps/sim/lib/copilot/request/types.ts b/apps/sim/lib/copilot/request/types.ts index ed76e5cd50..580b17238f 100644 --- a/apps/sim/lib/copilot/request/types.ts +++ b/apps/sim/lib/copilot/request/types.ts @@ -41,7 +41,7 @@ export interface ToolCallState { * For a subagent-scoped tool call, the invoking subagent's channel id (its * outer tool_use id, = event.scope.parentToolCallId). Captured at dispatch so * the executor can thread it into the server tool context and scope the - * workspace_file -> edit_content intent handoff per file subagent. Undefined + * prepare_file_edit -> apply_file_edit intent handoff per file subagent. Undefined * for main-lane tool calls. */ parentToolCallId?: string diff --git a/apps/sim/lib/copilot/resources/extraction.test.ts b/apps/sim/lib/copilot/resources/extraction.test.ts index 65e8d40bc6..c47413711f 100644 --- a/apps/sim/lib/copilot/resources/extraction.test.ts +++ b/apps/sim/lib/copilot/resources/extraction.test.ts @@ -5,9 +5,9 @@ import { describe, expect, it } from 'vitest' import { extractDeletedResourcesFromToolResult, extractResourcesFromToolResult } from './extraction' describe('extractResourcesFromToolResult', () => { - it('extracts file resources from create_file results', () => { + it('extracts file resources from create_empty_file results', () => { const resources = extractResourcesFromToolResult( - 'create_file', + 'create_empty_file', { fileName: 'notes.md', }, @@ -31,9 +31,9 @@ describe('extractResourcesFromToolResult', () => { ]) }) - it('uses the knowledge base id for knowledge_base tag mutations', () => { + it('uses the knowledge base id for manage_knowledge_base tag mutations', () => { const resources = extractResourcesFromToolResult( - 'knowledge_base', + 'manage_knowledge_base', { operation: 'update_tag', args: { @@ -63,7 +63,7 @@ describe('extractResourcesFromToolResult', () => { it('uses knowledgeBaseId from the tool result when update_tag args omit it', () => { const resources = extractResourcesFromToolResult( - 'knowledge_base', + 'manage_knowledge_base', { operation: 'update_tag', args: { @@ -93,7 +93,7 @@ describe('extractResourcesFromToolResult', () => { it('does not create resources for read-only knowledge base tag operations', () => { const resources = extractResourcesFromToolResult( - 'knowledge_base', + 'manage_knowledge_base', { operation: 'list_tags', args: { @@ -156,7 +156,7 @@ describe('extractDeletedResourcesFromToolResult', () => { { from: 'workflows/Lead%20Router', kind: 'workflow', id: 'wf-1' }, { from: 'workflows/Old%20Projects', kind: 'workflow_folder', id: 'wfolder-1' }, { from: 'tables/Leads', kind: 'table', id: 'tbl-1' }, - { from: 'knowledgebases/support-docs', kind: 'knowledge_base', id: 'kb-1' }, + { from: 'knowledgebases/support-docs', kind: 'manage_knowledge_base', id: 'kb-1' }, { from: 'files/missing.md', kind: 'file', error: 'Not found: files/missing.md' }, ], } @@ -181,10 +181,10 @@ describe('extractDeletedResourcesFromToolResult', () => { ).toEqual([{ type: 'table', id: 'table-1', title: 'Table' }]) }) - it('extracts deleted knowledge bases from knowledge_base result data', () => { + it('extracts deleted knowledge bases from manage_knowledge_base result data', () => { expect( extractDeletedResourcesFromToolResult( - 'knowledge_base', + 'manage_knowledge_base', { operation: 'delete', args: { knowledgeBaseIds: ['kb-1'] } }, { success: true, diff --git a/apps/sim/lib/copilot/resources/extraction.ts b/apps/sim/lib/copilot/resources/extraction.ts index 2a614d944b..fd1782a59f 100644 --- a/apps/sim/lib/copilot/resources/extraction.ts +++ b/apps/sim/lib/copilot/resources/extraction.ts @@ -1,18 +1,18 @@ import { - CreateFile, + CreateEmptyFile, CreateWorkflow, - DownloadToWorkspaceFile, + DownloadFile, EditWorkflow, Ffmpeg, - FunctionExecute, GenerateAudio, GenerateImage, GenerateVideo, Knowledge, - KnowledgeBase, + ManageKnowledgeBase, + PrepareFileEdit, Rm, + RunFunction, UserTable, - WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' import type { MothershipResource, MothershipResourceType } from './types' @@ -21,13 +21,13 @@ type ResourceType = MothershipResourceType const RESOURCE_TOOL_NAMES: Set = new Set([ UserTable.id, - CreateFile.id, - WorkspaceFile.id, - DownloadToWorkspaceFile.id, + CreateEmptyFile.id, + PrepareFileEdit.id, + DownloadFile.id, CreateWorkflow.id, EditWorkflow.id, - FunctionExecute.id, - KnowledgeBase.id, + RunFunction.id, + ManageKnowledgeBase.id, Knowledge.id, GenerateImage.id, GenerateVideo.id, @@ -110,8 +110,8 @@ export function extractResourcesFromToolResult( return [] } - case CreateFile.id: - case WorkspaceFile.id: { + case CreateEmptyFile.id: + case PrepareFileEdit.id: { const file = asRecord(data.file) if (file.id) { return [{ type: 'file', id: file.id as string, title: (file.name as string) || 'File' }] @@ -124,7 +124,7 @@ export function extractResourcesFromToolResult( return [] } - case FunctionExecute.id: { + case RunFunction.id: { if (result.tableId) { return [ { @@ -146,7 +146,7 @@ export function extractResourcesFromToolResult( return [] } - case DownloadToWorkspaceFile.id: + case DownloadFile.id: case GenerateImage.id: case GenerateVideo.id: case GenerateAudio.id: @@ -181,7 +181,7 @@ export function extractResourcesFromToolResult( return [] } - case KnowledgeBase.id: { + case ManageKnowledgeBase.id: { if (READ_ONLY_KB_OPS.has(getOperation(params) ?? '')) return [] const args = asRecord(params?.args) @@ -225,9 +225,9 @@ export function extractResourcesFromToolResult( } const DELETE_CAPABLE_TOOL_RESOURCE_TYPE: Record = { - [WorkspaceFile.id]: 'file', + [PrepareFileEdit.id]: 'file', [UserTable.id]: 'table', - [KnowledgeBase.id]: 'knowledgebase', + [ManageKnowledgeBase.id]: 'knowledgebase', // rm spans categories, so unlike every other entry its resource type comes // from each outcome's kind rather than from this map. The entry exists so // hasDeleteCapability(rm) holds; the rm case below ignores this value. @@ -241,7 +241,7 @@ const RM_KIND_RESOURCE_TYPE: Record = { workflow: 'workflow', workflow_folder: 'folder', table: 'table', - knowledge_base: 'knowledgebase', + manage_knowledge_base: 'knowledgebase', } export function hasDeleteCapability(toolName: string): boolean { @@ -281,7 +281,7 @@ export function extractDeletedResourcesFromToolResult( return [{ type, id, title: leaf ? decodeURIComponent(leaf) : 'Deleted resource' }] }) } - case WorkspaceFile.id: { + case PrepareFileEdit.id: { if (operation !== 'delete') return [] const target = getWorkspaceFileTarget(params) const fileId = (data.id as string) ?? (target.fileId as string) ?? (args.fileId as string) @@ -306,7 +306,7 @@ export function extractDeletedResourcesFromToolResult( return [] } - case KnowledgeBase.id: { + case ManageKnowledgeBase.id: { if (operation !== 'delete') return [] const deleted = Array.isArray(data.deleted) ? data.deleted : [] const resources = deleted.flatMap((entry): ChatResource[] => { diff --git a/apps/sim/lib/copilot/resources/types.ts b/apps/sim/lib/copilot/resources/types.ts index 7edc630d19..bc25956d3d 100644 --- a/apps/sim/lib/copilot/resources/types.ts +++ b/apps/sim/lib/copilot/resources/types.ts @@ -20,6 +20,8 @@ export interface MothershipResource { id: string title: string path?: string + /** Saved table view to open pinned (type "table" only). */ + viewId?: string } /** diff --git a/apps/sim/lib/copilot/tool-executor/executor.test.ts b/apps/sim/lib/copilot/tool-executor/executor.test.ts index 11672c1e8e..276585eeb7 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.test.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.test.ts @@ -48,25 +48,23 @@ describe('copilot tool executor fallback', () => { isSimExecuted.mockReturnValue(true) isClientExecuted.mockReturnValue(false) const handler = vi.fn().mockResolvedValue({ success: true }) - registerHandler('function_execute', handler) + registerHandler('run_function', handler) await expect( - executeTool('function_execute', { code: 'return 1' }, { userId: 'user-1', workflowId: '' }) + executeTool('run_function', { code: 'return 1' }, { userId: 'user-1', workflowId: '' }) ).resolves.toEqual({ success: false, - error: - "Permission denied: function_execute requires write access. You have 'none' permission.", + error: "Permission denied: run_function requires write access. You have 'none' permission.", }) await expect( executeTool( - 'function_execute', + 'run_function', { code: 'return 1' }, { userId: 'user-1', workflowId: '', userPermission: 'read' } ) ).resolves.toEqual({ success: false, - error: - "Permission denied: function_execute requires write access. You have 'read' permission.", + error: "Permission denied: run_function requires write access. You have 'read' permission.", }) expect(handler).not.toHaveBeenCalled() }) @@ -77,11 +75,11 @@ describe('copilot tool executor fallback', () => { isSimExecuted.mockReturnValue(true) isClientExecuted.mockReturnValue(false) const handler = vi.fn().mockResolvedValue({ success: true, output: 'ok' }) - registerHandler('function_execute', handler) + registerHandler('run_function', handler) await expect( executeTool( - 'function_execute', + 'run_function', { code: 'return 1' }, { userId: 'user-1', workflowId: '', userPermission: 'write' } ) @@ -263,13 +261,13 @@ describe('copilot tool executor fallback', () => { expect(executeAppTool).toHaveBeenCalledWith('unknown_client_tool', expect.any(Object)) }) - it('converts function_execute timeout from seconds to milliseconds for copilot calls', async () => { + it('converts run_function timeout from seconds to milliseconds for copilot calls', async () => { isKnownTool.mockReturnValue(false) isSimExecuted.mockReturnValue(false) executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) await executeTool( - 'function_execute', + 'run_function', { code: 'return 1', timeout: 7 }, { userId: 'user-1', @@ -280,7 +278,7 @@ describe('copilot tool executor fallback', () => { ) expect(executeAppTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ timeout: 7000, _context: expect.objectContaining({ @@ -296,12 +294,12 @@ describe('copilot tool executor fallback', () => { ) }) - it('converts function_execute timeout before invoking its registered Sim handler', async () => { + it('converts run_function timeout before invoking its registered Sim handler', async () => { isKnownTool.mockReturnValue(true) isSimExecuted.mockReturnValue(true) isClientExecuted.mockReturnValue(false) const handler = vi.fn().mockResolvedValue({ success: true, output: { result: 'ok' } }) - registerHandler('function_execute', handler) + registerHandler('run_function', handler) const context = { userId: 'user-1', @@ -309,7 +307,7 @@ describe('copilot tool executor fallback', () => { workspaceId: 'ws-1', copilotToolExecution: true, } - await executeTool('function_execute', { code: 'return 1', timeout: 7 }, context) + await executeTool('run_function', { code: 'return 1', timeout: 7 }, context) expect(handler).toHaveBeenCalledWith( expect.objectContaining({ @@ -321,13 +319,13 @@ describe('copilot tool executor fallback', () => { expect(executeAppTool).not.toHaveBeenCalled() }) - it('defaults copilot function_execute timeout to 10 seconds when omitted', async () => { + it('defaults copilot run_function timeout to 10 seconds when omitted', async () => { isKnownTool.mockReturnValue(false) isSimExecuted.mockReturnValue(false) executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) await executeTool( - 'function_execute', + 'run_function', { code: 'return 1' }, { userId: 'user-1', @@ -338,7 +336,7 @@ describe('copilot tool executor fallback', () => { ) expect(executeAppTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ timeout: 10_000, }), @@ -351,13 +349,13 @@ describe('copilot tool executor fallback', () => { ) }) - it('defaults copilot function_execute timeout to 10 seconds when invalid', async () => { + it('defaults copilot run_function timeout to 10 seconds when invalid', async () => { isKnownTool.mockReturnValue(false) isSimExecuted.mockReturnValue(false) executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) await executeTool( - 'function_execute', + 'run_function', { code: 'return 1', timeout: 0 }, { userId: 'user-1', @@ -368,7 +366,7 @@ describe('copilot tool executor fallback', () => { ) expect(executeAppTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ timeout: 10_000, }), @@ -381,13 +379,13 @@ describe('copilot tool executor fallback', () => { ) }) - it('does not let copilot function_execute timeout exceed the default execution limit', async () => { + it('does not let copilot run_function timeout exceed the default execution limit', async () => { isKnownTool.mockReturnValue(false) isSimExecuted.mockReturnValue(false) executeAppTool.mockResolvedValue({ success: true, output: { result: 'ok' } }) await executeTool( - 'function_execute', + 'run_function', { code: 'return 1', timeout: 10_000 }, { userId: 'user-1', @@ -398,7 +396,7 @@ describe('copilot tool executor fallback', () => { ) expect(executeAppTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ timeout: DEFAULT_EXECUTION_TIMEOUT_MS, }), diff --git a/apps/sim/lib/copilot/tool-executor/executor.ts b/apps/sim/lib/copilot/tool-executor/executor.ts index 6184d68220..969e427535 100644 --- a/apps/sim/lib/copilot/tool-executor/executor.ts +++ b/apps/sim/lib/copilot/tool-executor/executor.ts @@ -13,7 +13,7 @@ import type { } from './types' const logger = createLogger('ToolExecutor') -const FUNCTION_EXECUTE_TOOL_ID = 'function_execute' +const FUNCTION_EXECUTE_TOOL_ID = 'run_function' const DEFAULT_FUNCTION_EXECUTE_TIMEOUT_SECONDS = 10 const MILLISECONDS_PER_SECOND = 1000 diff --git a/apps/sim/lib/copilot/tool-executor/register-handlers.ts b/apps/sim/lib/copilot/tool-executor/register-handlers.ts index f2f9eb6d30..61276eb752 100644 --- a/apps/sim/lib/copilot/tool-executor/register-handlers.ts +++ b/apps/sim/lib/copilot/tool-executor/register-handlers.ts @@ -1,42 +1,40 @@ import { createLogger } from '@sim/logger' import { - CheckDeploymentStatus, Cp as CpTool, CreateWorkflow, CreateWorkspaceMcpServer, DeleteWorkspaceMcpServer, - DeployApi, - DeployChat, - DeployCustomBlock, - DeployMcp, + DeployAsApi, + DeployAsChat, + DeployAsMcp, DiffWorkflows, - FunctionExecute, GenerateApiKey, GetBlockOutputs, GetBlockUpstreamReferences, GetDeployedWorkflowState, - GetDeploymentLog, - GetPlatformActions, + GetDeploymentStatus, + GetUiReference, GetWorkflowData, GetWorkflowRunOptions, Glob as GlobTool, Grep as GrepTool, + ListDeploymentVersions, ListIntegrationTools, ListUserWorkspaces, ListWorkspaceMcpServers, LoadDeployment, ManageCredential, ManageCustomTool, - ManageMcpTool, + ManageMcpConnection, ManageSandbox, ManageSkill, - MaterializeFile, Mkdir as MkdirTool, Mv as MvTool, OauthGetAuthLink, OauthRequestAccess, OpenResource, PromoteToLive, + PublishCustomBlock, Read as ReadTool, Redeploy, RestoreResource, @@ -44,8 +42,10 @@ import { RunBlock, RunCode, RunFromBlock, + RunFunction, RunWorkflow, RunWorkflowUntilBlock, + SaveUpload, SetBlockEnabled, SetGlobalWorkflowVariables, UpdateDeploymentVersion, @@ -156,17 +156,17 @@ function buildHandlerMap(): Record { [GenerateApiKey.id]: h(executeGenerateApiKey), [SetGlobalWorkflowVariables.id]: h(executeSetGlobalWorkflowVariables), - [DeployApi.id]: h(executeDeployApi), - [DeployChat.id]: h(executeDeployChat), - [DeployMcp.id]: h(executeDeployMcp), - [DeployCustomBlock.id]: h(executeDeployCustomBlock), + [DeployAsApi.id]: h(executeDeployApi), + [DeployAsChat.id]: h(executeDeployChat), + [DeployAsMcp.id]: h(executeDeployMcp), + [PublishCustomBlock.id]: h(executeDeployCustomBlock), [Redeploy.id]: h(executeRedeploy), - [CheckDeploymentStatus.id]: h(executeCheckDeploymentStatus), + [GetDeploymentStatus.id]: h(executeCheckDeploymentStatus), [ListWorkspaceMcpServers.id]: h(executeListWorkspaceMcpServers), [CreateWorkspaceMcpServer.id]: h(executeCreateWorkspaceMcpServer), [UpdateWorkspaceMcpServer.id]: h(executeUpdateWorkspaceMcpServer), [DeleteWorkspaceMcpServer.id]: h(executeDeleteWorkspaceMcpServer), - [GetDeploymentLog.id]: h(executeGetDeploymentLog), + [ListDeploymentVersions.id]: h(executeGetDeploymentLog), [DiffWorkflows.id]: h(executeDiffWorkflows), [LoadDeployment.id]: h(executeLoadDeployment), [PromoteToLive.id]: h(executePromoteToLive), @@ -181,7 +181,7 @@ function buildHandlerMap(): Record { [RmTool.id]: h(executeVfsRm), [ManageCustomTool.id]: h(executeManageCustomTool), - [ManageMcpTool.id]: h(executeManageMcpTool), + [ManageMcpConnection.id]: h(executeManageMcpTool), [ManageSandbox.id]: h(executeManageSandbox), [ManageSkill.id]: h(executeManageSkill), [ManageCredential.id]: h(executeManageCredential), @@ -192,10 +192,10 @@ function buildHandlerMap(): Record { [OauthRequestAccess.id]: h(executeOAuthRequestAccess), [OpenResource.id]: h(executeOpenResource), [RestoreResource.id]: h(executeRestoreResource), - [GetPlatformActions.id]: h(executeGetPlatformActions), + [GetUiReference.id]: h(executeGetPlatformActions), [ListIntegrationTools.id]: h(executeListIntegrationTools), - [MaterializeFile.id]: h(executeMaterializeFile), - [FunctionExecute.id]: h(executeFunctionExecute), + [SaveUpload.id]: h(executeMaterializeFile), + [RunFunction.id]: h(executeFunctionExecute), [RunCode.id]: h(executeRunCode), ...buildServerToolHandlers(), diff --git a/apps/sim/lib/copilot/tools/client/store-utils.test.ts b/apps/sim/lib/copilot/tools/client/store-utils.test.ts index 7a84982189..788b42781e 100644 --- a/apps/sim/lib/copilot/tools/client/store-utils.test.ts +++ b/apps/sim/lib/copilot/tools/client/store-utils.test.ts @@ -173,8 +173,8 @@ describe('resolveToolDisplay', () => { }) it('falls back to a humanized tool label for generic tools', () => { - expect(resolveToolDisplay('deploy_api', ClientToolCallState.success)?.text).toBe( - 'Executed Deploy API' + expect(resolveToolDisplay('deploy_as_api', ClientToolCallState.success)?.text).toBe( + 'Executed Deploy As API' ) expect(resolveToolDisplay('oauth-integrations', ClientToolCallState.success)?.text).toBe( 'Executed OAuth Integrations' diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/context.test.ts b/apps/sim/lib/copilot/tools/handlers/deployment/context.test.ts index 9a877e3416..178d403603 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/context.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/context.test.ts @@ -33,7 +33,7 @@ describe('getCopilotDeploymentIdempotencyKey', () => { it('separates deployment intents within the same execution', () => { const context = { executionId: 'execution-1', toolCallId: 'call-1' } - expect(getCopilotDeploymentIdempotencyKey(context, 'deploy_api')).not.toBe( + expect(getCopilotDeploymentIdempotencyKey(context, 'deploy_as_api')).not.toBe( getCopilotDeploymentIdempotencyKey(context, 'redeploy') ) }) diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.test.ts b/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.test.ts index e37f623a23..1be8a3f514 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.test.ts @@ -192,7 +192,7 @@ describe('executeDeployCustomBlock', () => { const result = await executeDeployCustomBlock({ name: 'Enrich Lead' }, context) expect(result.success).toBe(false) - expect(result.error).toContain('deploy_api') + expect(result.error).toContain('deploy_as_api') expect(publishCustomBlockMock).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.ts b/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.ts index 58f1ac5464..c76fdf32b7 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/custom-block.ts @@ -262,7 +262,7 @@ export async function executeDeployCustomBlock( return { success: false, error: - 'Workflow must be deployed before publishing as a custom block. Use deploy_api first.', + 'Workflow must be deployed before publishing as a custom block. Use deploy_as_api first.', } } // Curation is required on publish: every consumer-visible field must be one diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.test.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.test.ts index 01727a9c9d..cd5b1ec5cd 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.test.ts @@ -134,7 +134,7 @@ describe('deployment handlers', () => { expect.any(Object), expect.objectContaining({ operation: expect.objectContaining({ id: 'workflows.deploy' }) }), expect.objectContaining({ - idempotencyKey: 'copilot:execution-1:operation:deploy_api', + idempotencyKey: 'copilot:execution-1:operation:deploy_as_api', }) ) }) @@ -170,7 +170,7 @@ describe('deployment handlers', () => { expect.any(Object), expect.objectContaining({ operation: expect.objectContaining({ id: 'workflows.deploy' }) }), expect.objectContaining({ - idempotencyKey: 'copilot:execution-1:operation:deploy_api', + idempotencyKey: 'copilot:execution-1:operation:deploy_as_api', }) ) }) diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts index 5fd15db45d..d5a0260f2d 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/deploy.ts @@ -211,7 +211,7 @@ export async function executeDeployApi( description: versionDescription, name: versionName, requestId: generateRequestId(), - idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_api'), + idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_as_api'), }) if (!result.success) { return { success: false, error: result.error || 'Failed to deploy workflow' } @@ -377,7 +377,7 @@ export async function executeDeployChat( includeThinking: params.includeThinking, includeToolCalls: params.includeToolCalls, requestId: generateRequestId(), - idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_chat'), + idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_as_chat'), }) const baseUrl = getBaseUrl() @@ -591,7 +591,7 @@ export async function executeRedeploy( description: versionDescription, name: versionName, requestId: generateRequestId(), - idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_api'), + idempotencyKey: getCopilotDeploymentIdempotencyKey(context, 'deploy_as_api'), }) if (!result.success) { return { success: false, error: result.error || 'Failed to redeploy workflow' } diff --git a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts index 0ec758912f..f876876533 100644 --- a/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts +++ b/apps/sim/lib/copilot/tools/handlers/deployment/manage.ts @@ -511,7 +511,8 @@ export async function executeUpdateDeploymentVersion( if (version === null) { return { success: false, - error: 'version must be a deployment version number (use get_deployment_log to find it)', + error: + 'version must be a deployment version number (use list_deployment_versions to find it)', } } diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts index 0717900344..f85769f99d 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.test.ts @@ -247,7 +247,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { ) expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ envVars: { API_KEY: 'secret-value' }, secretScope: 'selected', @@ -272,7 +272,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { expect(mockMaterializeCopilotCodeSecrets).not.toHaveBeenCalled() expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ envVars: {}, secretScope: 'selected', mountedSecrets: [] }), { resolvedSecretTraceRegistry: expect.any(ResolvedSecretTraceRegistry) } ) @@ -295,7 +295,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { names: ['TOKEN'], }, ])( - 'uses the shared $language compiler analysis before delegating source to function_execute', + 'uses the shared $language compiler analysis before delegating source to run_function', async ({ language, code, names }) => { await executeFunctionExecute({ language, code }, context as never) @@ -305,14 +305,14 @@ describe('executeFunctionExecute trace-secret provenance', () => { requestedNames: names, }) expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ code, language, mountedSecrets: names }), { resolvedSecretTraceRegistry: expect.any(ResolvedSecretTraceRegistry) } ) } ) - it('routes run_code shell commands through the same function_execute boundary', async () => { + it('routes run_code shell commands through the same run_function boundary', async () => { const code = 'printf %s "{{CLI_TOKEN}}"' const abortController = new AbortController() @@ -332,7 +332,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { requestedNames: ['CLI_TOKEN'], }) expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ code, language: 'shell', mountedSecrets: ['CLI_TOKEN'] }), { resolvedSecretTraceRegistry: expect.any(ResolvedSecretTraceRegistry), @@ -342,7 +342,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { ) }) - it('uses the trusted Mothership profile for function_execute without accepting a param override', async () => { + it('uses the trusted Mothership profile for run_function without accepting a param override', async () => { await executeFunctionExecute( { code: 'return 1', @@ -353,7 +353,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { ) expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ _context: expect.not.objectContaining({ sandboxProfile: expect.anything() }), }), @@ -373,7 +373,7 @@ describe('executeFunctionExecute trace-secret provenance', () => { expect(mockHasWorkspaceSandboxAccess).toHaveBeenCalledWith('ws_1') expect(mockExecuteTool).toHaveBeenCalledWith( - 'function_execute', + 'run_function', expect.objectContaining({ sandboxId: 'sandbox-1' }), expect.objectContaining({ internalSandboxProfile: 'mothership' }) ) @@ -1216,7 +1216,7 @@ describe('executeFunctionExecute unmountable namespaces', () => { it('keeps the uploads/ guidance intact', async () => { const message = await mountError({ inputFiles: ['uploads/report.json'] }) - expect(message).toContain('materialize_file') + expect(message).toContain('save_upload') }) it('still reports a genuine files/ miss as not found', async () => { diff --git a/apps/sim/lib/copilot/tools/handlers/function-execute.ts b/apps/sim/lib/copilot/tools/handlers/function-execute.ts index 01773a65f0..7580c3382d 100644 --- a/apps/sim/lib/copilot/tools/handlers/function-execute.ts +++ b/apps/sim/lib/copilot/tools/handlers/function-execute.ts @@ -254,10 +254,10 @@ function unmountableNamespaceReason(filePath: string): string | null { const path = `${filePath.replace(/^\/+|\/+$/g, '')}/` if (path.startsWith('uploads/')) { - return 'uploads/ files are not mountable into the sandbox. Use materialize_file to save it to a files/... path first, then mount that canonical path.' + return 'uploads/ files are not mountable into the sandbox. Use save_upload to save it to a files/... path first, then mount that canonical path.' } if (path.startsWith('internal/tool-results/')) { - return 'tool-result artifacts are stored by the copilot backend, not in workspace storage, so read and grep reach them but the sandbox cannot. This path is correct — searching for a different one will not find anything. Either read or grep the artifact and inline the values you need in code, or re-run the tool that produced it with an output path under files/ (function_execute: outputs.files[].path, user_table: outputPath) and mount that files/... path.' + return 'tool-result artifacts are stored by the copilot backend, not in workspace storage, so read and grep reach them but the sandbox cannot. This path is correct — searching for a different one will not find anything. Either read or grep the artifact and inline the values you need in code, or re-run the tool that produced it with an output path under files/ (run_function: outputs.files[].path, user_table: outputPath) and mount that files/... path.' } if (path.startsWith('internal/')) { return 'internal/ paths are served by the copilot backend, not from workspace storage, so read and grep reach them but the sandbox cannot. This path is correct — read or grep it and inline the values you need in code instead of mounting it.' @@ -417,7 +417,7 @@ export async function resolveInputFiles( `Input directory contains too many files (${descendants.length}). Maximum is ${MAX_MOUNTED_FILES}. Mount a smaller directory or individual files.` ) } - logger.info('Mounting workspace directory for function_execute', { + logger.info('Mounting workspace directory for run_function', { vfsPath: dirPath, sandboxPath: mountRoot, fileCount: descendants.length, @@ -745,7 +745,7 @@ export async function executeFunctionExecute( } try { - const result = await executeAppTool('function_execute', enrichedParams, { + const result = await executeAppTool('run_function', enrichedParams, { resolvedSecretTraceRegistry: mountedRegistry, ...(context.abortSignal ? { signal: context.abortSignal } : {}), ...(context.sandboxProfile ? { internalSandboxProfile: context.sandboxProfile } : {}), diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts index a0c5cbb908..7d43faa71b 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-custom-tool.ts @@ -45,7 +45,7 @@ export async function executeManageCustomTool( * workspace — so a caller could name another workspace and have it * authorized against their own. `upsertCustomTools` does no authz of its own * (it only scopes queries by the id it is handed), so nothing downstream - * caught it. Matches manage_mcp_tool and manage_skill. + * caught it. Matches manage_mcp_connection and manage_skill. */ const workspaceId = context.workspaceId diff --git a/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts b/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts index 5158176f27..da6dc0cdd4 100644 --- a/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts +++ b/apps/sim/lib/copilot/tools/handlers/management/manage-mcp-tool.ts @@ -184,12 +184,15 @@ export async function executeManageMcpTool( } } - return { success: false, error: `Unsupported operation for manage_mcp_tool: ${operation}` } + return { + success: false, + error: `Unsupported operation for manage_mcp_connection: ${operation}`, + } } catch (error) { logger.error( context.messageId - ? `manage_mcp_tool execution failed [messageId:${context.messageId}]` - : 'manage_mcp_tool execution failed', + ? `manage_mcp_connection execution failed [messageId:${context.messageId}]` + : 'manage_mcp_connection execution failed', { operation, workspaceId, diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts index 65af6a518c..45eb7b9fb9 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.test.ts @@ -223,19 +223,19 @@ describe('executeMaterializeFile - unsupported operation', () => { ) expect(result.success).toBe(false) - expect(result.error).toContain('Unsupported materialize_file operation "table"') + expect(result.error).toContain('Unsupported save_upload operation "table"') expect(result.error).toContain('table subagent') expect(mockFindUpload).not.toHaveBeenCalled() }) - it('rejects the knowledge_base operation and points to the knowledge subagent', async () => { + it('rejects the manage_knowledge_base operation and points to the knowledge subagent', async () => { const result = await executeMaterializeFile( - { fileNames: ['data.csv'], operation: 'knowledge_base' }, + { fileNames: ['data.csv'], operation: 'manage_knowledge_base' }, context ) expect(result.success).toBe(false) - expect(result.error).toContain('Unsupported materialize_file operation "knowledge_base"') + expect(result.error).toContain('Unsupported save_upload operation "manage_knowledge_base"') expect(result.error).toContain('knowledge subagent') expect(mockFindUpload).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts index d6b5e2dece..26f87478e5 100644 --- a/apps/sim/lib/copilot/tools/handlers/materialize-file.ts +++ b/apps/sim/lib/copilot/tools/handlers/materialize-file.ts @@ -45,7 +45,7 @@ import { admitCreateWorkspaceFile } from '@/lib/workspace-files/application/crea import { readWorkspaceFileMetadata } from '@/lib/workspace-files/application/read-workspace-file-metadata' import { extractWorkflowMetadata } from '@/app/api/v1/admin/types' -const logger = createLogger('MaterializeFile') +const logger = createLogger('SaveUpload') const MAX_MATERIALIZE_NAME_RETRIES = 8 const WORKSPACE_FILE_NAME_UNIQUE_INDEX = 'workspace_files_workspace_folder_name_active_unique' @@ -102,7 +102,7 @@ async function executeSave( if (isArchiveFileName(displayName)) { return { success: false, - error: `"${fileName}" is a .zip archive — save it by extracting instead: materialize_file(fileNames: ["${fileName}"], operation: "extract") unpacks it into files/ where the contents stay readable. The raw .zip remains in uploads/ for this chat.`, + error: `"${fileName}" is a .zip archive — save it by extracting instead: save_upload(fileNames: ["${fileName}"], operation: "extract") unpacks it into files/ where the contents stay readable. The raw .zip remains in uploads/ for this chat.`, } } @@ -256,7 +256,7 @@ async function executeImport( if (isArchiveFileName(row.displayName ?? row.originalName)) { return { success: false, - error: `"${fileName}" is a .zip archive, not a workflow JSON. Extract it first: materialize_file(fileNames: ["${fileName}"], operation: "extract").`, + error: `"${fileName}" is a .zip archive, not a workflow JSON. Extract it first: save_upload(fileNames: ["${fileName}"], operation: "extract").`, } } @@ -553,11 +553,11 @@ export async function executeMaterializeFile( } if (!context.chatId) { - return { success: false, error: 'No chat context available for materialize_file' } + return { success: false, error: 'No chat context available for save_upload' } } if (!context.workspaceId) { - return { success: false, error: 'No workspace context available for materialize_file' } + return { success: false, error: 'No workspace context available for save_upload' } } const principal = resolveCopilotFilePrincipal(context) @@ -569,7 +569,7 @@ export async function executeMaterializeFile( if (operation !== 'save' && operation !== 'import' && operation !== 'extract') { return { success: false, - error: `Unsupported materialize_file operation "${operation}". Use "save", "import", or "extract". For CSV/TSV/JSON → use the table subagent; for documents → use the knowledge subagent.`, + error: `Unsupported save_upload operation "${operation}". Use "save", "import", or "extract". For CSV/TSV/JSON → use the table subagent; for documents → use the knowledge subagent.`, } } @@ -615,7 +615,7 @@ export async function executeMaterializeFile( failed.push({ fileName, error: result.error ?? 'Failed to materialize file' }) } } catch (err) { - logger.error('materialize_file failed', { + logger.error('save_upload failed', { fileName, operation, chatId: context.chatId, diff --git a/apps/sim/lib/copilot/tools/handlers/param-types.ts b/apps/sim/lib/copilot/tools/handlers/param-types.ts index a7ece42315..afe24b1b1e 100644 --- a/apps/sim/lib/copilot/tools/handlers/param-types.ts +++ b/apps/sim/lib/copilot/tools/handlers/param-types.ts @@ -302,6 +302,8 @@ export interface OpenResourceItem { type?: OpenResourceType id?: string path?: string + /** Saved-view id or exact name to open a table pinned to (table type only). */ + view?: string } export interface OpenResourceParams { @@ -315,4 +317,5 @@ export interface ValidOpenResourceParams { type: OpenResourceType id?: string path?: string + view?: string } diff --git a/apps/sim/lib/copilot/tools/handlers/platform-actions.ts b/apps/sim/lib/copilot/tools/handlers/platform-actions.ts index c3c3ac1438..c7f0297452 100644 --- a/apps/sim/lib/copilot/tools/handlers/platform-actions.ts +++ b/apps/sim/lib/copilot/tools/handlers/platform-actions.ts @@ -1,5 +1,5 @@ /** - * Static content for the get_platform_actions tool. + * Static content for the get_ui_reference tool. * Contains the Sim platform quick reference and keyboard shortcuts. */ export const PLATFORM_ACTIONS_CONTENT = `# Sim Platform Quick Reference & Keyboard Shortcuts diff --git a/apps/sim/lib/copilot/tools/handlers/resources.test.ts b/apps/sim/lib/copilot/tools/handlers/resources.test.ts index 4807406e83..64fa0d7610 100644 --- a/apps/sim/lib/copilot/tools/handlers/resources.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/resources.test.ts @@ -39,6 +39,10 @@ vi.mock('@/lib/table/service', () => ({ getTableById: vi.fn(), })) +vi.mock('@/lib/table/views/service', () => ({ + getTableView: vi.fn(), +})) + vi.mock('@/lib/knowledge/application/knowledge-bases', () => ({ readKnowledgeBase: { operation: { id: 'knowledge.read' }, @@ -86,6 +90,8 @@ vi.mock('@/lib/logs/service', () => ({ getLogById: vi.fn(), })) +import { getTableById } from '@/lib/table/service' +import { getTableView } from '@/lib/table/views/service' import { executeOpenResource } from './resources' describe('executeOpenResource', () => { @@ -229,3 +235,52 @@ describe('executeOpenResource', () => { ).rejects.toThrow('knowledge database unavailable') }) }) + +describe('open_resource table views', () => { + const executionContext = { userId: 'user-1', workspaceId: 'ws-1' } as never + + it('opens a table pinned to a saved view by id, stamping viewId and a pinned title', async () => { + vi.mocked(getTableById).mockResolvedValue({ + id: 'tbl-1', + name: 'Leads', + workspaceId: 'ws-1', + schema: { columns: [{ id: 'col_a', name: 'status', type: 'string' }] }, + } as never) + vi.mocked(getTableView).mockResolvedValue({ + id: 'view-1', + name: 'Overdue', + isDefault: false, + config: {}, + } as never) + + const result = await executeOpenResource( + { resources: [{ type: 'table', id: 'tbl-1', view: 'view-1' }] }, + executionContext + ) + + expect(result.success).toBe(true) + expect(result.resources?.[0]).toMatchObject({ + type: 'table', + id: 'tbl-1', + title: 'Leads — Overdue', + viewId: 'view-1', + }) + }) + + it('rejects an unknown view id and points at views.json', async () => { + vi.mocked(getTableById).mockResolvedValue({ + id: 'tbl-1', + name: 'Leads', + workspaceId: 'ws-1', + schema: { columns: [] }, + } as never) + vi.mocked(getTableView).mockResolvedValue(null as never) + + const missing = await executeOpenResource( + { resources: [{ type: 'table', id: 'tbl-1', view: 'view-nope' }] }, + executionContext + ) + expect(missing.success).toBe(false) + expect((missing.output as { errors: string[] }).errors[0]).toContain('views.json') + }) +}) diff --git a/apps/sim/lib/copilot/tools/handlers/resources.ts b/apps/sim/lib/copilot/tools/handlers/resources.ts index 743c67b778..0bbb90efe0 100644 --- a/apps/sim/lib/copilot/tools/handlers/resources.ts +++ b/apps/sim/lib/copilot/tools/handlers/resources.ts @@ -6,7 +6,9 @@ import { canonicalWorkspaceFilePath } from '@/lib/copilot/vfs/path-utils' import { asOrchestrationError } from '@/lib/core/orchestration/types' import { readKnowledgeBase } from '@/lib/knowledge/application/knowledge-bases' import { getLogById } from '@/lib/logs/service' +import type { TableSchema } from '@/lib/table' import { getTableById } from '@/lib/table/service' +import { getTableView } from '@/lib/table/views/service' import { findWorkspaceFileRecord, type WorkspaceFileRecord, @@ -76,6 +78,21 @@ async function resolveResource( return { error: `Table not found in the current workspace.` } resourceId = tbl.id title = tbl.name + if (item.view) { + const view = await getTableView( + item.view.trim(), + tbl.id, + (tbl.schema as TableSchema).columns, + context.workspaceId ?? undefined + ) + if (!view) { + return { + error: `No view with id "${item.view.trim()}" on table "${tbl.name}". View ids are listed in the table's views.json.`, + } + } + title = `${tbl.name} — ${view.name}` + return { type: resourceType, id: resourceId, title, viewId: view.id } + } } if (resourceType === 'knowledgebase') { if (!item.id) return { error: 'knowledgebase resources require `id`.' } @@ -174,5 +191,8 @@ function validateOpenResourceItem( if (!item.id && !(item.type === 'file' && item.path)) { return { success: false, error: `${item.type} resources require \`id\`` } } - return { success: true, params: { type: item.type, id: item.id, path: item.path } } + return { + success: true, + params: { type: item.type, id: item.id, path: item.path, view: item.view }, + } } diff --git a/apps/sim/lib/copilot/tools/handlers/run-code.ts b/apps/sim/lib/copilot/tools/handlers/run-code.ts index e27babc451..68345ea752 100644 --- a/apps/sim/lib/copilot/tools/handlers/run-code.ts +++ b/apps/sim/lib/copilot/tools/handlers/run-code.ts @@ -2,7 +2,7 @@ import type { ToolExecutionContext, ToolExecutionResult } from '@/lib/copilot/to import { executeFunctionExecute } from '@/lib/copilot/tools/handlers/function-execute' /** - * Compute-only variant of function_execute for info-gathering agents: same + * Compute-only variant of run_function for info-gathering agents: same * sandbox and inputs, but it must never create or overwrite workspace * resources. The write vectors (outputs.files, outputTable) are rejected here * on top of the Go executor's fail-fast guard; run_code is also absent from diff --git a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts index 3e511d1be0..633a4bacdd 100644 --- a/apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/upload-file-reader.test.ts @@ -184,7 +184,7 @@ describe('readChatUpload', () => { const result = await readChatUpload('bundle.zip', CHAT_ID) - expect(result?.content).toContain('materialize_file') + expect(result?.content).toContain('save_upload') expect(result?.content).toContain('extract') expect(mockReadFileRecord).not.toHaveBeenCalled() }) @@ -200,7 +200,7 @@ describe('readChatUpload', () => { const result = await readChatUpload('huge.zip', CHAT_ID) - expect(result?.content).toContain('materialize_file') + expect(result?.content).toContain('save_upload') expect(mockFetchBuffer).not.toHaveBeenCalled() expect(mockReadFileRecord).not.toHaveBeenCalled() }) @@ -243,7 +243,7 @@ describe('grepChatUpload', () => { const error = await grepChatUpload('bundle.zip', CHAT_ID, 'foo').catch((e) => e) expect(error).toBeInstanceOf(WorkspaceFileGrepError) - expect(error.message).toContain('materialize_file') + expect(error.message).toContain('save_upload') expect(error.message).toContain('extract') expect(mockReadFileRecord).not.toHaveBeenCalled() }) diff --git a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts index 3245a6b897..2d2d6880db 100644 --- a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts +++ b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.test.ts @@ -321,13 +321,13 @@ describe('vfs mv/cp', () => { expect(result.error).toContain('across categories') }) - it('rejects uploads with a materialize_file pointer', async () => { + it('rejects uploads with a save_upload pointer', async () => { const result = await executeVfsMv( { sources: ['uploads/data.csv'], destination: 'files/data.csv' }, context ) expect(result.success).toBe(false) - expect(result.error).toContain('materialize_file') + expect(result.error).toContain('save_upload') }) it('rejects read-only categories', async () => { diff --git a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts index 8fa50ce6d6..946cabe015 100644 --- a/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts +++ b/apps/sim/lib/copilot/tools/handlers/vfs-mutate.ts @@ -54,7 +54,7 @@ const MUTATE_CATEGORIES = new Set(['files', 'workflows', 'tables', 'know const CATEGORY_REJECTIONS: Record = { uploads: - 'uploads/ files are chat-scoped and immutable. Use materialize_file to promote one into files/ first.', + 'uploads/ files are chat-scoped and immutable. Use save_upload to promote one into files/ first.', 'recently-deleted': 'recently-deleted/ items cannot be moved or copied. Restore them with restore_resource first.', } diff --git a/apps/sim/lib/copilot/tools/permissions.test.ts b/apps/sim/lib/copilot/tools/permissions.test.ts index a15ee1f791..3edfc033df 100644 --- a/apps/sim/lib/copilot/tools/permissions.test.ts +++ b/apps/sim/lib/copilot/tools/permissions.test.ts @@ -34,8 +34,8 @@ describe('copilotWriteDeniedMessage', () => { }) it('omits the operation label when there is no operation', () => { - expect(copilotWriteDeniedMessage('knowledge_base', undefined, 'read')).toBe( - "Permission denied: knowledge_base requires write access. You have 'read' permission." + expect(copilotWriteDeniedMessage('manage_knowledge_base', undefined, 'read')).toBe( + "Permission denied: manage_knowledge_base requires write access. You have 'read' permission." ) }) }) diff --git a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts index 0f574e5611..f65a8b2ab7 100644 --- a/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts +++ b/apps/sim/lib/copilot/tools/registry/server-tool-adapter.test.ts @@ -24,7 +24,7 @@ describe('server tool adapter authority boundary', () => { }) it('overwrites model-supplied workspace scope and forwards trusted delegation context', async () => { - const handler = createServerToolHandler('workspace_file') + const handler = createServerToolHandler('prepare_file_edit') await handler( { workspaceId: 'attacker-workspace', operation: 'rename' }, @@ -39,7 +39,7 @@ describe('server tool adapter authority boundary', () => { ) expect(mocks.routeExecution).toHaveBeenCalledWith( - 'workspace_file', + 'prepare_file_edit', expect.objectContaining({ workspaceId: 'workspace-1', operation: 'rename' }), expect.objectContaining({ userId: 'user-1', @@ -55,7 +55,7 @@ describe('server tool adapter authority boundary', () => { const storageError = new Error('update workspace_files set secret_column = value') mocks.routeExecution.mockRejectedValue(storageError) - const result = await createServerToolHandler('workspace_file')( + const result = await createServerToolHandler('prepare_file_edit')( {}, { userId: 'user-1', @@ -68,13 +68,13 @@ describe('server tool adapter authority boundary', () => { expect(result).toEqual({ success: false, - error: `[workspace_file] ${TOOL_RESULT_UNAVAILABLE_ERROR}`, + error: `[prepare_file_edit] ${TOOL_RESULT_UNAVAILABLE_ERROR}`, }) expect(result.error).not.toContain('workspace_files') expect(mocks.loggerError).toHaveBeenCalledWith( 'Server tool execution failed', { - toolId: 'workspace_file', + toolId: 'prepare_file_edit', abortSignalAborted: false, }, storageError diff --git a/apps/sim/lib/copilot/tools/server/base-tool.ts b/apps/sim/lib/copilot/tools/server/base-tool.ts index 2af97ba749..b4081e340a 100644 --- a/apps/sim/lib/copilot/tools/server/base-tool.ts +++ b/apps/sim/lib/copilot/tools/server/base-tool.ts @@ -16,7 +16,7 @@ export interface ServerToolContext { messageId?: string /** * The invoking subagent's channel id (its outer tool_use id). Used to scope - * the workspace_file -> edit_content intent handoff to a single file subagent + * the prepare_file_edit -> apply_file_edit intent handoff to a single file subagent * so two file agents writing concurrently never consume each other's pending * intent. Undefined for main-agent tool calls (which never overlap). */ diff --git a/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts b/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts index 14693f7591..f024b421de 100644 --- a/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts +++ b/apps/sim/lib/copilot/tools/server/docs/search-documentation.test.ts @@ -9,7 +9,7 @@ const { mockGenerateSearchEmbedding } = vi.hoisted(() => ({ })) vi.mock('@/lib/copilot/generated/tool-catalog-v1', () => ({ - SearchDocumentation: { id: 'search_documentation' }, + SearchSimDocs: { id: 'search_sim_docs' }, })) vi.mock('@/lib/knowledge/embeddings', () => ({ generateSearchEmbedding: mockGenerateSearchEmbedding, diff --git a/apps/sim/lib/copilot/tools/server/docs/search-documentation.ts b/apps/sim/lib/copilot/tools/server/docs/search-documentation.ts index ad14c3937a..1a61dcbbfc 100644 --- a/apps/sim/lib/copilot/tools/server/docs/search-documentation.ts +++ b/apps/sim/lib/copilot/tools/server/docs/search-documentation.ts @@ -2,7 +2,7 @@ import { db } from '@sim/db' import { docsEmbeddings } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { sql } from 'drizzle-orm' -import { SearchDocumentation } from '@/lib/copilot/generated/tool-catalog-v1' +import { SearchSimDocs } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { generateSearchEmbedding } from '@/lib/knowledge/embeddings' @@ -15,7 +15,7 @@ interface DocsSearchParams { const DEFAULT_DOCS_SIMILARITY_THRESHOLD = 0.3 export const searchDocumentationServerTool: BaseServerTool = { - name: SearchDocumentation.id, + name: SearchSimDocs.id, async execute(params: DocsSearchParams): Promise { const logger = createLogger('SearchDocumentationServerTool') const { query, topK = 10, threshold } = params diff --git a/apps/sim/lib/copilot/tools/server/enrichment/enrichment-run.ts b/apps/sim/lib/copilot/tools/server/enrichment/enrichment-run.ts index bde0e4873b..8b7a2a3d7e 100644 --- a/apps/sim/lib/copilot/tools/server/enrichment/enrichment-run.ts +++ b/apps/sim/lib/copilot/tools/server/enrichment/enrichment-run.ts @@ -1,5 +1,5 @@ import { createLogger } from '@sim/logger' -import { EnrichmentRun } from '@/lib/copilot/generated/tool-catalog-v1' +import { RunEnrichment } from '@/lib/copilot/generated/tool-catalog-v1' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { getEnrichment } from '@/enrichments/registry' import { runEnrichment } from '@/enrichments/run' @@ -25,7 +25,7 @@ interface EnrichmentRunResult { * bill (see image/generate-image.ts). */ export const enrichmentRunServerTool: BaseServerTool = { - name: EnrichmentRun.id, + name: RunEnrichment.id, async execute(params: EnrichmentRunParams, context): Promise { const logger = createLogger('EnrichmentRunServerTool') const { enrichmentId, inputs } = params diff --git a/apps/sim/lib/copilot/tools/server/files/create-file.ts b/apps/sim/lib/copilot/tools/server/files/create-file.ts index 430545f8b0..8f36784b1a 100644 --- a/apps/sim/lib/copilot/tools/server/files/create-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/create-file.ts @@ -13,7 +13,7 @@ import { } from '@/lib/workspace-files/application/write-workspace-file-by-path' const logger = createLogger('CreateFileServerTool') -const CREATE_FILE_TOOL_ID = 'create_file' +const CREATE_FILE_TOOL_ID = 'create_empty_file' interface CreateFileArgs { fileName: string @@ -48,7 +48,10 @@ export const createFileServerTool: BaseServerTool files). pptx also gets `iconImage` // (react-icons → sharp → PNG), which only works here because the E2B sandbox is -// a full Linux VM. The agent's edit_content source runs inside an async IIFE so +// a full Linux VM. The agent's apply_file_edit source runs inside an async IIFE so // top-level await (addImage/iconImage) works; the finalizer writes the binary. const PPTX_NODE_PREAMBLE = ` const PptxGenJS = require('pptxgenjs'); diff --git a/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts index ff1f7b10f2..dba8bb892b 100644 --- a/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/download-to-workspace-file.ts @@ -3,7 +3,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { z } from 'zod' import { executeCopilotFileUseCase } from '@/lib/copilot/application/execute-file-use-case' import { messageForCopilotFileError } from '@/lib/copilot/auth/file-delegation' -import { DownloadToWorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' +import { DownloadFile } from '@/lib/copilot/generated/tool-catalog-v1' import { assertServerToolNotAborted, type BaseServerTool, @@ -137,7 +137,7 @@ export const downloadToWorkspaceFileServerTool: BaseServerTool< DownloadToWorkspaceFileArgs, DownloadToWorkspaceFileResult > = { - name: DownloadToWorkspaceFile.id, + name: DownloadFile.id, inputSchema: DownloadToWorkspaceFileArgsSchema, outputSchema: DownloadToWorkspaceFileResultSchema, diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index 0f453ebd67..99665a3375 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -30,10 +30,10 @@ type EditContentResult = { } export const editContentServerTool: BaseServerTool = { - name: 'edit_content', + name: 'apply_file_edit', async execute(params: EditContentArgs, context?: ServerToolContext): Promise { if (!context?.userId) { - logger.error('Unauthorized attempt to use edit_content') + logger.error('Unauthorized attempt to use apply_file_edit') throw new Error('Authentication required') } @@ -52,12 +52,12 @@ export const editContentServerTool: BaseServerTool { }) ) - // edit_content from channel F1 must get fileA — NOT the latest (fileB). + // apply_file_edit from channel F1 must get fileA — NOT the latest (fileB). const a = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F1' }) expect(a?.fileId).toBe('fileA') - // edit_content from channel F2 gets fileB. + // apply_file_edit from channel F2 gets fileB. const b = await consumeLatestFileIntent(ws, { ...scope, channelId: 'F2' }) expect(b?.fileId).toBe('fileB') }) diff --git a/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts b/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts index 82f7977b17..e9841ce42d 100644 --- a/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts +++ b/apps/sim/lib/copilot/tools/server/files/file-intent-store.ts @@ -12,7 +12,7 @@ export type PendingFileIntent = { chatId?: string messageId?: string // The invoking file subagent's channel id (its outer tool_use id). Lets - // edit_content consume the intent for ITS OWN file subagent instead of the + // apply_file_edit consume the intent for ITS OWN file subagent instead of the // latest in the message, so two file agents writing concurrently never cross // their content into each other's file. channelId?: string diff --git a/apps/sim/lib/copilot/tools/server/files/file-preview.ts b/apps/sim/lib/copilot/tools/server/files/file-preview.ts index d99f219021..f706231d5e 100644 --- a/apps/sim/lib/copilot/tools/server/files/file-preview.ts +++ b/apps/sim/lib/copilot/tools/server/files/file-preview.ts @@ -135,9 +135,9 @@ function buildAppendPreview(existingContent: string, incomingContent: string): s /** * Reads the current UTF-8 text of a workspace file for streaming previews. * - * Preview runs in the SSE loop on `workspace_file` **call** events, which are + * Preview runs in the SSE loop on `prepare_file_edit` **call** events, which are * processed **before** the async tool executor persists {@link storeFileIntent}. - * Loading the base here avoids a race where `edit_content` `args_delta` arrives + * Loading the base here avoids a race where `apply_file_edit` `args_delta` arrives * before Redis holds `existingContent`, which would make append previews look like * full-file replacement until the intent landed. */ @@ -193,7 +193,7 @@ export function buildFilePreviewText({ // Fail closed (like `patch`/`update` below) when the base file content has not loaded yet: a base-less // `append` preview is just the streamed fragment, and a collaborative editor applying it as the full // body would reconcile the seeded doc down to that fragment (a wipe). Skipping the preview until the - // base is available costs only a brief render delay; the final durable `edit_content` write is + // base is available costs only a brief render delay; the final durable `apply_file_edit` write is // authoritative. An empty file has `existingContent === ''` (defined), so it is unaffected. if (existingContent === undefined) { return undefined diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index f40fbe0065..6ac90e138d 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -10,7 +10,7 @@ import { messageForCopilotFileError, resolveCopilotFilePrincipal, } from '@/lib/copilot/auth/file-delegation' -import { WorkspaceFile } from '@/lib/copilot/generated/tool-catalog-v1' +import { PrepareFileEdit } from '@/lib/copilot/generated/tool-catalog-v1' import { assertServerToolNotAborted, type BaseServerTool, @@ -191,7 +191,7 @@ export type CompileForWriteResult = | { ok: false; message: string } /** - * Shared write-time doc handling for create + edit_content: validates and builds + * Shared write-time doc handling for create + apply_file_edit: validates and builds * the document (E2B doc sandbox when enabled — Node pptx/docx, Python pdf/xlsx — * else isolated-vm JS) and returns the source MIME to store, or a user-facing * failure message. Non-doc files resolve to `fallbackMime`. The remote backend publishes a @@ -260,7 +260,7 @@ export async function compileDocForWrite(args: { } export const workspaceFileServerTool: BaseServerTool = { - name: WorkspaceFile.id, + name: PrepareFileEdit.id, async execute( params: WorkspaceFileArgs, context?: ServerToolContext @@ -478,7 +478,7 @@ export const workspaceFileServerTool: BaseServerTool ({ - KnowledgeBase: { id: 'knowledge_base' }, + ManageKnowledgeBase: { id: 'manage_knowledge_base' }, })) vi.mock('@/lib/core/telemetry', () => ({ PlatformEvents: { @@ -224,7 +224,7 @@ function expectDelegatedPrincipal(call: unknown): void { }) } -describe('knowledge_base trusted application delegation', () => { +describe('manage_knowledge_base trusted application delegation', () => { beforeEach(() => { vi.clearAllMocks() mockReadKnowledgeBase.mockResolvedValue({ knowledgeBase: KNOWLEDGE_BASE, folderPath: '/' }) @@ -754,7 +754,7 @@ describe('knowledge_base trusted application delegation', () => { ) }) -describe('knowledge_base add_file delegation', () => { +describe('manage_knowledge_base add_file delegation', () => { beforeEach(() => { vi.clearAllMocks() mockAddWorkspaceFiles.mockResolvedValue({ diff --git a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts index 1f5aa48d4c..cd9f300a2a 100644 --- a/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts +++ b/apps/sim/lib/copilot/tools/server/knowledge/knowledge-base.ts @@ -10,7 +10,7 @@ import { messageForCopilotKnowledgeError, requireCopilotKnowledgeWorkspaceId, } from '@/lib/copilot/application/execute-knowledge-use-case' -import { KnowledgeBase } from '@/lib/copilot/generated/tool-catalog-v1' +import { ManageKnowledgeBase } from '@/lib/copilot/generated/tool-catalog-v1' import { projectToolErrorMessageForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result' import { assertServerToolNotAborted, @@ -252,7 +252,7 @@ function isKnowledgeDocumentTagValueAssignment( * Knowledge base tool for copilot to create, list, and get knowledge bases */ export const knowledgeBaseServerTool: BaseServerTool = { - name: KnowledgeBase.id, + name: ManageKnowledgeBase.id, async execute( params: KnowledgeBaseArgs, context?: ServerToolContext @@ -1070,7 +1070,7 @@ export const knowledgeBaseServerTool: BaseServerTool ({ })) vi.mock('@/lib/copilot/generated/tool-catalog-v1', () => ({ - SearchOnline: { id: 'search_online' }, + WebSearch: { id: 'web_search' }, })) vi.mock('@/tools', () => ({ executeTool: mockExecuteTool })) diff --git a/apps/sim/lib/copilot/tools/server/other/search-online.ts b/apps/sim/lib/copilot/tools/server/other/search-online.ts index 330d930c64..272c80d103 100644 --- a/apps/sim/lib/copilot/tools/server/other/search-online.ts +++ b/apps/sim/lib/copilot/tools/server/other/search-online.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' -import { SearchOnline } from '@/lib/copilot/generated/tool-catalog-v1' +import { WebSearch } from '@/lib/copilot/generated/tool-catalog-v1' import { projectToolErrorMessageForCopilot } from '@/lib/copilot/request/tools/resolved-secret-result' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import { env } from '@/lib/core/config/env' @@ -31,7 +31,7 @@ interface SearchResponse { } export const searchOnlineServerTool: BaseServerTool = { - name: SearchOnline.id, + name: WebSearch.id, async execute(params: OnlineSearchParams, context?: ServerToolContext): Promise { const logger = createLogger('SearchOnlineServerTool') const { query, num = 10, type = 'search', gl, hl } = params diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index 7d87b43221..66848fa468 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -2,19 +2,19 @@ import { createLogger } from '@sim/logger' import { z } from 'zod' import { getBlockVisibilityForCopilot } from '@/lib/copilot/block-visibility' import { - CreateFile, - DownloadToWorkspaceFile, + CreateEmptyFile, + DownloadFile, Ffmpeg, GenerateAudio, GenerateImage, GenerateVideo, - KnowledgeBase, ManageCredential, ManageCustomTool, - ManageMcpTool, + ManageKnowledgeBase, + ManageMcpConnection, ManageSkill, + PrepareFileEdit, UserTable, - WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' import { copilotToolCanWrite } from '@/lib/copilot/tools/permissions' import { @@ -53,6 +53,7 @@ import { tableColumnsServerTool } from '@/lib/copilot/tools/server/table/table-c import { tableEnrichmentsServerTool } from '@/lib/copilot/tools/server/table/table-enrichments' import { tableManageServerTool } from '@/lib/copilot/tools/server/table/table-manage' import { tableRowsServerTool } from '@/lib/copilot/tools/server/table/table-rows' +import { tableViewsServerTool } from '@/lib/copilot/tools/server/table/table-views' import { userTableServerTool } from '@/lib/copilot/tools/server/table/user-table' import { getCredentialsServerTool } from '@/lib/copilot/tools/server/user/get-credentials' import { setEnvironmentVariablesServerTool } from '@/lib/copilot/tools/server/user/set-environment-variables' @@ -90,7 +91,7 @@ const VISIBILITY_GATED_TOOLS = new Set([ ]) const WRITE_ACTIONS: Record = { - [KnowledgeBase.id]: [ + [ManageKnowledgeBase.id]: [ 'create', 'add_file', 'update', @@ -133,19 +134,19 @@ const WRITE_ACTIONS: Record = { 'add_enrichment', ], [ManageCustomTool.id]: ['add', 'edit', 'delete'], - [ManageMcpTool.id]: ['add', 'edit', 'delete'], + [ManageMcpConnection.id]: ['add', 'edit', 'delete'], [ManageSkill.id]: ['add', 'edit', 'delete'], [ManageCredential.id]: ['rename', 'delete'], - [WorkspaceFile.id]: ['create', 'append', 'update', 'delete', 'rename', 'patch'], + [PrepareFileEdit.id]: ['create', 'append', 'update', 'delete', 'rename', 'patch'], [editContentServerTool.name]: ['*'], - [CreateFile.id]: ['*'], + [CreateEmptyFile.id]: ['*'], rename_file: ['*'], [shareFileServerTool.name]: ['*'], move_file: ['*'], create_file_folder: ['*'], rename_file_folder: ['*'], move_file_folder: ['*'], - [DownloadToWorkspaceFile.id]: ['*'], + [DownloadFile.id]: ['*'], [GenerateImage.id]: ['generate'], [GenerateVideo.id]: ['generate'], [GenerateAudio.id]: ['generate'], @@ -182,6 +183,7 @@ const baseServerToolRegistry: Record = { [tableColumnsServerTool.name]: tableColumnsServerTool, [tableAutomationsServerTool.name]: tableAutomationsServerTool, [tableEnrichmentsServerTool.name]: tableEnrichmentsServerTool, + [tableViewsServerTool.name]: tableViewsServerTool, [workspaceFileServerTool.name]: workspaceFileServerTool, [editContentServerTool.name]: editContentServerTool, [createFileServerTool.name]: createFileServerTool, diff --git a/apps/sim/lib/copilot/tools/server/table/table-views.test.ts b/apps/sim/lib/copilot/tools/server/table/table-views.test.ts new file mode 100644 index 0000000000..56d4df7e79 --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/table/table-views.test.ts @@ -0,0 +1,123 @@ +/** + * @vitest-environment node + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const useCases = vi.hoisted(() => ({ + list: vi.fn(), + read: vi.fn(), + create: vi.fn(), + update: vi.fn(), + del: vi.fn(), +})) + +vi.mock('@/lib/table/application/views', () => ({ + listTableViewsUseCase: { operation: { id: 'tables.views.list' }, execute: useCases.list }, + readTableViewUseCase: { operation: { id: 'tables.views.read' }, execute: useCases.read }, + createTableViewUseCase: { operation: { id: 'tables.views.create' }, execute: useCases.create }, + updateTableViewUseCase: { operation: { id: 'tables.views.update' }, execute: useCases.update }, + deleteTableViewUseCase: { operation: { id: 'tables.views.delete' }, execute: useCases.del }, +})) + +const executeUseCase = vi.hoisted(() => vi.fn()) +vi.mock('@/lib/copilot/application/execute-table-use-case', () => ({ + executeCopilotTableUseCase: executeUseCase, +})) + +import { tableViewsServerTool } from '@/lib/copilot/tools/server/table/table-views' + +const context = { userId: 'user-1', workspaceId: 'ws-1', copilotToolExecution: true } as never + +const columns = [ + { id: 'col_a', name: 'status', type: 'string' }, + { id: 'col_b', name: 'due', type: 'date' }, +] +const table = { id: 'tbl-1', schema: { columns } } + +describe('table_views adapter', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('translates stored id-domain configs to column names on list', async () => { + executeUseCase.mockResolvedValueOnce({ + table, + views: [ + { + id: 'view-1', + name: 'Overdue', + isDefault: true, + config: { + filter: { all: [{ field: 'col_a', op: 'ne', value: 'Done' }] }, + sort: [{ field: 'col_b', direction: 'asc' }], + }, + }, + ], + }) + + const result = await tableViewsServerTool.execute( + { operation: 'list_views', args: { tableId: 'tbl-1' } }, + context + ) + + expect(result.success).toBe(true) + expect(result.data.views[0].filter).toEqual({ + all: [{ field: 'status', op: 'ne', value: 'Done' }], + }) + expect(result.data.views[0].sort).toEqual([{ field: 'due', direction: 'asc' }]) + }) + + it('translates agent column names to stable ids on create', async () => { + executeUseCase.mockResolvedValueOnce({ table, views: [] }).mockResolvedValueOnce({ + view: { id: 'view-2', name: 'Mine', isDefault: false, config: {} }, + table, + }) + + const result = await tableViewsServerTool.execute( + { + operation: 'create_view', + args: { + tableId: 'tbl-1', + name: 'Mine', + filter: { all: [{ field: 'status', op: 'eq', value: 'Open' }] }, + }, + }, + context + ) + + expect(result.success).toBe(true) + const createInput = executeUseCase.mock.calls[1][2] + expect(createInput.config.filter).toEqual({ + all: [{ field: 'col_a', op: 'eq', value: 'Open' }], + }) + }) + + it('rejects unknown column names with the columns spelled out', async () => { + executeUseCase.mockResolvedValueOnce({ table, views: [] }) + + await expect( + tableViewsServerTool.execute( + { + operation: 'create_view', + args: { + tableId: 'tbl-1', + name: 'Broken', + filter: { all: [{ field: 'nope', op: 'eq', value: 1 }] }, + }, + }, + context + ) + ).rejects.toThrow(/Unknown column/) + }) + + it('rejects unsupported operations without invoking anything', async () => { + const result = await tableViewsServerTool.execute( + { operation: 'insert_row', args: { tableId: 'tbl-1' } }, + context + ) + expect(result.success).toBe(false) + expect(result.message).toContain('insert_row') + expect(executeUseCase).not.toHaveBeenCalled() + }) +}) diff --git a/apps/sim/lib/copilot/tools/server/table/table-views.ts b/apps/sim/lib/copilot/tools/server/table/table-views.ts new file mode 100644 index 0000000000..fe7cdaf27d --- /dev/null +++ b/apps/sim/lib/copilot/tools/server/table/table-views.ts @@ -0,0 +1,197 @@ +import { executeCopilotTableUseCase } from '@/lib/copilot/application/execute-table-use-case' +import { TableViews } from '@/lib/copilot/generated/tool-catalog-v1' +import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' +import type { SortSpec, TablePredicateInput, TableSchema, TableViewConfig } from '@/lib/table' +import { + createTableViewUseCase, + deleteTableViewUseCase, + listTableViewsUseCase, + readTableViewUseCase, + updateTableViewUseCase, +} from '@/lib/table/application/views' +import { viewConfigIdsToNames, viewConfigNamesToIds } from '@/lib/table/views/service' + +type TableViewsArgs = { + operation: string + args?: Record +} + +type TableViewsResult = { + success: boolean + message: string + data?: any +} + +/** + * Saved-view slice of the split table surface. Unlike the other slices this is + * NOT a user_table passthrough — it adapts the dedicated view use cases. + * Agents speak column NAMES; stored configs are keyed by stable column id, so + * inputs translate names→ids on the way in and every returned view translates + * ids→names on the way out. + */ +export const tableViewsServerTool: BaseServerTool = { + name: TableViews.id, + async execute(params: TableViewsArgs, context?: ServerToolContext) { + const operation = params?.operation + const args = params?.args ?? {} + const tableId = args.tableId as string | undefined + const workspaceId = context?.workspaceId + if (!tableId) return { success: false, message: 'Table ID is required' } + if (!workspaceId) return { success: false, message: 'Workspace ID is required' } + + const presentView = ( + view: { id: string; name: string; isDefault: boolean; config: TableViewConfig }, + columns: TableSchema['columns'] + ) => { + const named = viewConfigIdsToNames(view.config, columns) + return { + id: view.id, + name: view.name, + isDefault: view.isDefault, + filter: named.filter ?? null, + sort: named.sort ?? null, + hiddenColumns: named.hiddenColumns?.length ? named.hiddenColumns : undefined, + } + } + + const namedConfigFromArgs = (columns: TableSchema['columns']): TableViewConfig => + viewConfigNamesToIds( + { + filter: (args.filter as TablePredicateInput | undefined) ?? null, + sort: (args.sort as SortSpec | undefined) ?? null, + hiddenColumns: args.hiddenColumns as string[] | undefined, + } as TableViewConfig, + columns + ) + + switch (operation) { + case 'list_views': { + const result = await executeCopilotTableUseCase( + context, + listTableViewsUseCase, + { tableId, workspaceId }, + { tableId } + ) + const columns = (result.table.schema as TableSchema).columns + const views = result.views.map((view) => presentView(view, columns)) + return { + success: true, + message: `Table has ${views.length} view(s)`, + data: { views }, + } + } + case 'get_view': { + if (!args.viewId) return { success: false, message: 'viewId is required' } + const result = await executeCopilotTableUseCase( + context, + readTableViewUseCase, + { tableId, workspaceId, viewId: args.viewId }, + { tableId } + ) + const columns = (result.table.schema as TableSchema).columns + return { + success: true, + message: 'View loaded', + data: { view: presentView(result.view, columns) }, + } + } + case 'create_view': { + if (!args.name) return { success: false, message: 'name is required' } + const listed = await executeCopilotTableUseCase( + context, + listTableViewsUseCase, + { tableId, workspaceId }, + { tableId } + ) + const columns = (listed.table.schema as TableSchema).columns + const created = await executeCopilotTableUseCase( + context, + createTableViewUseCase, + { tableId, workspaceId, name: args.name, config: namedConfigFromArgs(columns) }, + { tableId } + ) + if (args.isDefault === true) { + await executeCopilotTableUseCase( + context, + updateTableViewUseCase, + { tableId, workspaceId, viewId: created.view.id, isDefault: true }, + { tableId } + ) + } + return { + success: true, + message: `Created view "${created.view.name}" (${created.view.id})${args.isDefault === true ? ' as default' : ''}`, + data: { + view: presentView({ ...created.view, isDefault: args.isDefault === true }, columns), + }, + } + } + case 'update_view': { + if (!args.viewId) return { success: false, message: 'viewId is required' } + const listed = await executeCopilotTableUseCase( + context, + listTableViewsUseCase, + { tableId, workspaceId }, + { tableId } + ) + const columns = (listed.table.schema as TableSchema).columns + const hasConfigChange = + args.filter !== undefined || args.sort !== undefined || args.hiddenColumns !== undefined + const updated = await executeCopilotTableUseCase( + context, + updateTableViewUseCase, + { + tableId, + workspaceId, + viewId: args.viewId, + name: args.name as string | undefined, + ...(hasConfigChange ? { configPatch: namedConfigFromArgs(columns) } : {}), + isDefault: args.isDefault as boolean | undefined, + }, + { tableId } + ) + return { + success: true, + message: `Updated view "${updated.view.name}"`, + data: { view: presentView(updated.view, columns) }, + } + } + case 'delete_view': { + if (!args.viewId) return { success: false, message: 'viewId is required' } + const result = await executeCopilotTableUseCase( + context, + deleteTableViewUseCase, + { tableId, workspaceId, viewId: args.viewId }, + { tableId } + ) + return { success: true, message: `Deleted view "${result.viewName}"` } + } + case 'set_default_view': { + if (!args.viewId) return { success: false, message: 'viewId is required' } + const listed = await executeCopilotTableUseCase( + context, + listTableViewsUseCase, + { tableId, workspaceId }, + { tableId } + ) + const columns = (listed.table.schema as TableSchema).columns + const updated = await executeCopilotTableUseCase( + context, + updateTableViewUseCase, + { tableId, workspaceId, viewId: args.viewId, isDefault: true }, + { tableId } + ) + return { + success: true, + message: `"${updated.view.name}" is now the default view`, + data: { view: presentView(updated.view, columns) }, + } + } + default: + return { + success: false, + message: `table_views does not support operation '${operation}' (allowed: list_views, get_view, create_view, update_view, delete_view, set_default_view)`, + } + } + }, +} diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts index 83c263815a..88f2dd9156 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.test.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.test.ts @@ -604,7 +604,7 @@ describe('userTableServerTool.import_file', () => { expect(mockBatchInsertRows).not.toHaveBeenCalled() }) - it('points a chat-upload path at materialize_file instead of globbing files/', async () => { + it('points a chat-upload path at save_upload instead of globbing files/', async () => { mockResolveWorkspaceFileReference.mockResolvedValueOnce(null) const result = await userTableServerTool.execute( @@ -616,7 +616,7 @@ describe('userTableServerTool.import_file', () => { ) expect(result.success).toBe(false) - expect(result.message).toMatch(/materialize_file/) + expect(result.message).toMatch(/save_upload/) expect(result.message).not.toMatch(/glob\("files/) }) diff --git a/apps/sim/lib/copilot/tools/server/table/user-table.ts b/apps/sim/lib/copilot/tools/server/table/user-table.ts index aa03686ccb..8ced02221e 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -48,6 +48,7 @@ import { readTableUseCase, updateTableUseCase, } from '@/lib/table/application/tables' +import { readTableViewUseCase } from '@/lib/table/application/views' import { namedRowMapper } from '@/lib/table/cell-format' import { isSupportedCurrencyCode } from '@/lib/table/currency' import { normalizeTablePredicate } from '@/lib/table/query-builder/predicate' @@ -56,11 +57,13 @@ import { normalizeSelectOptionsInput } from '@/lib/table/select-options' import type { RowData, SortSpec, + TablePredicate, TablePredicateInput, TableSchema, WorkflowGroupDependencies, WorkflowGroupDeploymentMode, } from '@/lib/table/types' +import { viewConfigIdsToNames } from '@/lib/table/views/service' import type { ResolvedSecretTraceProvenanceV1 } from '@/executor/utils/resolved-secret-trace-registry' const logger = createLogger('UserTableServerTool') @@ -145,6 +148,19 @@ async function importRowsProvenanceForModel( await registry.importCrossingProvenance(provenance, values, { trusted: true }) } +/** AND-combines a saved view's predicate with an explicit one; either may be absent. */ +function mergeViewPredicate( + viewFilter: TablePredicateInput | undefined, + explicit: TablePredicateInput | undefined +): TablePredicate | undefined { + const parts: TablePredicate[] = [] + if (viewFilter) parts.push(normalizeTablePredicate(viewFilter)) + if (explicit) parts.push(normalizeTablePredicate(explicit)) + if (parts.length === 0) return undefined + if (parts.length === 1) return parts[0] + return { all: parts } +} + export const userTableServerTool: BaseServerTool = { name: UserTable.id, async execute(params: UserTableArgs, context?: ServerToolContext): Promise { @@ -398,6 +414,31 @@ export const userTableServerTool: BaseServerTool return { success: false, message: 'Workspace ID is required' } } + // Saved-view scope: the view's stored filter ANDs with any explicit + // filter (query-within-the-view), its sort applies only when no + // explicit order is given, and layout fields (hidden columns, order, + // widths) are ignored — agents always see full rows. Views are + // referenced by id only (from views.json or table_views list_views). + let viewFilter: TablePredicateInput | undefined + let viewSort: SortSpec | undefined + let appliedViewName: string | undefined + if (typeof args.view === 'string' && args.view.trim() !== '') { + const viewId = (args.view as string).trim() + const resolved = await executeCopilotTableUseCase( + context, + readTableViewUseCase, + { tableId: args.tableId, workspaceId, viewId }, + { tableId: args.tableId } + ) + const named = viewConfigIdsToNames( + resolved.view.config, + (resolved.table.schema as TableSchema).columns + ) + viewFilter = (named.filter as TablePredicateInput | null) ?? undefined + viewSort = (named.sort as SortSpec | null) ?? undefined + appliedViewName = resolved.view.name + } + const queryLimitError = limitError(args.limit, TABLE_LIMITS.MAX_QUERY_LIMIT) if (queryLimitError) { return { success: false, message: queryLimitError } @@ -409,10 +450,11 @@ export const userTableServerTool: BaseServerTool { tableId: args.tableId, assertedWorkspaceId: workspaceId, - predicate: args.filter - ? normalizeTablePredicate(args.filter as TablePredicateInput) - : undefined, - sort: args.order as SortSpec | undefined, + predicate: mergeViewPredicate( + viewFilter, + args.filter as TablePredicateInput | undefined + ), + sort: (args.order as SortSpec | undefined) ?? viewSort, limit: args.limit ?? TABLE_LIMITS.MAX_QUERY_LIMIT, cursor: args.cursor, includeTotal: !args.cursor, @@ -431,7 +473,9 @@ export const userTableServerTool: BaseServerTool // nextCursor covers both cut kinds (explicit limit or the 5MB byte // budget) — either way the truthful signal is "more rows exist". The // token is opaque; the agent echoes it back as `cursor` to continue. - const countSuffix = result.totalCount != null ? ` of ${result.totalCount}` : '' + const viewSuffix = appliedViewName ? ` (view: ${appliedViewName})` : '' + const countSuffix = + (result.totalCount != null ? ` of ${result.totalCount}` : '') + viewSuffix const message = result.nextCursor ? `Returned ${result.rows.length}${countSuffix} rows (more available — pass cursor=${result.nextCursor} to continue)` : `Returned ${result.rows.length}${countSuffix} rows` diff --git a/apps/sim/lib/copilot/tools/tool-display.test.ts b/apps/sim/lib/copilot/tools/tool-display.test.ts index 027ce68d91..3152068872 100644 --- a/apps/sim/lib/copilot/tools/tool-display.test.ts +++ b/apps/sim/lib/copilot/tools/tool-display.test.ts @@ -4,9 +4,9 @@ import { describe, expect, it } from 'vitest' import { FfmpegOperationValues, - KnowledgeBaseOperationValues, - MaterializeFileOperationValues, + ManageKnowledgeBaseOperationValues, QueryUserTableOperationValues, + SaveUploadOperationValues, SearchKnowledgeBaseOperationValues, TOOL_CATALOG, type ToolCatalogEntry, @@ -65,22 +65,22 @@ describe('humanizeToolName', () => { it('keeps canonical acronym casing', () => { expect(humanizeToolName('create_workspace_mcp_server')).toBe('Create Workspace MCP Server') - expect(humanizeToolName('deploy_api')).toBe('Deploy API') + expect(humanizeToolName('deploy_as_api')).toBe('Deploy As API') expect(humanizeToolName('oauth_request_access')).toBe('OAuth Request Access') }) }) describe('getToolDisplayTitle natural-language coverage', () => { it('gives gerund titles to tools that previously fell through to humanize', () => { - expect(getToolDisplayTitle('deploy_api')).toBe('Deploying API') + expect(getToolDisplayTitle('deploy_as_api')).toBe('Deploying API') expect(getToolDisplayTitle('list_workspace_mcp_servers')).toBe('Listing MCP servers') expect(getToolDisplayTitle('oauth_get_auth_link')).toBe('Getting authorization link') expect(getToolDisplayTitle('diff_workflows')).toBe('Comparing workflows') }) - it('falls back to running code for function_execute without a title', () => { - expect(getToolDisplayTitle('function_execute')).toBe('Running code') - expect(getToolDisplayTitle('function_execute', { title: 'Crunching numbers' })).toBe( + it('falls back to running code for run_function without a title', () => { + expect(getToolDisplayTitle('run_function')).toBe('Running code') + expect(getToolDisplayTitle('run_function', { title: 'Crunching numbers' })).toBe( 'Crunching numbers' ) }) @@ -141,14 +141,14 @@ describe('getToolDisplayTitle natural-language coverage', () => { describe('getToolDisplayTitle for deployments', () => { it.each([ - ['deploy_api', undefined, 'Deploying API'], - ['deploy_api', { action: 'deploy' }, 'Deploying API'], - ['deploy_api', { action: 'undeploy' }, 'Undeploying API'], - ['deploy_chat', { action: 'deploy' }, 'Deploying chat'], - ['deploy_chat', { action: 'undeploy' }, 'Undeploying chat'], - ['deploy_custom_block', { action: 'deploy' }, 'Deploying custom block'], - ['deploy_custom_block', { action: 'undeploy' }, 'Undeploying custom block'], - ['deploy_mcp', undefined, 'Deploying MCP tool'], + ['deploy_as_api', undefined, 'Deploying API'], + ['deploy_as_api', { action: 'deploy' }, 'Deploying API'], + ['deploy_as_api', { action: 'undeploy' }, 'Undeploying API'], + ['deploy_as_chat', { action: 'deploy' }, 'Deploying chat'], + ['deploy_as_chat', { action: 'undeploy' }, 'Undeploying chat'], + ['publish_custom_block', { action: 'deploy' }, 'Deploying custom block'], + ['publish_custom_block', { action: 'undeploy' }, 'Undeploying custom block'], + ['deploy_as_mcp', undefined, 'Deploying MCP tool'], ['redeploy', undefined, 'Redeploying API'], ])('uses the action and deployment type for %s', (toolName, args, expected) => { expect(getToolDisplayTitle(toolName, args)).toBe(expected) @@ -216,19 +216,21 @@ describe('mvDisplayVerb', () => { describe('getToolDisplayTitle for the vfs verbs', () => { it('shows the created file name', () => { expect( - getToolDisplayTitle('create_file', { + getToolDisplayTitle('create_empty_file', { outputs: { files: [{ path: 'files/Reports/Quarterly%20Report.pdf', mode: 'create' }], }, }) ).toBe('Creating Quarterly Report.pdf') - expect(getToolDisplayTitle('create_file', { fileName: 'notes.md' })).toBe('Creating notes.md') + expect(getToolDisplayTitle('create_empty_file', { fileName: 'notes.md' })).toBe( + 'Creating notes.md' + ) expect( - getToolDisplayTitle('create_file', { + getToolDisplayTitle('create_empty_file', { outputs: { files: [{ path: 'files/notes.md', mode: 'overwrite' }] }, }) ).toBe('Overwriting notes.md') - expect(getToolDisplayTitle('create_file')).toBe('Creating file') + expect(getToolDisplayTitle('create_empty_file')).toBe('Creating file') }) it('titles rm from toolTitle, falling back to the paths', () => { @@ -305,7 +307,7 @@ describe('getToolDisplayTitle for managed resources', () => { }, 'Creating lookupWeather', ], - ['manage_mcp_tool', { operation: 'edit', config: { name: 'Linear' } }, 'Updating Linear'], + ['manage_mcp_connection', { operation: 'edit', config: { name: 'Linear' } }, 'Updating Linear'], ['manage_skill', { operation: 'delete', name: 'sales-research' }, 'Deleting sales-research'], [ 'manage_credential', @@ -318,7 +320,7 @@ describe('getToolDisplayTitle for managed resources', () => { ], ['rm', { paths: ['workflows/Marketing/Q3%20Campaigns'] }, 'Deleting Q3 Campaigns'], ['manage_custom_tool', { operation: 'list' }, 'Viewing custom tools'], - ['manage_mcp_tool', { operation: 'list' }, 'Viewing MCP servers'], + ['manage_mcp_connection', { operation: 'list' }, 'Viewing MCP servers'], ['manage_skill', { operation: 'list' }, 'Viewing skills'], ])('uses verb + resource name for %s', (toolName, args, expected) => { expect(getToolDisplayTitle(toolName, args)).toBe(expected) @@ -335,15 +337,15 @@ describe('getToolDisplayTitle for operation-driven tools', () => { }) it('covers every knowledge-base operation with its actual verb and resource', () => { - for (const operation of KnowledgeBaseOperationValues) { - expect(getToolDisplayTitle('knowledge_base', { operation })).not.toBe( + for (const operation of ManageKnowledgeBaseOperationValues) { + expect(getToolDisplayTitle('manage_knowledge_base', { operation })).not.toBe( 'Managing knowledge base' ) } - expect(getToolDisplayTitle('knowledge_base', { operation: 'query' })).toBe( + expect(getToolDisplayTitle('manage_knowledge_base', { operation: 'query' })).toBe( 'Searching knowledge base' ) - expect(getToolDisplayTitle('knowledge_base', { operation: 'sync_connector' })).toBe( + expect(getToolDisplayTitle('manage_knowledge_base', { operation: 'sync_connector' })).toBe( 'Syncing knowledge base connector' ) }) @@ -381,19 +383,19 @@ describe('getToolDisplayTitle for operation-driven tools', () => { }) it('distinguishes saving uploads from importing workflows', () => { - for (const operation of MaterializeFileOperationValues) { + for (const operation of SaveUploadOperationValues) { expect( - getToolDisplayTitle('materialize_file', { operation, fileNames: ['Lead Router.json'] }) + getToolDisplayTitle('save_upload', { operation, fileNames: ['Lead Router.json'] }) ).not.toBe('Preparing file') } expect( - getToolDisplayTitle('materialize_file', { + getToolDisplayTitle('save_upload', { operation: 'save', fileNames: ['Quarterly Report.pdf'], }) ).toBe('Saving Quarterly Report.pdf') expect( - getToolDisplayTitle('materialize_file', { + getToolDisplayTitle('save_upload', { operation: 'import', fileNames: ['Lead Router.json'], }) diff --git a/apps/sim/lib/copilot/tools/tool-display.ts b/apps/sim/lib/copilot/tools/tool-display.ts index f15c80c522..61a3096cb3 100644 --- a/apps/sim/lib/copilot/tools/tool-display.ts +++ b/apps/sim/lib/copilot/tools/tool-display.ts @@ -449,36 +449,37 @@ const TOOL_TITLES: Record = { table_columns: 'Editing table columns', table_automations: 'Managing table automations', table_enrichments: 'Managing table enrichments', - workspace_file: 'Editing file', - edit_content: 'Applying file content', + table_views: 'Managing table views', + prepare_file_edit: 'Editing file', + apply_file_edit: 'Applying file content', create_workflow: 'Creating workflow', edit_workflow: 'Editing workflow', - knowledge_base: 'Managing knowledge base', + manage_knowledge_base: 'Managing knowledge base', search_knowledge_base: 'Searching knowledge base', open_resource: 'Opening resource', generate_image: 'Generating image', generate_video: 'Generating video', generate_audio: 'Generating audio', ffmpeg: 'Processing media', - check_deployment_status: 'Checking deployment status', - create_file: 'Creating file', + get_deployment_status: 'Checking deployment status', + create_empty_file: 'Creating file', create_file_folder: 'Creating folder', create_workspace_mcp_server: 'Creating MCP server', delete_workspace_mcp_server: 'Deleting MCP server', - deploy_api: 'Deploying API', - deploy_chat: 'Deploying chat', - deploy_custom_block: 'Deploying custom block', - deploy_mcp: 'Deploying MCP tool', + deploy_as_api: 'Deploying API', + deploy_as_chat: 'Deploying chat', + publish_custom_block: 'Deploying custom block', + deploy_as_mcp: 'Deploying MCP tool', diff_workflows: 'Comparing workflows', - download_to_workspace_file: 'Downloading file', - function_execute: 'Running code', + download_file: 'Downloading file', + run_function: 'Running code', complete_scheduled_task: 'Completing scheduled task', generate_api_key: 'Generating API key', get_block_outputs: 'Getting block outputs', get_block_upstream_references: 'Getting block references', get_deployed_workflow_state: 'Getting deployed workflow', - get_deployment_log: 'Getting deployment logs', - get_platform_actions: 'Getting platform actions', + list_deployment_versions: 'Getting deployment logs', + get_ui_reference: 'Getting platform actions', get_scheduled_task_logs: 'Reading scheduled task logs', get_workflow_data: 'Getting workflow data', get_workflow_run_options: 'Getting run options', @@ -487,7 +488,7 @@ const TOOL_TITLES: Record = { list_user_workspaces: 'Listing workspaces', list_workspace_mcp_servers: 'Listing MCP servers', load_deployment: 'Loading deployment', - materialize_file: 'Preparing file', + save_upload: 'Preparing file', manage_sandbox: 'Managing sandbox', manage_scheduled_task: 'Managing scheduled task', move_file: 'Moving file', @@ -503,8 +504,7 @@ const TOOL_TITLES: Record = { restore_resource: 'Restoring resource', run_block: 'Running block', scheduled_task: 'Managing scheduled task', - search_documentation: 'Searching documentation', - search_patterns: 'Searching patterns', + search_sim_docs: 'Searching documentation', set_block_enabled: 'Toggling block', set_environment_variables: 'Setting environment variables', set_global_workflow_variables: 'Setting workflow variables', @@ -532,7 +532,7 @@ const TOOL_TITLES: Record = { auth: 'Auth Agent', knowledge: 'Knowledge Agent', table: 'Table Agent', - agent: 'Tools Agent', + extensions: 'Extensions Agent', research: 'Research Agent', scout: 'Scout Agent', search: 'Search Agent', @@ -676,15 +676,15 @@ export function getToolDisplayTitle(name: string, args?: Record } switch (name) { - case 'deploy_api': + case 'deploy_as_api': return deploymentTitle(args, 'API') - case 'deploy_chat': + case 'deploy_as_chat': return deploymentTitle(args, 'chat') - case 'deploy_custom_block': + case 'publish_custom_block': return deploymentTitle(args, 'custom block') case 'ffmpeg': return ffmpegTitle(args) - case 'knowledge_base': + case 'manage_knowledge_base': return knowledgeBaseTitle(args) case 'query_user_table': case 'table_manage': @@ -692,6 +692,7 @@ export function getToolDisplayTitle(name: string, args?: Record case 'table_columns': case 'table_automations': case 'table_enrichments': + case 'table_views': return queryUserTableTitle(args) case 'search_knowledge_base': return searchKnowledgeBaseTitle(args) @@ -701,7 +702,7 @@ export function getToolDisplayTitle(name: string, args?: Record return manageScheduledTaskTitle(args) case 'user_table': return userTableTitle(args) - case 'materialize_file': + case 'save_upload': return materializeFileTitle(args) case 'open_resource': return openResourceTitle(args) @@ -771,7 +772,7 @@ export function getToolDisplayTitle(name: string, args?: Record } case 'set_global_workflow_variables': return setGlobalWorkflowVariablesTitle(args) - case 'create_file': + case 'create_empty_file': return createFileTitle(args) case 'share_file': { const action = stringArg(args, 'action') || 'share' @@ -799,7 +800,7 @@ export function getToolDisplayTitle(name: string, args?: Record const target = firstStringArg(args, 'serverName', 'name', 'title') return `Deleting ${target || 'MCP server'}` } - case 'search_online': { + case 'web_search': { const target = firstStringArg(args, 'toolTitle', 'title') return target ? `Searching online for ${target}` : 'Searching online' } @@ -838,7 +839,7 @@ export function getToolDisplayTitle(name: string, args?: Record summarizeTargets(stringArrayArg(args, 'paths').map(pathLeaf), 'resource') return target ? `Deleting ${target}` : 'Deleting' } - case 'enrichment_run': { + case 'run_enrichment': { const subject = nestedStringArg( args, 'inputs', @@ -850,7 +851,7 @@ export function getToolDisplayTitle(name: string, args?: Record ) return subject ? `Searching for ${subject}` : 'Searching' } - case 'scrape_page': { + case 'web_scrape': { const url = stringArg(args, 'url') return url ? `Scraping ${url}` : 'Scraping page' } @@ -886,11 +887,11 @@ export function getToolDisplayTitle(name: string, args?: Record const reason = stringArg(args, 'reason') return reason ? `Waiting for you: ${reason}` : 'Waiting for you in the browser' } - case 'crawl_website': { + case 'web_crawl': { const url = stringArg(args, 'url') return url ? `Crawling ${url}` : 'Crawling website' } - case 'get_page_contents': { + case 'web_fetch': { const urls = stringArrayArg(args, 'urls') if (urls.length === 1) return `Getting ${urls[0]}` if (urls.length > 1) return `Getting ${urls.length} pages` @@ -910,7 +911,7 @@ export function getToolDisplayTitle(name: string, args?: Record list: { verb: 'Viewing', resource: 'custom tools' }, }) } - case 'manage_mcp_tool': { + case 'manage_mcp_connection': { const target = firstStringArg(args, 'serverName', 'name', 'title') || nestedStringArg(args, 'config', 'name') @@ -957,9 +958,10 @@ export function getToolDisplayTitle(name: string, args?: Record } break } - case 'workspace_file': - case 'function_execute': { - const title = name === 'workspace_file' ? workspaceFileTitle(args) : stringArg(args, 'title') + case 'prepare_file_edit': + case 'run_function': { + const title = + name === 'prepare_file_edit' ? workspaceFileTitle(args) : stringArg(args, 'title') if (title) return title break } diff --git a/apps/sim/lib/copilot/vfs/resource-writer.ts b/apps/sim/lib/copilot/vfs/resource-writer.ts index 0c3dd20a16..6db6bc0319 100644 --- a/apps/sim/lib/copilot/vfs/resource-writer.ts +++ b/apps/sim/lib/copilot/vfs/resource-writer.ts @@ -146,7 +146,7 @@ export async function writeWorkspaceFileByPath(args: { /** * Forwarded to {@link updateWorkspaceFileContent} on an overwrite. Defaults to `true` (stream a * markdown overwrite into any open collaborative editor). Pass `false` for a write whose content is - * only a placeholder — e.g. `create_file`'s empty shell, whose real content lands via a later write. + * only a placeholder — e.g. `create_empty_file`'s empty shell, whose real content lands via a later write. */ syncLiveDoc?: boolean /** Private provenance for the exact bytes being written. */ diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 0fc76e8992..cb5348a061 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -1221,3 +1221,38 @@ export function serializeTriggerOverview( lines.push('') return lines.join('\n') } + +/** + * tables/{name}/views.json — the table's saved views in the column-NAME + * domain agents speak (stored configs are id-keyed; the caller translates). + * Layout-only fields (order, widths, pinned) are omitted: they are UI + * concerns and never change which rows a view selects. + */ +export function serializeTableViews( + views: Array<{ + id: string + name: string + isDefault: boolean + filter?: unknown + sort?: unknown + hiddenColumns?: string[] + updatedAt: Date | string + }> +): string { + return JSON.stringify( + { + views: views.map((view) => ({ + id: view.id, + name: view.name, + isDefault: view.isDefault, + filter: view.filter ?? null, + sort: view.sort ?? null, + hiddenColumns: view.hiddenColumns?.length ? view.hiddenColumns : undefined, + updatedAt: view.updatedAt instanceof Date ? view.updatedAt.toISOString() : view.updatedAt, + })), + note: 'Query a view via query_user_table {operation: "query_rows", args: {tableId, view: ""}} — the saved filter ANDs with any extra filter you pass. Manage views via the table agent (table_views).', + }, + null, + 2 + ) +} diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 96ca933172..f7ccc552e8 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -87,6 +87,7 @@ import { serializeSandboxCatalog, serializeSkill, serializeTableMeta, + serializeTableViews, serializeTriggerOverview, serializeTriggerSchema, serializeVersions, @@ -125,6 +126,12 @@ import { validateMermaidSource } from '@/lib/mermaid/validate' import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access' import { intersectIntegrationAllowlists } from '@/lib/permission-groups/integration-allowlist' import { listTables } from '@/lib/table/service' +import { + listTableViewsByWorkspace, + normalizeStoredViewConfig, + pruneViewConfig, + viewConfigIdsToNames, +} from '@/lib/table/views/service' import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { findWorkspaceFileRecord } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import type { @@ -1941,15 +1948,43 @@ export class WorkspaceVFS { */ private async materializeTables(workspaceId: string): Promise { try { - const [tables, folderPaths] = await Promise.all([ + const [tables, folderPaths, viewsByTable] = await Promise.all([ listTables(workspaceId), this.registerResourceFolders(workspaceId, 'table', 'tables'), + listTableViewsByWorkspace(workspaceId), ]) for (const table of tables) { const safeName = sanitizeName(table.name) const folderPath = table.folderId ? folderPaths.get(table.folderId) : undefined const prefix = folderPath ? `tables/${folderPath}/${safeName}` : `tables/${safeName}` + const viewRows = viewsByTable.get(table.id) ?? [] + if (viewRows.length > 0) { + const columns = table.schema.columns + this.files.set( + `${prefix}/views.json`, + serializeTableViews( + viewRows.map((row) => { + const config = viewConfigIdsToNames( + pruneViewConfig( + normalizeStoredViewConfig(row.config as Record), + columns + ), + columns + ) + return { + id: row.id, + name: row.name, + isDefault: row.isDefault, + filter: config.filter ?? null, + sort: config.sort ?? null, + hiddenColumns: config.hiddenColumns, + updatedAt: row.updatedAt, + } + }) + ) + ) + } this.files.set( `${prefix}/meta.json`, serializeTableMeta({ diff --git a/apps/sim/lib/folders/application/resource-vfs.ts b/apps/sim/lib/folders/application/resource-vfs.ts index 8c997fa7f1..00afc85937 100644 Binary files a/apps/sim/lib/folders/application/resource-vfs.ts and b/apps/sim/lib/folders/application/resource-vfs.ts differ diff --git a/apps/sim/lib/mcp/application/workflow-deployments.ts b/apps/sim/lib/mcp/application/workflow-deployments.ts index b0543432e1..04c52e29a7 100644 --- a/apps/sim/lib/mcp/application/workflow-deployments.ts +++ b/apps/sim/lib/mcp/application/workflow-deployments.ts @@ -281,7 +281,7 @@ export const deployWorkflowMcpTool = defineAuthorizedWorkspaceUseCase({ if (!context.workflow.isDeployed) { throw new OrchestrationError( 'validation', - 'Workflow must be deployed before adding as an MCP tool. Use deploy_api first.' + 'Workflow must be deployed before adding as an MCP tool. Use deploy_as_api first.' ) } if ( diff --git a/apps/sim/lib/mcp/workflow-mcp-sync.ts b/apps/sim/lib/mcp/workflow-mcp-sync.ts index 1c3d5c597f..8918d0c86f 100644 --- a/apps/sim/lib/mcp/workflow-mcp-sync.ts +++ b/apps/sim/lib/mcp/workflow-mcp-sync.ts @@ -134,7 +134,7 @@ export async function generateParameterSchemaForWorkflow( /** * Load a workflow's active deployed state and return its start-trigger input - * format fields. Shared so callers (e.g. the copilot `deploy_mcp` tool) can + * format fields. Shared so callers (e.g. the copilot `deploy_as_mcp` tool) can * build a parameter schema from the same input source the deploy modal uses. */ export async function getDeployedWorkflowInputFormat( diff --git a/apps/sim/lib/table/application/views.ts b/apps/sim/lib/table/application/views.ts index 1df29d7cc9..1363079d81 100644 --- a/apps/sim/lib/table/application/views.ts +++ b/apps/sim/lib/table/application/views.ts @@ -43,7 +43,7 @@ export const listTableViewsUseCase = defineAuthorizedTableUseCase({ (context.table.schema as TableSchema).columns, context.workspaceId ) - return { views } + return { views, table: context.table } }, }) @@ -62,7 +62,7 @@ export const readTableViewUseCase = defineAuthorizedTableUseCase({ context.workspaceId ) if (!view) throw new OrchestrationError('not_found', 'View not found') - return { view } + return { view, table: context.table } }, }) diff --git a/apps/sim/lib/table/application/workspace-file-imports.ts b/apps/sim/lib/table/application/workspace-file-imports.ts index f0e710a066..83fa556ad7 100644 --- a/apps/sim/lib/table/application/workspace-file-imports.ts +++ b/apps/sim/lib/table/application/workspace-file-imports.ts @@ -144,7 +144,7 @@ async function resolveSafeSourceFile( if (reference.replace(/^\/+/, '').startsWith('uploads/')) { throw new OrchestrationError( 'validation', - `Cannot import "${reference}": chat uploads are not workspace files. Use materialize_file to save it to a files/... path first, then pass that canonical path.` + `Cannot import "${reference}": chat uploads are not workspace files. Use save_upload to save it to a files/... path first, then pass that canonical path.` ) } throw new OrchestrationError( diff --git a/apps/sim/lib/table/views/service.test.ts b/apps/sim/lib/table/views/service.test.ts index 29e88f403b..e2a669d432 100644 --- a/apps/sim/lib/table/views/service.test.ts +++ b/apps/sim/lib/table/views/service.test.ts @@ -237,3 +237,52 @@ describe('getTableView', () => { expect(await getTableView('view-elsewhere', 'table-1', columns)).toBeNull() }) }) + +describe('view config name/id translation', () => { + const columns = [ + { id: 'col_a', name: 'status', type: 'string' }, + { id: 'col_b', name: 'due', type: 'date' }, + ] as never[] + + it('round-trips a config between id and name domains', async () => { + const { viewConfigIdsToNames, viewConfigNamesToIds } = await import('@/lib/table/views/service') + const stored = { + filter: { + any: [ + { field: 'col_a', op: 'eq', value: 'Open' }, + { all: [{ field: 'col_b', op: 'isNotNull' }] }, + ], + }, + sort: [{ field: 'col_b', direction: 'desc' }], + hiddenColumns: ['col_a'], + } as never + const named = viewConfigIdsToNames(stored, columns as never) + expect(named.filter).toEqual({ + any: [ + { field: 'status', op: 'eq', value: 'Open' }, + { all: [{ field: 'due', op: 'isNotNull' }] }, + ], + }) + expect(named.sort).toEqual([{ field: 'due', direction: 'desc' }]) + expect(named.hiddenColumns).toEqual(['status']) + expect(viewConfigNamesToIds(named, columns as never)).toEqual(stored) + }) + + it('passes stale ids through on read but rejects unknown names on write', async () => { + const { viewConfigIdsToNames, viewConfigNamesToIds } = await import('@/lib/table/views/service') + const withStale = { filter: { all: [{ field: 'col_gone', op: 'isNull' }] } } as never + expect( + ( + viewConfigIdsToNames(withStale, columns as never).filter as never as { + all: { field: string }[] + } + ).all[0].field + ).toBe('col_gone') + expect(() => + viewConfigNamesToIds( + { filter: { all: [{ field: 'nope', op: 'isNull' }] } } as never, + columns as never + ) + ).toThrow(/Unknown column/) + }) +}) diff --git a/apps/sim/lib/table/views/service.ts b/apps/sim/lib/table/views/service.ts index de0c8c2411..b75b36de91 100644 --- a/apps/sim/lib/table/views/service.ts +++ b/apps/sim/lib/table/views/service.ts @@ -336,3 +336,85 @@ export async function deleteTableView( } return deleted.length > 0 } + +/** + * All of a workspace's views in one query, keyed by tableId — the snapshot + * materializer's shape (per-table listTableViews would be N queries). Configs + * are returned RAW (id-domain, unpruned); callers translate/prune with each + * table's own columns. + */ +export async function listTableViewsByWorkspace( + workspaceId: string +): Promise>> { + const rows = await db + .select() + .from(tableViews) + .where(eq(tableViews.workspaceId, workspaceId)) + .orderBy(asc(tableViews.createdAt), asc(tableViews.id)) + const byTable = new Map>() + for (const row of rows) { + const list = byTable.get(row.tableId) ?? [] + list.push(row) + byTable.set(row.tableId, list) + } + return byTable +} + +function mapPredicateFields( + node: PredicateNode, + mapField: (field: string) => string +): PredicateNode { + if ('all' in node) return { all: node.all.map((child) => mapPredicateFields(child, mapField)) } + if ('any' in node) return { any: node.any.map((child) => mapPredicateFields(child, mapField)) } + const leaf = node as Predicate + return { ...leaf, field: mapField(leaf.field) } +} + +/** + * Stored (id-domain) view config → the column-NAME domain agents speak. + * Unknown ids pass through unchanged, mirroring pruneViewConfig's philosophy + * for filters: surfacing a stale reference beats silently widening the view. + */ +export function viewConfigIdsToNames( + config: TableViewConfig, + columns: ColumnDefinition[] +): TableViewConfig { + const nameById = new Map(columns.map((col) => [getColumnId(col), col.name])) + const toName = (field: string) => nameById.get(field) ?? field + const out: TableViewConfig = { ...config } + if (config.filter) out.filter = mapPredicateFields(config.filter, toName) as typeof config.filter + if (config.sort) out.sort = config.sort.map((s) => ({ ...s, field: toName(s.field) })) + if (config.hiddenColumns) out.hiddenColumns = config.hiddenColumns.map(toName) + return out +} + +/** + * Agent-supplied (name-domain) view config → the id-domain stored shape. + * Unknown column names are an error — a saved view with a dangling reference + * is exactly the artifact this translation exists to prevent. + */ +export function viewConfigNamesToIds( + config: TableViewConfig, + columns: ColumnDefinition[] +): TableViewConfig { + const idByName = new Map(columns.map((col) => [col.name, getColumnId(col)])) + const unknown = new Set() + const toId = (field: string) => { + const id = idByName.get(field) + if (!id) { + unknown.add(field) + return field + } + return id + } + const out: TableViewConfig = { ...config } + if (config.filter) out.filter = mapPredicateFields(config.filter, toId) as typeof config.filter + if (config.sort) out.sort = config.sort.map((s) => ({ ...s, field: toId(s.field) })) + if (config.hiddenColumns) out.hiddenColumns = config.hiddenColumns.map(toId) + if (unknown.size > 0) { + throw new TableViewValidationError( + `Unknown column(s): ${[...unknown].join(', ')}. Use exact column names from get_schema.` + ) + } + return out +} diff --git a/apps/sim/lib/uploads/archive.test.ts b/apps/sim/lib/uploads/archive.test.ts index 91fe18f2de..a8484f0049 100644 --- a/apps/sim/lib/uploads/archive.test.ts +++ b/apps/sim/lib/uploads/archive.test.ts @@ -485,7 +485,7 @@ describe('decompressArchiveBufferToWorkspaceFiles', () => { }) it('rolls back the folders it created when an upload fails mid-extraction', async () => { - // `materialize_file` refuses to re-extract into a root folder that still has any + // `save_upload` refuses to re-extract into a root folder that still has any // child, so a folder left behind by a failed run turns every retry into // "already extracted" until a human deletes the tree by hand. const buffer = await buildZip({ 'a/one.txt': 'first', 'b/two.txt': 'second' }) diff --git a/apps/sim/lib/uploads/archive.ts b/apps/sim/lib/uploads/archive.ts index 561d48330f..b6894fc27b 100644 --- a/apps/sim/lib/uploads/archive.ts +++ b/apps/sim/lib/uploads/archive.ts @@ -350,7 +350,7 @@ export async function decompressArchiveBufferToWorkspaceFiles( // Uploads themselves can still fail mid-loop (storage/DB errors, quota crossed // by another writer), so a failure rolls back every file written so far *and* // every folder this call materialized — callers and their retries must never - // observe a partial tree. Leftover folders are not cosmetic: `materialize_file` + // observe a partial tree. Leftover folders are not cosmetic: `save_upload` // refuses to re-extract into a root folder that still has any child, so a // half-extracted tree would make every retry fail until a human deletes it. const folderIdCache = new Map() diff --git a/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts b/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts index 50d1571246..24750a15f7 100644 --- a/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts +++ b/apps/sim/lib/uploads/contexts/workspace/track-chat-upload.test.ts @@ -417,7 +417,7 @@ describe('trackChatUpload', () => { /** * The ownership lookup and the write are separate statements, so a - * concurrent `materialize_file` can flip the row to context='workspace' + * concurrent `save_upload` can flip the row to context='workspace' * in between. The UPDATE must re-assert every ownership predicate rather * than matching on the captured row id alone, or it would drag a saved * workspace file back into chat scope. diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index cc83cc5e98..ebadf170bc 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -930,7 +930,7 @@ export async function trackChatUpload( if (updated.length === 0) { // The ownership lookup is a separate statement, so re-assert every // predicate here — this UPDATE is the atomic check. A concurrent - // `materialize_file` flips the same row to context='workspace' and + // `save_upload` flips the same row to context='workspace' and // clears chatId; matching on id alone would drag that saved file back // into chat scope, hiding it from the Files listing and re-exposing it // to the chat-delete cascade. diff --git a/apps/sim/lib/uploads/utils/file-utils.ts b/apps/sim/lib/uploads/utils/file-utils.ts index 6d04e92c7e..89324ad340 100644 --- a/apps/sim/lib/uploads/utils/file-utils.ts +++ b/apps/sim/lib/uploads/utils/file-utils.ts @@ -294,7 +294,7 @@ export function isArchiveFileName(filename: string): boolean { * `files/`, so this points at the explicit one-time extract step. */ export function buildArchiveExtractGuidance(name: string): string { - return `"${name}" is a .zip archive — its contents can't be read directly. Extract it once with materialize_file(fileNames: ["${name}"], operation: "extract"), then read the unpacked files under files/ (e.g. glob("files//**") then read("files///content")).` + return `"${name}" is a .zip archive — its contents can't be read directly. Extract it once with save_upload(fileNames: ["${name}"], operation: "extract"), then read the unpacked files under files/ (e.g. glob("files//**") then read("files///content")).` } const EXTENSION_TO_MIME: Record = { diff --git a/apps/sim/lib/uploads/utils/validation.ts b/apps/sim/lib/uploads/utils/validation.ts index b400fa621a..8729908061 100644 --- a/apps/sim/lib/uploads/utils/validation.ts +++ b/apps/sim/lib/uploads/utils/validation.ts @@ -97,7 +97,7 @@ export const SUPPORTED_VIDEO_EXTENSIONS = ['mp4', 'mov', 'avi', 'mkv', 'webm'] a /** * Archive formats accepted as chat attachments. A `.zip` is stored once in - * uploads/; the agent must extract it (materialize_file operation "extract") to + * uploads/; the agent must extract it (save_upload operation "extract") to * decompress it into workspace files/ before reading its contents. */ export const SUPPORTED_ARCHIVE_EXTENSIONS = ['zip'] as const @@ -229,7 +229,7 @@ export const CHAT_ACCEPT_ATTRIBUTE = [ /** * Accept attribute for the mothership copilot input only. Archives are scoped * here — NOT in {@link CHAT_ACCEPT_ATTRIBUTE} — because only the copilot flow - * has zip handling (materialize_file "extract"); a zip picked in a workflow or + * has zip handling (save_upload "extract"); a zip picked in a workflow or * deployed chat would flow into execution, where no parser exists. */ export const MOTHERSHIP_ACCEPT_ATTRIBUTE = [ diff --git a/apps/sim/lib/workflows/custom-blocks/operations.ts b/apps/sim/lib/workflows/custom-blocks/operations.ts index 4056e4db2c..9df7d48812 100644 --- a/apps/sim/lib/workflows/custom-blocks/operations.ts +++ b/apps/sim/lib/workflows/custom-blocks/operations.ts @@ -244,7 +244,7 @@ export async function listCustomBlocksWithInputs( /** * The custom block bound to a workflow (with live-derived input fields), or `null` * when the workflow isn't published as a block. One block per workflow is enforced - * at publish time. Used by the copilot deploy_custom_block tool. + * at publish time. Used by the copilot publish_custom_block tool. */ export async function getCustomBlockWithInputsByWorkflowId( workflowId: string diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.test.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.test.ts index 098305930d..612ae65b3e 100644 --- a/apps/sim/lib/workflows/orchestration/chat-deploy.test.ts +++ b/apps/sim/lib/workflows/orchestration/chat-deploy.test.ts @@ -46,7 +46,7 @@ describe('performChatDeploy password guards', () => { }) /** - * The copilot `deploy_chat` tool reaches this function without a route + * The copilot `deploy_as_chat` tool reaches this function without a route * contract, so these guards are the only thing standing between an agent and * a deployment nobody can log into. */ diff --git a/apps/sim/lib/workflows/orchestration/chat-deploy.ts b/apps/sim/lib/workflows/orchestration/chat-deploy.ts index 6a28925145..f4388208a8 100644 --- a/apps/sim/lib/workflows/orchestration/chat-deploy.ts +++ b/apps/sim/lib/workflows/orchestration/chat-deploy.ts @@ -60,7 +60,7 @@ export interface PerformChatDeployResult { * Deploys a chat: deploys the underlying workflow via `performFullDeploy`, * encrypts passwords, creates or updates the chat record, fires telemetry, * and records an audit entry. Both the chat API route and the copilot - * `deploy_chat` tool must use this function. + * `deploy_as_chat` tool must use this function. */ export async function performChatDeploy( params: ChatDeployPayload @@ -81,7 +81,7 @@ export async function performChatDeploy( /** * Validate the password here rather than only at the HTTP boundary. The - * copilot `deploy_chat` tool reaches this function without going through a + * copilot `deploy_as_chat` tool reaches this function without going through a * route contract, so a whitespace-only or over-long password would otherwise * be encrypted and stored — and neither can ever be submitted through the * chat login form, permanently locking visitors out of the deployment. @@ -171,7 +171,7 @@ export async function performChatDeploy( /** * A password-protected chat must end up with a stored password. Both HTTP * routes already reject this; without the same guard here a copilot - * `deploy_chat` call could create one with no password, which fails closed at + * `deploy_as_chat` call could create one with no password, which fails closed at * login with an opaque "Authentication configuration error". */ if (authType === 'password' && !encryptedPassword && !existingDeployment?.password) { @@ -309,7 +309,7 @@ export interface PerformChatUndeployResult { /** * Undeploys a chat: deletes the chat record and records an audit entry. - * Both the chat manage DELETE route and the copilot `deploy_chat` undeploy + * Both the chat manage DELETE route and the copilot `deploy_as_chat` undeploy * action must use this function. */ export async function performChatUndeploy(