From c52d63303d99e47e162e69d3ae14c23f9dca293d Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Mon, 6 Apr 2026 18:09:52 -0700 Subject: [PATCH] file/folder tools --- apps/sim/app/api/mcp/copilot/route.ts | 8 +- .../home/components/message-content/utils.ts | 2 +- .../app/workspace/[workspaceId]/home/types.ts | 6 +- .../copilot/chat/persisted-message.test.ts | 4 +- .../sim/lib/copilot/chat/workspace-context.ts | 34 ++++++- .../lib/copilot/generated/tool-catalog-v1.ts | 26 +++--- .../copilot/request/handlers/handlers.test.ts | 6 +- .../tools/client/tool-display-registry.ts | 22 ++--- apps/sim/lib/copilot/tools/mcp/definitions.ts | 89 +++---------------- apps/sim/lib/copilot/vfs/serializers.ts | 2 + apps/sim/lib/copilot/vfs/workspace-vfs.ts | 52 ++++++++++- 11 files changed, 126 insertions(+), 125 deletions(-) diff --git a/apps/sim/app/api/mcp/copilot/route.ts b/apps/sim/app/api/mcp/copilot/route.ts index 5711bf7638..fca77e30cb 100644 --- a/apps/sim/app/api/mcp/copilot/route.ts +++ b/apps/sim/app/api/mcp/copilot/route.ts @@ -122,12 +122,10 @@ Sim is a workflow automation platform. Workflows are visual pipelines of connect 1. \`list_workspaces\` → know where to work 2. \`create_workflow(name, workspaceId)\` → get a workflowId -3. \`sim_build(request, workflowId)\` → plan and build in one pass +3. \`sim_workflow(request, workflowId)\` → plan and build in one pass 4. \`sim_test(request, workflowId)\` → verify it works 5. \`sim_deploy("deploy as api", workflowId)\` → make it accessible externally (optional) -For fine-grained control, use \`sim_plan\` → \`sim_edit\` instead of \`sim_build\`. Pass the plan object from sim_plan EXACTLY as-is to sim_edit's context.plan field. - ### Working with Existing Workflows When the user refers to a workflow by name or description ("the email one", "my Slack bot"): @@ -670,7 +668,7 @@ async function handleDirectToolCall( /** * Build mode uses the main chat orchestrator with the 'fast' command instead of - * the subagent endpoint. In Go, 'build' is not a registered subagent — it's a mode + * the subagent endpoint. In Go, 'workflow' is not a registered subagent — it's a mode * (ModeFast) on the main chat processor that bypasses subagent orchestration and * executes all tools directly. */ @@ -768,7 +766,7 @@ async function handleSubagentToolCall( userId: string, abortSignal?: AbortSignal ): Promise { - if (toolDef.agentId === 'build') { + if (toolDef.agentId === 'workflow') { return handleBuildToolCall(args, userId, abortSignal) } 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 a60860b964..cd5ffb8cd8 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 @@ -43,7 +43,7 @@ const TOOL_ICONS: Record = { workspace_file: File, create_workflow: Layout, edit_workflow: Pencil, - build: Hammer, + workflow: Hammer, run: PlayOutline, deploy: Rocket, auth: Integration, diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index e410ee7c9c..ab994b54d7 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -1,7 +1,6 @@ import { Agent, Auth, - Build, CreateWorkflow, Debug, Deploy, @@ -26,6 +25,7 @@ import { Table, UserMemory, UserTable, + Workflow, WorkspaceFile, } from '@/lib/copilot/generated/tool-catalog-v1' import type { ChatContext } from '@/stores/panel' @@ -178,7 +178,7 @@ export interface ChatMessage { } export const SUBAGENT_LABELS: Record = { - build: 'Build agent', + workflow: 'Workflow agent', deploy: 'Deploy agent', auth: 'Integration agent', research: 'Research agent', @@ -281,7 +281,7 @@ export const TOOL_UI_METADATA: Record = { phaseLabel: 'Resource', phase: 'resource', }, - [Build.id]: { title: 'Building', phaseLabel: 'Build', phase: 'subagent' }, + [Workflow.id]: { title: 'Managing workflow', phaseLabel: 'Workflow', phase: 'subagent' }, [Run.id]: { title: 'Running', phaseLabel: 'Run', phase: 'subagent' }, [Deploy.id]: { title: 'Deploying', phaseLabel: 'Deploy', phase: 'subagent' }, [Auth.id]: { diff --git a/apps/sim/lib/copilot/chat/persisted-message.test.ts b/apps/sim/lib/copilot/chat/persisted-message.test.ts index cf2d9e3531..793bff1702 100644 --- a/apps/sim/lib/copilot/chat/persisted-message.test.ts +++ b/apps/sim/lib/copilot/chat/persisted-message.test.ts @@ -20,7 +20,7 @@ describe('persisted-message', () => { { type: 'tool_call', timestamp: Date.now(), - calledBy: 'build', + calledBy: 'workflow', toolCall: { id: 'tool-1', name: 'read', @@ -46,7 +46,7 @@ describe('persisted-message', () => { state: 'success', params: { path: 'foo.txt' }, result: { success: true, output: { ok: true } }, - calledBy: 'build', + calledBy: 'workflow', }, }, { diff --git a/apps/sim/lib/copilot/chat/workspace-context.ts b/apps/sim/lib/copilot/chat/workspace-context.ts index 6c8ac9a970..86d77f808c 100644 --- a/apps/sim/lib/copilot/chat/workspace-context.ts +++ b/apps/sim/lib/copilot/chat/workspace-context.ts @@ -45,6 +45,7 @@ export interface WorkspaceMdData { description?: string | null isDeployed: boolean lastRunAt?: Date | null + folderPath?: string | null }> knowledgeBases: Array<{ id: string @@ -92,15 +93,40 @@ export function buildWorkspaceMd(data: WorkspaceMdData): string { } if (data.workflows.length > 0) { - const lines = data.workflows.map((wf) => { - const parts = [`- **${wf.name}** (${wf.id})`] - if (wf.description) parts.push(` ${wf.description}`) + const rootWorkflows: typeof data.workflows = [] + const folderWorkflows = new Map() + + for (const wf of data.workflows) { + if (wf.folderPath) { + const existing = folderWorkflows.get(wf.folderPath) ?? [] + existing.push(wf) + folderWorkflows.set(wf.folderPath, existing) + } else { + rootWorkflows.push(wf) + } + } + + const formatWf = (wf: (typeof data.workflows)[0], indent: string) => { + const parts = [`${indent}- **${wf.name}** (${wf.id})`] + if (wf.description) parts.push(`${indent} ${wf.description}`) const flags: string[] = [] if (wf.isDeployed) flags.push('deployed') if (wf.lastRunAt) flags.push(`last run: ${wf.lastRunAt.toISOString().split('T')[0]}`) if (flags.length > 0) parts[0] += ` — ${flags.join(', ')}` return parts.join('\n') - }) + } + + const lines: string[] = [] + for (const wf of rootWorkflows) { + lines.push(formatWf(wf, '')) + } + const sortedFolders = [...folderWorkflows.entries()].sort((a, b) => a[0].localeCompare(b[0])) + for (const [folder, wfs] of sortedFolders) { + lines.push(`- 📁 **${folder}/**`) + for (const wf of wfs) { + lines.push(formatWf(wf, ' ')) + } + } sections.push(`## Workflows (${data.workflows.length})\n${lines.join('\n')}`) } else { sections.push('## Workflows (0)\n(none)') diff --git a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts index 867a5032c7..2b70d91b38 100644 --- a/apps/sim/lib/copilot/generated/tool-catalog-v1.ts +++ b/apps/sim/lib/copilot/generated/tool-catalog-v1.ts @@ -9,7 +9,6 @@ export interface ToolCatalogEntry { id: | 'agent' | 'auth' - | 'build' | 'check_deployment_status' | 'complete_job' | 'context_write' @@ -84,13 +83,13 @@ export interface ToolCatalogEntry { | 'update_workspace_mcp_server' | 'user_memory' | 'user_table' + | 'workflow' | 'workspace_file' internal?: boolean mode: 'async' | 'sync' name: | 'agent' | 'auth' - | 'build' | 'check_deployment_status' | 'complete_job' | 'context_write' @@ -165,13 +164,13 @@ export interface ToolCatalogEntry { | 'update_workspace_mcp_server' | 'user_memory' | 'user_table' + | 'workflow' | 'workspace_file' requiredPermission?: 'admin' | 'write' requiresConfirmation?: boolean subagentId?: | 'agent' | 'auth' - | 'build' | 'debug' | 'deploy' | 'file_write' @@ -181,6 +180,7 @@ export interface ToolCatalogEntry { | 'run' | 'superagent' | 'table' + | 'workflow' } export const Agent: ToolCatalogEntry = { @@ -202,15 +202,6 @@ export const Auth: ToolCatalogEntry = { internal: true, } -export const Build: ToolCatalogEntry = { - id: 'build', - name: 'build', - executor: 'subagent', - mode: 'async', - subagentId: 'build', - internal: true, -} - export const CheckDeploymentStatus: ToolCatalogEntry = { id: 'check_deployment_status', name: 'check_deployment_status', @@ -801,6 +792,15 @@ export const UserTable: ToolCatalogEntry = { requiresConfirmation: true, } +export const Workflow: ToolCatalogEntry = { + id: 'workflow', + name: 'workflow', + executor: 'subagent', + mode: 'async', + subagentId: 'workflow', + internal: true, +} + export const WorkspaceFile: ToolCatalogEntry = { id: 'workspace_file', name: 'workspace_file', @@ -812,7 +812,6 @@ export const WorkspaceFile: ToolCatalogEntry = { export const TOOL_CATALOG: Record = { [Agent.id]: Agent, [Auth.id]: Auth, - [Build.id]: Build, [CheckDeploymentStatus.id]: CheckDeploymentStatus, [CompleteJob.id]: CompleteJob, [ContextWrite.id]: ContextWrite, @@ -887,5 +886,6 @@ export const TOOL_CATALOG: Record = { [UpdateWorkspaceMcpServer.id]: UpdateWorkspaceMcpServer, [UserMemory.id]: UserMemory, [UserTable.id]: UserTable, + [Workflow.id]: Workflow, [WorkspaceFile.id]: WorkspaceFile, } diff --git a/apps/sim/lib/copilot/request/handlers/handlers.test.ts b/apps/sim/lib/copilot/request/handlers/handlers.test.ts index 0a22559537..e8e606eb2b 100644 --- a/apps/sim/lib/copilot/request/handlers/handlers.test.ts +++ b/apps/sim/lib/copilot/request/handlers/handlers.test.ts @@ -132,7 +132,7 @@ describe('sse-handlers tool lifecycle', () => { context.subAgentParentStack = ['parent-1'] context.toolCalls.set('parent-1', { id: 'parent-1', - name: 'build', + name: 'workflow', status: 'pending', startTime: Date.now(), }) @@ -140,7 +140,7 @@ describe('sse-handlers tool lifecycle', () => { await subAgentHandlers.tool( { type: MothershipStreamV1EventType.tool, - scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'build' }, + scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'workflow' }, payload: { toolCallId: 'sub-tool-1', toolName: 'create_workflow', @@ -158,7 +158,7 @@ describe('sse-handlers tool lifecycle', () => { await subAgentHandlers.tool( { type: MothershipStreamV1EventType.tool, - scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'build' }, + scope: { lane: 'subagent', parentToolCallId: 'parent-1', agentId: 'workflow' }, payload: { toolCallId: 'sub-tool-1', toolName: 'create_workflow', diff --git a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts index 6d0b04ae28..f9756090e7 100644 --- a/apps/sim/lib/copilot/tools/client/tool-display-registry.ts +++ b/apps/sim/lib/copilot/tools/client/tool-display-registry.ts @@ -371,20 +371,20 @@ const META_manage_skill: ToolMetadata = { }, } -const META_build: ToolMetadata = { +const META_workflow: ToolMetadata = { displayNames: { - [ClientToolCallState.generating]: { text: 'Building', icon: Loader2 }, - [ClientToolCallState.pending]: { text: 'Building', icon: Loader2 }, - [ClientToolCallState.executing]: { text: 'Building', icon: Loader2 }, - [ClientToolCallState.success]: { text: 'Built', icon: Wrench }, - [ClientToolCallState.error]: { text: 'Failed to build', icon: XCircle }, - [ClientToolCallState.rejected]: { text: 'Skipped build', icon: XCircle }, - [ClientToolCallState.aborted]: { text: 'Aborted build', icon: XCircle }, + [ClientToolCallState.generating]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.pending]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.executing]: { text: 'Managing workflow', icon: Loader2 }, + [ClientToolCallState.success]: { text: 'Done', icon: Wrench }, + [ClientToolCallState.error]: { text: 'Failed', icon: XCircle }, + [ClientToolCallState.rejected]: { text: 'Skipped', icon: XCircle }, + [ClientToolCallState.aborted]: { text: 'Aborted', icon: XCircle }, }, uiConfig: { subagent: { - streamingLabel: 'Building', - completedLabel: 'Built', + streamingLabel: 'Managing workflow', + completedLabel: 'Done', shouldCollapse: true, outputArtifacts: [], }, @@ -2298,7 +2298,7 @@ const TOOL_METADATA_BY_ID: Record = { checkoff_todo: META_checkoff_todo, crawl_website: META_crawl_website, create_workspace_mcp_server: META_create_workspace_mcp_server, - build: META_build, + workflow: META_workflow, create_folder: META_create_folder, create_workflow: META_create_workflow, agent: META_agent, diff --git a/apps/sim/lib/copilot/tools/mcp/definitions.ts b/apps/sim/lib/copilot/tools/mcp/definitions.ts index 6ca813a3b3..22b2e1d516 100644 --- a/apps/sim/lib/copilot/tools/mcp/definitions.ts +++ b/apps/sim/lib/copilot/tools/mcp/definitions.ts @@ -58,7 +58,7 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ name: 'create_workflow', toolId: 'create_workflow', description: - 'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_build for new workflows. Use workspaceId to place it in a specific workspace.', + 'Create a new empty workflow. Returns the new workflow ID. Always call this FIRST before sim_workflow for new workflows. Use workspaceId to place it in a specific workspace.', inputSchema: { type: 'object', properties: { @@ -263,14 +263,15 @@ export const DIRECT_TOOL_DEFS: DirectToolDef[] = [ export const SUBAGENT_TOOL_DEFS: SubagentToolDef[] = [ { - name: 'sim_build', - agentId: 'build', - description: `Build a workflow end-to-end in a single step. This is the fast mode equivalent for headless/MCP usage. + name: 'sim_workflow', + agentId: 'workflow', + description: `Create, modify, test, debug, and organize workflows end-to-end in a single step. USE THIS WHEN: - Building a new workflow from scratch - Modifying an existing workflow -- You want to gather information and build in one pass without separate plan→edit steps +- You want to gather information and build in one pass +- Moving, renaming, or organizing workflows and folders WORKFLOW ID (REQUIRED): - For NEW workflows: First call create_workflow to get a workflowId, then pass it here @@ -282,6 +283,7 @@ CAN DO: - Add, modify, or remove blocks - Configure block settings and connections - Set environment variables and workflow variables +- Move, rename, delete workflows and folders CANNOT DO: - Run or test workflows (use sim_test separately) @@ -289,8 +291,8 @@ CANNOT DO: WORKFLOW: 1. Call create_workflow to get a workflowId (for new workflows) -2. Call sim_build with the request and workflowId -3. Build agent gathers info and builds in one pass +2. Call sim_workflow with the request and workflowId +3. Workflow agent gathers info and builds in one pass 4. Call sim_test to verify it works 5. Optionally call sim_deploy to make it externally accessible`, inputSchema: { @@ -298,7 +300,7 @@ WORKFLOW: properties: { request: { type: 'string', - description: 'What you want to build or modify in the workflow.', + description: 'What you want to build, modify, or organize.', }, workflowId: { type: 'string', @@ -337,77 +339,6 @@ DO NOT USE (use direct tools instead): }, annotations: { readOnlyHint: true }, }, - { - name: 'sim_plan', - agentId: 'plan', - description: `Plan workflow changes by gathering required information. For most cases, prefer sim_build which combines planning and editing in one step. - -USE THIS WHEN: -- You need fine-grained control over the build process -- You want to inspect the plan before executing it - -WORKFLOW ID (REQUIRED): -- For NEW workflows: First call create_workflow to get a workflowId, then pass it here -- For EXISTING workflows: Always pass the workflowId parameter - -This tool gathers information about available blocks, credentials, and the current workflow state. - -RETURNS: A plan object containing block configurations, connections, and technical details. -IMPORTANT: Pass the returned plan EXACTLY to sim_edit - do not modify or summarize it.`, - inputSchema: { - type: 'object', - properties: { - request: { - type: 'string', - description: 'What you want to build or modify in the workflow.', - }, - workflowId: { - type: 'string', - description: - 'REQUIRED. The workflow ID. For new workflows, call create_workflow first to get this.', - }, - context: { type: 'object' }, - }, - required: ['request', 'workflowId'], - }, - annotations: { readOnlyHint: true }, - }, - { - name: 'sim_edit', - agentId: 'edit', - description: `Execute a workflow plan from sim_plan. For most cases, prefer sim_build which combines planning and editing in one step. - -WORKFLOW ID (REQUIRED): -- You MUST provide the workflowId parameter - -PLAN (REQUIRED): -- Pass the EXACT plan object from sim_plan in the context.plan field -- Do NOT modify, summarize, or interpret the plan - pass it verbatim - -After sim_edit completes, you can test immediately with sim_test, or deploy with sim_deploy to make it accessible externally.`, - inputSchema: { - type: 'object', - properties: { - message: { type: 'string', description: 'Optional additional instructions for the edit.' }, - workflowId: { - type: 'string', - description: - 'REQUIRED. The workflow ID to edit. Get this from create_workflow for new workflows.', - }, - plan: { - type: 'object', - description: 'The plan object from sim_plan. Pass it EXACTLY as returned, do not modify.', - }, - context: { - type: 'object', - description: - 'Additional context. Put the plan in context.plan if not using the plan field directly.', - }, - }, - required: ['workflowId'], - }, - annotations: { destructiveHint: false, openWorldHint: true }, - }, { name: 'sim_deploy', agentId: 'deploy', diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 4e72ad1d18..38644bc65e 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -12,6 +12,7 @@ export function serializeWorkflowMeta(wf: { id: string name: string description?: string | null + folderId?: string | null isDeployed: boolean deployedAt?: Date | null runCount: number @@ -24,6 +25,7 @@ export function serializeWorkflowMeta(wf: { id: wf.id, name: wf.name, description: wf.description || undefined, + folderId: wf.folderId || undefined, isDeployed: wf.isDeployed, deployedAt: wf.deployedAt?.toISOString(), runCount: wf.runCount, diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 662beaeeaa..b1c021405d 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -66,7 +66,7 @@ import { listCustomTools } from '@/lib/workflows/custom-tools/operations' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' import { listSkills } from '@/lib/workflows/skills/operations' -import { listWorkflows } from '@/lib/workflows/utils' +import { listFolders, listWorkflows } from '@/lib/workflows/utils' import { assertActiveWorkspaceAccess, getUsersWithPermissions, @@ -262,10 +262,11 @@ function getStaticComponentFiles(): Map { * * Structure: * WORKSPACE.md — workspace identity, members, inventory (auto-generated) - * workflows/{name}/meta.json + * workflows/{name}/meta.json (root-level workflows) * workflows/{name}/state.json (sanitized blocks with embedded connections) * workflows/{name}/executions.json * workflows/{name}/deployment.json + * workflows/{folder}/{name}/... (workflows inside folders, nested folders supported) * knowledgebases/{name}/meta.json * knowledgebases/{name}/documents.json * knowledgebases/{name}/connectors.json @@ -415,20 +416,62 @@ export class WorkspaceVFS { } } + /** + * Build a map from folderId to its full VFS path segment (e.g. "My Folder/Sub Folder"). + * Handles nested folders via parentId traversal. + */ + private buildFolderPaths( + folders: Array<{ folderId: string; folderName: string; parentId: string | null }> + ): Map { + const folderMap = new Map() + for (const f of folders) { + folderMap.set(f.folderId, { name: f.folderName, parentId: f.parentId }) + } + + const cache = new Map() + const resolve = (id: string): string => { + if (cache.has(id)) return cache.get(id)! + const folder = folderMap.get(id) + if (!folder) return '' + const parentPath = folder.parentId ? resolve(folder.parentId) : '' + const path = parentPath + ? `${parentPath}/${sanitizeName(folder.name)}` + : sanitizeName(folder.name) + cache.set(id, path) + return path + } + + for (const id of folderMap.keys()) { + resolve(id) + } + return cache + } + /** * Materialize all workflows using the shared listWorkflows function. + * Workflows are nested under their folder paths in the VFS: + * workflows/{folder}/{name}/ (if in a folder) + * workflows/{name}/ (if at workspace root) * Returns a summary for WORKSPACE.md generation. */ private async materializeWorkflows( workspaceId: string, _userId: string ): Promise { - const workflowRows = await listWorkflows(workspaceId) + const [workflowRows, folderRows] = await Promise.all([ + listWorkflows(workspaceId), + listFolders(workspaceId), + ]) + + const folderPaths = this.buildFolderPaths(folderRows) await Promise.all( workflowRows.map(async (wf) => { const safeName = sanitizeName(wf.name) - const prefix = `workflows/${safeName}/` + const folderPath = wf.folderId ? folderPaths.get(wf.folderId) : null + const prefix = folderPath + ? `workflows/${folderPath}/${safeName}/` + : `workflows/${safeName}/` this.files.set(`${prefix}meta.json`, serializeWorkflowMeta(wf)) @@ -506,6 +549,7 @@ export class WorkspaceVFS { description: wf.description, isDeployed: wf.isDeployed, lastRunAt: wf.lastRunAt, + folderPath: wf.folderId ? (folderPaths.get(wf.folderId) ?? null) : null, })) }