From 041ec63c40bb2d1460ab9585c2d6e0cfaab8a391 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 11:10:35 -0700 Subject: [PATCH 1/7] Add handle dragging tab to input chat --- .../resource-tabs/resource-tabs.tsx | 22 ++++++++--- .../home/components/user-input/user-input.tsx | 37 ++++++++++++++++++- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx index ba5481c3381..62b967940ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tabs.tsx @@ -160,12 +160,22 @@ export function ResourceTabs({ [chatId, onRemoveResource] ) - const handleDragStart = useCallback((e: React.DragEvent, idx: number) => { - dragStartIdx.current = idx - setDraggedIdx(idx) - e.dataTransfer.effectAllowed = 'move' - e.dataTransfer.setData('text/plain', String(idx)) - }, []) + const handleDragStart = useCallback( + (e: React.DragEvent, idx: number) => { + dragStartIdx.current = idx + setDraggedIdx(idx) + e.dataTransfer.effectAllowed = 'copyMove' + e.dataTransfer.setData('text/plain', String(idx)) + const resource = resources[idx] + if (resource) { + e.dataTransfer.setData( + 'application/x-sim-resource', + JSON.stringify({ type: resource.type, id: resource.id, title: resource.title }) + ) + } + }, + [resources] + ) const handleDragOver = useCallback((e: React.DragEvent, idx: number) => { e.preventDefault() diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 1a0066eec05..f2f5d565ed3 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -611,6 +611,7 @@ export function UserInput({ const files = useFileAttachments({ userId: userId || session?.user?.id, disabled: false, isLoading: isSending }) const hasFiles = files.attachedFiles.some((f) => !f.uploading && f.key) + const contextManagement = useContextManagement({ message: value }) const handleContextAdd = useCallback( @@ -696,6 +697,37 @@ export function UserInput({ [textareaRef, value, handleContextAdd, mentionMenu] ) + const handleContainerDragOver = useCallback( + (e: React.DragEvent) => { + if (e.dataTransfer.types.includes('application/x-sim-resource')) { + e.preventDefault() + e.stopPropagation() + e.dataTransfer.dropEffect = 'copy' + return + } + files.handleDragOver(e) + }, + [files] + ) + + const handleContainerDrop = useCallback( + (e: React.DragEvent) => { + const resourceJson = e.dataTransfer.getData('application/x-sim-resource') + if (resourceJson) { + e.preventDefault() + e.stopPropagation() + try { + const resource = JSON.parse(resourceJson) as MothershipResource + handleResourceSelect(resource, false) + } catch { + // Invalid JSON — ignore + } + } + files.handleDrop(e) + }, + [handleResourceSelect, files] + ) + useEffect(() => { if (wasSendingRef.current && !isSending) { textareaRef.current?.focus() @@ -977,8 +1009,8 @@ export function UserInput({ )} onDragEnter={files.handleDragEnter} onDragLeave={files.handleDragLeave} - onDragOver={files.handleDragOver} - onDrop={files.handleDrop} + onDragOver={handleContainerDragOver} + onDrop={handleContainerDrop} > {/* Context pills row */} {contextManagement.selectedContexts.length > 0 && ( @@ -1211,6 +1243,7 @@ export function UserInput({ )} + ) } From 2b1418183e0ef3b82747bf6bb854cb9df7e1650c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 11:17:07 -0700 Subject: [PATCH 2/7] Add back delete tools --- .../orchestrator/tool-executor/index.ts | 6 +++ .../orchestrator/tool-executor/param-types.ts | 8 ++++ .../tool-executor/workflow-tools/mutations.ts | 47 +++++++++++++++++++ .../tools/server/files/workspace-file.ts | 29 +++++++++++- apps/sim/lib/copilot/tools/server/router.ts | 3 +- .../copilot/tools/server/table/user-table.ts | 15 ++++++ apps/sim/lib/copilot/tools/shared/schemas.ts | 3 +- 7 files changed, 108 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts index 11a0d66c693..d1a59b241e5 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/index.ts @@ -52,6 +52,8 @@ import type { CreateFolderParams, CreateWorkflowParams, CreateWorkspaceMcpServerParams, + DeleteFolderParams, + DeleteWorkflowParams, DeleteWorkspaceMcpServerParams, DeployApiParams, DeployChatParams, @@ -80,6 +82,8 @@ import { executeVfsGlob, executeVfsGrep, executeVfsList, executeVfsRead } from ' import { executeCreateFolder, executeCreateWorkflow, + executeDeleteFolder, + executeDeleteWorkflow, executeGenerateApiKey, executeGetBlockOutputs, executeGetBlockUpstreamReferences, @@ -832,9 +836,11 @@ const SIM_WORKFLOW_TOOL_HANDLERS: Record< create_folder: (p, c) => executeCreateFolder(p as CreateFolderParams, c), rename_workflow: (p, c) => executeRenameWorkflow(p as unknown as RenameWorkflowParams, c), update_workflow: (p, c) => executeUpdateWorkflow(p as unknown as UpdateWorkflowParams, c), + delete_workflow: (p, c) => executeDeleteWorkflow(p as unknown as DeleteWorkflowParams, c), move_workflow: (p, c) => executeMoveWorkflow(p as unknown as MoveWorkflowParams, c), move_folder: (p, c) => executeMoveFolder(p as unknown as MoveFolderParams, c), rename_folder: (p, c) => executeRenameFolder(p as unknown as RenameFolderParams, c), + delete_folder: (p, c) => executeDeleteFolder(p as unknown as DeleteFolderParams, c), get_workflow_data: (p, c) => executeGetWorkflowData(p as GetWorkflowDataParams, c), get_block_outputs: (p, c) => executeGetBlockOutputs(p as GetBlockOutputsParams, c), get_block_upstream_references: (p, c) => diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts index 93eda72f224..0fd03c11b3e 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/param-types.ts @@ -169,6 +169,10 @@ export interface UpdateWorkflowParams { description?: string } +export interface DeleteWorkflowParams { + workflowId: string +} + export interface MoveWorkflowParams { workflowId: string folderId: string | null @@ -184,6 +188,10 @@ export interface RenameFolderParams { name: string } +export interface DeleteFolderParams { + folderId: string +} + export interface UpdateWorkspaceMcpServerParams { serverId: string name?: string diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts index a9e2156f9dc..d08821f4188 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts @@ -11,6 +11,8 @@ import { import { createFolderRecord, createWorkflowRecord, + deleteFolderRecord, + deleteWorkflowRecord, setWorkflowVariables, updateFolderRecord, updateWorkflowRecord, @@ -68,6 +70,8 @@ function buildExecutionError(error: unknown): ToolCallResult { import type { CreateFolderParams, CreateWorkflowParams, + DeleteFolderParams, + DeleteWorkflowParams, GenerateApiKeyParams, MoveFolderParams, MoveWorkflowParams, @@ -546,6 +550,28 @@ export async function executeUpdateWorkflow( } } +export async function executeDeleteWorkflow( + params: DeleteWorkflowParams, + context: ExecutionContext +): Promise { + try { + const workflowId = params.workflowId + if (!workflowId) { + return { success: false, error: 'workflowId is required' } + } + + const { workflow: workflowRecord } = await ensureWorkflowAccess(workflowId, context.userId) + await deleteWorkflowRecord(workflowId) + + return { + success: true, + output: { workflowId, name: workflowRecord.name, deleted: true }, + } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + export async function executeRenameFolder( params: RenameFolderParams, context: ExecutionContext @@ -571,6 +597,27 @@ export async function executeRenameFolder( } } +export async function executeDeleteFolder( + params: DeleteFolderParams, + context: ExecutionContext +): Promise { + try { + const folderId = params.folderId + if (!folderId) { + return { success: false, error: 'folderId is required' } + } + + const deleted = await deleteFolderRecord(folderId) + if (!deleted) { + return { success: false, error: 'Folder not found' } + } + + return { success: true, output: { folderId, deleted: true } } + } catch (error) { + return { success: false, error: error instanceof Error ? error.message : String(error) } + } +} + export async function executeRunBlock( params: RunBlockParams, context: ExecutionContext 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 a6ee4d842a1..75c6abe6110 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import type { BaseServerTool, ServerToolContext } from '@/lib/copilot/tools/server/base-tool' import type { WorkspaceFileArgs, WorkspaceFileResult } from '@/lib/copilot/tools/shared/schemas' import { + deleteWorkspaceFile, getWorkspaceFile, renameWorkspaceFile, updateWorkspaceFileContent, @@ -158,10 +159,36 @@ export const workspaceFileServerTool: BaseServerTool).fileId as string | undefined + if (!fileId) { + return { success: false, message: 'fileId is required for delete operation' } + } + + const fileRecord = await getWorkspaceFile(workspaceId, fileId) + if (!fileRecord) { + return { success: false, message: `File with ID "${fileId}" not found` } + } + + await deleteWorkspaceFile(workspaceId, fileId) + + logger.info('Workspace file deleted via copilot', { + fileId, + name: fileRecord.name, + userId: context.userId, + }) + + return { + success: true, + message: `File "${fileRecord.name}" deleted successfully`, + data: { id: fileId, name: fileRecord.name }, + } + } + default: return { success: false, - message: `Unknown operation: ${operation}. Supported: write, update, rename. Use the filesystem to list/read files.`, + message: `Unknown operation: ${operation}. Supported: write, update, rename, delete. Use the filesystem to list/read files.`, } } } catch (error) { diff --git a/apps/sim/lib/copilot/tools/server/router.ts b/apps/sim/lib/copilot/tools/server/router.ts index e2958a85c52..e36702a7560 100644 --- a/apps/sim/lib/copilot/tools/server/router.ts +++ b/apps/sim/lib/copilot/tools/server/router.ts @@ -41,6 +41,7 @@ const WRITE_ACTIONS: Record = { 'create', 'create_from_file', 'import_file', + 'delete', 'insert_row', 'batch_insert_rows', 'update_row', @@ -56,7 +57,7 @@ const WRITE_ACTIONS: Record = { manage_mcp_tool: ['add', 'edit', 'delete'], manage_skill: ['add', 'edit', 'delete'], manage_credential: ['rename', 'delete'], - workspace_file: ['write', 'update', 'rename'], + workspace_file: ['write', 'update', 'delete', 'rename'], } function isActionAllowed(toolName: string, action: string, userPermission: string): boolean { 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 c09e94a2eef..6cab39f13e0 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -12,6 +12,7 @@ import { deleteRow, deleteRowsByFilter, deleteRowsByIds, + deleteTable, getRowById, getTableById, insertRow, @@ -274,6 +275,20 @@ export const userTableServerTool: BaseServerTool } } + case 'delete': { + if (!args.tableId) { + return { success: false, message: 'Table ID is required' } + } + + const requestId = crypto.randomUUID().slice(0, 8) + await deleteTable(args.tableId, requestId) + + return { + success: true, + message: `Deleted table ${args.tableId}`, + } + } + case 'insert_row': { if (!args.tableId) { return { success: false, message: 'Table ID is required' } diff --git a/apps/sim/lib/copilot/tools/shared/schemas.ts b/apps/sim/lib/copilot/tools/shared/schemas.ts index 69f423eecd4..addf280c05c 100644 --- a/apps/sim/lib/copilot/tools/shared/schemas.ts +++ b/apps/sim/lib/copilot/tools/shared/schemas.ts @@ -112,6 +112,7 @@ export const UserTableArgsSchema = z.object({ 'import_file', 'get', 'get_schema', + 'delete', 'insert_row', 'batch_insert_rows', 'get_row', @@ -171,7 +172,7 @@ export type UserTableResult = z.infer // workspace_file - shared schema used by server tool and Go catalog export const WorkspaceFileArgsSchema = z.object({ - operation: z.enum(['write', 'update', 'rename']), + operation: z.enum(['write', 'update', 'delete', 'rename']), args: z .object({ fileId: z.string().optional(), From 10a03845ee1789dde2845a3d33dd0ab6122c077d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 13:43:43 -0700 Subject: [PATCH 3/7] Handle deletions properly with resources view --- .../resource-content/resource-content.tsx | 58 +++++++++- .../[workspaceId]/home/hooks/use-chat.ts | 23 +++- .../app/workspace/[workspaceId]/home/types.ts | 1 + .../[workspaceId]/knowledge/[id]/base.tsx | 27 ++--- .../[tableId]/components/table/table.tsx | 11 +- apps/sim/components/emcn/icons/database-x.tsx | 29 +++++ apps/sim/components/emcn/icons/file-x.tsx | 27 +++++ apps/sim/components/emcn/icons/index.ts | 4 + apps/sim/components/emcn/icons/table-x.tsx | 29 +++++ apps/sim/components/emcn/icons/workflow-x.tsx | 29 +++++ .../sse/handlers/tool-execution.ts | 26 +++++ .../tool-executor/workflow-tools/mutations.ts | 38 +++---- apps/sim/lib/copilot/orchestrator/types.ts | 1 + apps/sim/lib/copilot/resources.ts | 103 ++++++++++++++++++ 14 files changed, 359 insertions(+), 47 deletions(-) create mode 100644 apps/sim/components/emcn/icons/database-x.tsx create mode 100644 apps/sim/components/emcn/icons/file-x.tsx create mode 100644 apps/sim/components/emcn/icons/table-x.tsx create mode 100644 apps/sim/components/emcn/icons/workflow-x.tsx 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 eaf634acd9d..fefa1f5d3c8 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 @@ -4,7 +4,7 @@ import { lazy, Suspense, useCallback, useEffect, useMemo } from 'react' import { Square } from 'lucide-react' import { useRouter } from 'next/navigation' import { Button, PlayOutline, Skeleton, Tooltip } from '@/components/emcn' -import { BookOpen, SquareArrowUpRight } from '@/components/emcn/icons' +import { BookOpen, FileX, SquareArrowUpRight, WorkflowX } from '@/components/emcn/icons' import { markRunToolManuallyStopped, reportManualRunToolStop, @@ -62,9 +62,11 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource case 'workflow': return ( - - - + ) case 'knowledgebase': @@ -228,6 +230,44 @@ export function EmbeddedKnowledgeBaseActions({ ) } +interface EmbeddedWorkflowProps { + workspaceId: string + workflowId: string +} + +function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) { + const workflowExists = useWorkflowRegistry((state) => Boolean(state.workflows[workflowId])) + const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase) + const hydrationWorkflowId = useWorkflowRegistry((state) => state.hydration.workflowId) + const isMetadataLoaded = hydrationPhase !== 'idle' && hydrationPhase !== 'metadata-loading' + const hasLoadError = + hydrationPhase === 'error' && hydrationWorkflowId === workflowId + + if (!isMetadataLoaded) return LOADING_SKELETON + + if (!workflowExists || hasLoadError) { + return ( +
+ +
+

+ Workflow not found +

+

+ This workflow may have been deleted or moved +

+
+
+ ) + } + + return ( + + + + ) +} + interface EmbeddedFileProps { workspaceId: string fileId: string @@ -242,8 +282,14 @@ function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) { if (!file) { return ( -
- File not found +
+ +
+

File not found

+

+ This file may have been deleted or moved +

+
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 3329290d251..3aa2bdc1d77 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -51,7 +51,7 @@ export interface UseChatReturn { resources: MothershipResource[] activeResourceId: string | null setActiveResourceId: (id: string | null) => void - addResource: (resource: MothershipResource) => void + addResource: (resource: MothershipResource) => boolean removeResource: (resourceType: MothershipResourceType, resourceId: string) => void reorderResources: (resources: MothershipResource[]) => void } @@ -262,7 +262,11 @@ export function useChat( const { data: chatHistory } = useChatHistory(initialChatId) - const addResource = useCallback((resource: MothershipResource) => { + const addResource = useCallback((resource: MothershipResource): boolean => { + if (resourcesRef.current.some((r) => r.type === resource.type && r.id === resource.id)) { + return false + } + setResources((prev) => { const exists = prev.some((r) => r.type === resource.type && r.id === resource.id) if (exists) return prev @@ -270,7 +274,6 @@ export function useChat( }) setActiveResourceId(resource.id) - // Persist to database if we have a chat ID const currentChatId = chatIdRef.current if (currentChatId) { fetch('/api/copilot/chat/resources', { @@ -281,6 +284,7 @@ export function useChat( logger.warn('Failed to persist resource', err) }) } + return true }, []) const removeResource = useCallback((resourceType: MothershipResourceType, resourceId: string) => { @@ -548,7 +552,6 @@ export function useChat( ) if (resource) { addResource(resource) - invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id) onResourceEventRef.current?.() } } @@ -561,6 +564,7 @@ export function useChat( if (resource?.type && resource?.id) { addResource(resource) invalidateResourceQueries(queryClient, workspaceId, resource.type, resource.id) + onResourceEventRef.current?.() if (resource.type === 'workflow') { if (ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)) { @@ -572,6 +576,15 @@ export function useChat( } break } + case 'resource_deleted': { + const resource = parsed.resource + if (resource?.type && resource?.id) { + removeResource(resource.type as MothershipResourceType, resource.id) + invalidateResourceQueries(queryClient, workspaceId, resource.type as MothershipResourceType, resource.id) + onResourceEventRef.current?.() + } + break + } case 'tool_error': { const id = parsed.toolCallId || getPayloadData(parsed)?.id if (!id) break @@ -608,7 +621,7 @@ export function useChat( } } }, - [workspaceId, queryClient, addResource] + [workspaceId, queryClient, addResource, removeResource] ) const persistPartialResponse = useCallback(async () => { diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index 0be840c2672..da2898d1b64 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -23,6 +23,7 @@ export type SSEEventType = | 'tool_result' | 'tool_error' | 'resource_added' + | 'resource_deleted' | 'subagent_start' | 'subagent_end' | 'structured_result' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index c03eae8db8e..1c7eccde5a7 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -21,7 +21,7 @@ import { Tooltip, Trash, } from '@/components/emcn' -import { Database } from '@/components/emcn/icons' +import { Database, DatabaseX } from '@/components/emcn/icons' import { SearchHighlight } from '@/components/ui/search-highlight' import { cn } from '@/lib/core/utils/cn' import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants' @@ -1029,20 +1029,17 @@ export function KnowledgeBase({ if (error && !knowledgeBase) { return ( - router.push(`/workspace/${workspaceId}/knowledge`), - }, - { label: knowledgeBaseName }, - ]} - columns={DOCUMENT_COLUMNS} - rows={[]} - emptyMessage='Error loading knowledge base' - /> +
+ +
+

+ Knowledge base not found +

+

+ This knowledge base may have been deleted or moved +

+
+
) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 601a17be4c5..cb23448a15a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -30,6 +30,7 @@ import { Pencil, Plus, Table as TableIcon, + TableX, Trash, TypeBoolean, TypeJson, @@ -1234,8 +1235,14 @@ export function Table({ if (!isLoadingTable && !tableData) { return ( -
- Table not found +
+ +
+

Table not found

+

+ This table may have been deleted or moved +

+
) } diff --git a/apps/sim/components/emcn/icons/database-x.tsx b/apps/sim/components/emcn/icons/database-x.tsx new file mode 100644 index 00000000000..e6f955de8e0 --- /dev/null +++ b/apps/sim/components/emcn/icons/database-x.tsx @@ -0,0 +1,29 @@ +import type { SVGProps } from 'react' + +/** + * Database-X icon component - cylinder database with an X mark indicating a missing or deleted knowledge base + * @param props - SVG properties including className, fill, etc. + */ +export function DatabaseX(props: SVGProps) { + return ( + + + + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/file-x.tsx b/apps/sim/components/emcn/icons/file-x.tsx new file mode 100644 index 00000000000..07175014715 --- /dev/null +++ b/apps/sim/components/emcn/icons/file-x.tsx @@ -0,0 +1,27 @@ +import type { SVGProps } from 'react' + +/** + * File-X icon component - document with an X mark indicating a missing or deleted file + * @param props - SVG properties including className, fill, etc. + */ +export function FileX(props: SVGProps) { + return ( + + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 0a364f3ebd1..f573411bf64 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -20,6 +20,7 @@ export { Connections } from './connections' export { Copy } from './copy' export { Cursor } from './cursor' export { Database } from './database' +export { DatabaseX } from './database-x' export { DocumentAttachment } from './document-attachment' export { Download } from './download' export { Duplicate } from './duplicate' @@ -27,6 +28,7 @@ export { Expand } from './expand' export { ExternalLink } from './external-link' export { Eye } from './eye' export { File } from './file' +export { FileX } from './file-x' export { Fingerprint } from './fingerprint' export { FolderCode } from './folder-code' export { FolderPlus } from './folder-plus' @@ -64,6 +66,7 @@ export { ShieldCheck } from './shield-check' export { Sim } from './sim' export { SquareArrowUpRight } from './square-arrow-up-right' export { Table } from './table' +export { TableX } from './table-x' export { Tag } from './tag' export { TerminalWindow } from './terminal-window' export { Trash } from './trash' @@ -78,6 +81,7 @@ export { Upload } from './upload' export { User } from './user' export { UserPlus } from './user-plus' export { Users } from './users' +export { WorkflowX } from './workflow-x' export { Wrap } from './wrap' export { Wrench } from './wrench' export { X } from './x' diff --git a/apps/sim/components/emcn/icons/table-x.tsx b/apps/sim/components/emcn/icons/table-x.tsx new file mode 100644 index 00000000000..4418df7c462 --- /dev/null +++ b/apps/sim/components/emcn/icons/table-x.tsx @@ -0,0 +1,29 @@ +import type { SVGProps } from 'react' + +/** + * Table-X icon component - grid table with an X mark indicating a missing or deleted table + * @param props - SVG properties including className, fill, etc. + */ +export function TableX(props: SVGProps) { + return ( + + + + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/workflow-x.tsx b/apps/sim/components/emcn/icons/workflow-x.tsx new file mode 100644 index 00000000000..9ae1f4c4707 --- /dev/null +++ b/apps/sim/components/emcn/icons/workflow-x.tsx @@ -0,0 +1,29 @@ +import type { SVGProps } from 'react' + +/** + * Workflow-X icon component - workflow graph with an X mark indicating a missing or deleted workflow + * @param props - SVG properties including className, fill, etc. + */ +export function WorkflowX(props: SVGProps) { + return ( + + + + + + + + + ) +} diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts index 41ae7a3565d..c99dc44c07f 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts @@ -22,9 +22,12 @@ import type { ToolCallResult, } from '@/lib/copilot/orchestrator/types' import { + extractDeletedResourcesFromToolResult, extractResourcesFromToolResult, + isDeleteToolName, isResourceToolName, persistChatResources, + removeChatResources, } from '@/lib/copilot/resources' import { getTableById } from '@/lib/table/service' import { uploadWorkspaceFile } from '@/lib/uploads/contexts/workspace/workspace-file-manager' @@ -537,6 +540,29 @@ export async function executeToolAndReport( }) } } + + if (isDeleteToolName(toolCall.name)) { + const deleted = extractDeletedResourcesFromToolResult( + toolCall.name, + toolCall.params, + result.output + ) + if (deleted.length > 0) { + removeChatResources(execContext.chatId, deleted).catch((err) => { + logger.warn('Failed to remove chat resources after deletion', { + chatId: execContext.chatId, + error: err instanceof Error ? err.message : String(err), + }) + }) + + for (const resource of deleted) { + await options?.onEvent?.({ + type: 'resource_deleted', + resource: { type: resource.type, id: resource.id, title: resource.title }, + }) + } + } + } } } catch (error) { toolCall.status = 'error' diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts index d08821f4188..186ebf6bd00 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts @@ -572,33 +572,29 @@ export async function executeDeleteWorkflow( } } -export async function executeRenameFolder( - params: RenameFolderParams, - context: ExecutionContext +export async function executeDeleteFolder( + params: DeleteFolderParams, + _context: ExecutionContext ): Promise { try { const folderId = params.folderId if (!folderId) { return { success: false, error: 'folderId is required' } } - const name = typeof params.name === 'string' ? params.name.trim() : '' - if (!name) { - return { success: false, error: 'name is required' } - } - if (name.length > 200) { - return { success: false, error: 'Folder name must be 200 characters or less' } - } - await updateFolderRecord(folderId, { name }) + const deleted = await deleteFolderRecord(folderId) + if (!deleted) { + return { success: false, error: 'Folder not found' } + } - return { success: true, output: { folderId, name } } + return { success: true, output: { folderId, deleted: true } } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } } -export async function executeDeleteFolder( - params: DeleteFolderParams, +export async function executeRenameFolder( + params: RenameFolderParams, context: ExecutionContext ): Promise { try { @@ -606,13 +602,17 @@ export async function executeDeleteFolder( if (!folderId) { return { success: false, error: 'folderId is required' } } - - const deleted = await deleteFolderRecord(folderId) - if (!deleted) { - return { success: false, error: 'Folder not found' } + const name = typeof params.name === 'string' ? params.name.trim() : '' + if (!name) { + return { success: false, error: 'name is required' } + } + if (name.length > 200) { + return { success: false, error: 'Folder name must be 200 characters or less' } } - return { success: true, output: { folderId, deleted: true } } + await updateFolderRecord(folderId, { name }) + + return { success: true, output: { folderId, name } } } catch (error) { return { success: false, error: error instanceof Error ? error.message : String(error) } } diff --git a/apps/sim/lib/copilot/orchestrator/types.ts b/apps/sim/lib/copilot/orchestrator/types.ts index 82ced642aae..376d29273bb 100644 --- a/apps/sim/lib/copilot/orchestrator/types.ts +++ b/apps/sim/lib/copilot/orchestrator/types.ts @@ -11,6 +11,7 @@ export type SSEEventType = | 'tool_result' | 'tool_error' | 'resource_added' + | 'resource_deleted' | 'subagent_start' | 'subagent_end' | 'structured_result' diff --git a/apps/sim/lib/copilot/resources.ts b/apps/sim/lib/copilot/resources.ts index ba118953ee3..75f3353e41e 100644 --- a/apps/sim/lib/copilot/resources.ts +++ b/apps/sim/lib/copilot/resources.ts @@ -167,6 +167,73 @@ export function extractResourcesFromToolResult( } } +/** + * Extracts resource descriptors from a tool execution result when the tool + * performed a deletion. Returns one or more deleted resources for tools that + * destroy workspace entities. + */ +export function extractDeletedResourcesFromToolResult( + toolName: string, + params: Record | undefined, + output: unknown +): ChatResource[] { + const result = asRecord(output) + const data = asRecord(result.data) + const args = asRecord(params?.args) + const operation = (args.operation ?? params?.operation) as string | undefined + + switch (toolName) { + case 'delete_workflow': { + const workflowId = (result.workflowId as string) ?? (params?.workflowId as string) + if (workflowId && result.deleted) { + return [{ type: 'workflow', id: workflowId, title: (result.name as string) || 'Workflow' }] + } + return [] + } + + case 'workspace_file': { + if (operation !== 'delete') return [] + const fileId = (data.id as string) ?? (args.fileId as string) + if (fileId) { + return [{ type: 'file', id: fileId, title: (data.name as string) || 'File' }] + } + return [] + } + + case 'user_table': { + if (operation !== 'delete') return [] + const tableId = (args.tableId as string) ?? (params?.tableId as string) + if (tableId) { + return [{ type: 'table', id: tableId, title: 'Table' }] + } + return [] + } + + case 'knowledge_base': { + if (operation !== 'delete') return [] + const kbId = (data.id as string) ?? (args.knowledgeBaseId as string) + if (kbId) { + return [{ type: 'knowledgebase', id: kbId, title: (data.name as string) || 'Knowledge Base' }] + } + return [] + } + + default: + return [] + } +} + +const DELETE_TOOL_NAMES = new Set([ + 'delete_workflow', + 'workspace_file', + 'user_table', + 'knowledge_base', +]) + +export function isDeleteToolName(toolName: string): boolean { + return DELETE_TOOL_NAMES.has(toolName) +} + /** * Appends resources to a chat's JSONB resources column, deduplicating by type+id. * Updates the title of existing resources if the new title is more specific. @@ -215,3 +282,39 @@ export async function persistChatResources( }) } } + +/** + * Removes resources from a chat's JSONB resources column by type+id. + */ +export async function removeChatResources( + chatId: string, + toRemove: ChatResource[] +): Promise { + if (toRemove.length === 0) return + + try { + const [chat] = await db + .select({ resources: copilotChats.resources }) + .from(copilotChats) + .where(eq(copilotChats.id, chatId)) + .limit(1) + + if (!chat) return + + const existing = Array.isArray(chat.resources) ? (chat.resources as ChatResource[]) : [] + const removeKeys = new Set(toRemove.map((r) => `${r.type}:${r.id}`)) + const filtered = existing.filter((r) => !removeKeys.has(`${r.type}:${r.id}`)) + + if (filtered.length === existing.length) return + + await db + .update(copilotChats) + .set({ resources: sql`${JSON.stringify(filtered)}::jsonb` }) + .where(eq(copilotChats.id, chatId)) + } catch (err) { + logger.warn('Failed to remove chat resources', { + chatId, + error: err instanceof Error ? err.message : String(err), + }) + } +} From 604fd9a590a32de94af75171c79b66bcd8e0b6a2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 13:44:20 -0700 Subject: [PATCH 4/7] Fix lint --- .../resource-content/resource-content.tsx | 13 +--- .../user-input/components/context-pills.tsx | 14 +--- .../home/components/user-input/user-input.tsx | 77 +++++++++++++------ .../home/components/user-message-content.tsx | 12 +-- .../app/workspace/[workspaceId]/home/home.tsx | 14 +++- .../[workspaceId]/home/hooks/use-chat.ts | 15 +++- .../[workspaceId]/knowledge/[id]/base.tsx | 2 +- .../[tableId]/components/table/table.tsx | 2 +- apps/sim/lib/copilot/resources.ts | 9 +-- 9 files changed, 90 insertions(+), 68 deletions(-) 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 fefa1f5d3c8..6545b6ce143 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 @@ -62,11 +62,7 @@ export function ResourceContent({ workspaceId, resource, previewMode }: Resource case 'workflow': return ( - + ) case 'knowledgebase': @@ -240,8 +236,7 @@ function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) { const hydrationPhase = useWorkflowRegistry((state) => state.hydration.phase) const hydrationWorkflowId = useWorkflowRegistry((state) => state.hydration.workflowId) const isMetadataLoaded = hydrationPhase !== 'idle' && hydrationPhase !== 'metadata-loading' - const hasLoadError = - hydrationPhase === 'error' && hydrationWorkflowId === workflowId + const hasLoadError = hydrationPhase === 'error' && hydrationWorkflowId === workflowId if (!isMetadataLoaded) return LOADING_SKELETON @@ -250,7 +245,7 @@ function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) {
-

+

Workflow not found

@@ -285,7 +280,7 @@ function EmbeddedFile({ workspaceId, fileId, previewMode }: EmbeddedFileProps) {

-

File not found

+

File not found

This file may have been deleted or moved

diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/context-pills.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/context-pills.tsx index 854c18bef43..a1d9c013a92 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/context-pills.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/context-pills.tsx @@ -31,19 +31,9 @@ function getContextIcon(ctx: ChatContext) { switch (ctx.kind) { case 'workflow': case 'current_workflow': - return ( - - ) + return case 'workflow_block': - return ( - - ) + return case 'knowledge': return case 'templates': diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index f2f5d565ed3..ab3c9a85973 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -32,7 +32,17 @@ type WindowWithSpeech = Window & { } import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { ArrowUp, AtSign, ChevronRight, Folder, Loader2, Mic, Paperclip, Plus, X } from 'lucide-react' +import { + ArrowUp, + AtSign, + ChevronRight, + Folder, + Loader2, + Mic, + Paperclip, + Plus, + X, +} from 'lucide-react' import { useParams } from 'next/navigation' import { createPortal } from 'react-dom' import { @@ -66,7 +76,15 @@ import { import { useSession } from '@/lib/auth/auth-client' import { cn } from '@/lib/core/utils/cn' import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation' -import { ContextPills } from './components' +import { + type AvailableItem, + useAvailableResources, +} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' +import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' +import type { + MothershipResource, + MothershipResourceType, +} from '@/app/workspace/[workspaceId]/home/types' import { useCaretViewport, useContextManagement, @@ -78,17 +96,12 @@ import { computeMentionHighlightRanges, extractContextTokens, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/utils' -import type { ChatContext } from '@/stores/panel' -import { - useAvailableResources, - type AvailableItem, -} from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' -import { getResourceConfig } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry' -import type { MothershipResource, MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types' import { useFolders } from '@/hooks/queries/folders' import { useFolderStore } from '@/stores/folders/store' import type { FolderTreeNode } from '@/stores/folders/types' +import type { ChatContext } from '@/stores/panel' import { useAnimatedPlaceholder } from '../../hooks' +import { ContextPills } from './components' const TEXTAREA_BASE_CLASSES = cn( 'm-0 box-border h-auto min-h-[24px] w-full resize-none', @@ -184,9 +197,7 @@ function ResourceMentionMenu({ ) } // When no query, show all items flat - return availableResources.flatMap(({ type, items }) => - items.map((item) => ({ type, item })) - ) + return availableResources.flatMap(({ type, items }) => items.map((item) => ({ type, item }))) }, [availableResources, query]) // Reset active index when query changes @@ -263,7 +274,9 @@ function ResourceMentionMenu({ onClick={() => handleSelect({ type, id: item.id, title: item.name })} className={cn( 'flex cursor-pointer items-center gap-[8px] px-[8px] py-[6px] text-[13px]', - index === activeIndex ? 'bg-[var(--surface-active)]' : 'hover:bg-[var(--surface-active)]' + index === activeIndex + ? 'bg-[var(--surface-active)]' + : 'hover:bg-[var(--surface-active)]' )} > {config.renderDropdownItem({ item })} @@ -292,7 +305,13 @@ interface ResourceTypeFolderProps { onSelect: (resource: MothershipResource) => void } -function ResourceTypeFolder({ type, items, config, workspaceId, onSelect }: ResourceTypeFolderProps) { +function ResourceTypeFolder({ + type, + items, + config, + workspaceId, + onSelect, +}: ResourceTypeFolderProps) { const [expanded, setExpanded] = useState(false) const Icon = config.icon @@ -578,7 +597,11 @@ export interface FileAttachmentForApi { interface UserInputProps { defaultValue?: string - onSubmit: (text: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => void + onSubmit: ( + text: string, + fileAttachments?: FileAttachmentForApi[], + contexts?: ChatContext[] + ) => void isSending: boolean onStopGeneration: () => void isInitialView?: boolean @@ -608,10 +631,13 @@ export function UserInput({ const animatedPlaceholder = useAnimatedPlaceholder(isInitialView) const placeholder = isInitialView ? animatedPlaceholder : 'Send message to Sim' - const files = useFileAttachments({ userId: userId || session?.user?.id, disabled: false, isLoading: isSending }) + const files = useFileAttachments({ + userId: userId || session?.user?.id, + disabled: false, + isLoading: isSending, + }) const hasFiles = files.attachedFiles.some((f) => !f.uploading && f.key) - const contextManagement = useContextManagement({ message: value }) const handleContextAdd = useCallback( @@ -741,10 +767,13 @@ export function UserInput({ } }, [isInitialView, textareaRef]) - const handleContainerClick = useCallback((e: React.MouseEvent) => { - if ((e.target as HTMLElement).closest('button')) return - textareaRef.current?.focus() - }, [textareaRef]) + const handleContainerClick = useCallback( + (e: React.MouseEvent) => { + if ((e.target as HTMLElement).closest('button')) return + textareaRef.current?.focus() + }, + [textareaRef] + ) const handleSubmit = useCallback(() => { const fileAttachmentsForApi: FileAttachmentForApi[] = files.attachedFiles @@ -898,7 +927,6 @@ export function UserInput({ [isInitialView] ) - const toggleListening = useCallback(() => { if (isListening) { recognitionRef.current?.stop() @@ -1122,7 +1150,9 @@ export function UserInput({ onClose={() => { mentionMenu.closeMentionMenu() }} - query={mentionMenu.getActiveMentionQueryAtPosition(mentionMenu.getCaretPos())?.query ?? ''} + query={ + mentionMenu.getActiveMentionQueryAtPosition(mentionMenu.getCaretPos())?.query ?? '' + } />, document.body )} @@ -1243,7 +1273,6 @@ export function UserInput({
)} -
) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content.tsx index f08d4921048..5955427ba25 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content.tsx @@ -1,8 +1,7 @@ 'use client' -import { cn } from '@/lib/core/utils/cn' -import type { ChatMessageContext } from '../types' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' +import type { ChatMessageContext } from '../types' interface UserMessageContentProps { content: string @@ -48,15 +47,10 @@ function MentionHighlight({ context, token }: { context: ChatMessageContext; tok return null }) - const bgColor = workflowColor - ? `${workflowColor}40` - : 'rgba(50, 189, 126, 0.4)' + const bgColor = workflowColor ? `${workflowColor}40` : 'rgba(50, 189, 126, 0.4)' return ( - + {token} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index c9478ae507f..795aabe0ef4 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -15,11 +15,17 @@ import { } from '@/lib/core/utils/browser-storage' import { persistImportedWorkflow } from '@/lib/workflows/operations/import-export' import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks' -import { MessageContent, MothershipView, TemplatePrompts, UserInput, UserMessageContent } from './components' -import type { FileAttachmentForApi } from './components/user-input/user-input' import type { ChatContext } from '@/stores/panel' -import type { MothershipResource, MothershipResourceType } from './types' +import { + MessageContent, + MothershipView, + TemplatePrompts, + UserInput, + UserMessageContent, +} from './components' +import type { FileAttachmentForApi } from './components/user-input/user-input' import { useAutoScroll, useChat } from './hooks' +import type { MothershipResource, MothershipResourceType } from './types' const logger = createLogger('Home') @@ -245,7 +251,7 @@ export function Home({ chatId }: HomeProps = {}) { (context: ChatContext) => { let resourceType: MothershipResourceType | null = null let resourceId: string | null = null - let resourceTitle: string = context.label + const resourceTitle: string = context.label switch (context.kind) { case 'workflow': diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 3aa2bdc1d77..af9812f0a8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -20,11 +20,11 @@ import { taskKeys, useChatHistory, } from '@/hooks/queries/tasks' -import type { ChatContext } from '@/stores/panel' import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order' import { useExecutionStream } from '@/hooks/use-execution-stream' import { useExecutionStore } from '@/stores/execution/store' import { useFolderStore } from '@/stores/folders/store' +import type { ChatContext } from '@/stores/panel' import { useTerminalConsoleStore } from '@/stores/terminal' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { invalidateResourceQueries } from '../components/mothership-view/components/resource-registry' @@ -46,7 +46,11 @@ export interface UseChatReturn { isSending: boolean error: string | null resolvedChatId: string | undefined - sendMessage: (message: string, fileAttachments?: FileAttachmentForApi[], contexts?: ChatContext[]) => Promise + sendMessage: ( + message: string, + fileAttachments?: FileAttachmentForApi[], + contexts?: ChatContext[] + ) => Promise stopGeneration: () => Promise resources: MothershipResource[] activeResourceId: string | null @@ -580,7 +584,12 @@ export function useChat( const resource = parsed.resource if (resource?.type && resource?.id) { removeResource(resource.type as MothershipResourceType, resource.id) - invalidateResourceQueries(queryClient, workspaceId, resource.type as MothershipResourceType, resource.id) + invalidateResourceQueries( + queryClient, + workspaceId, + resource.type as MothershipResourceType, + resource.id + ) onResourceEventRef.current?.() } break diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx index 1c7eccde5a7..48d7e47c6c5 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx @@ -1032,7 +1032,7 @@ export function KnowledgeBase({
-

+

Knowledge base not found

diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index cb23448a15a..7687516a648 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -1238,7 +1238,7 @@ export function Table({

-

Table not found

+

Table not found

This table may have been deleted or moved

diff --git a/apps/sim/lib/copilot/resources.ts b/apps/sim/lib/copilot/resources.ts index 75f3353e41e..7f275f0da36 100644 --- a/apps/sim/lib/copilot/resources.ts +++ b/apps/sim/lib/copilot/resources.ts @@ -213,7 +213,9 @@ export function extractDeletedResourcesFromToolResult( if (operation !== 'delete') return [] const kbId = (data.id as string) ?? (args.knowledgeBaseId as string) if (kbId) { - return [{ type: 'knowledgebase', id: kbId, title: (data.name as string) || 'Knowledge Base' }] + return [ + { type: 'knowledgebase', id: kbId, title: (data.name as string) || 'Knowledge Base' }, + ] } return [] } @@ -286,10 +288,7 @@ export async function persistChatResources( /** * Removes resources from a chat's JSONB resources column by type+id. */ -export async function removeChatResources( - chatId: string, - toRemove: ChatResource[] -): Promise { +export async function removeChatResources(chatId: string, toRemove: ChatResource[]): Promise { if (toRemove.length === 0) return try { From 819ccb54ee97f6b08c088cb61de7716fb30accd8 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 14:01:31 -0700 Subject: [PATCH 5/7] Add permisssions checking --- .../home/components/user-input/user-input.tsx | 1 + .../sse/handlers/tool-execution.ts | 4 +- .../tool-executor/workflow-tools/mutations.ts | 13 +++++- apps/sim/lib/copilot/resources.ts | 46 ++++++++++--------- .../copilot/tools/server/table/user-table.ts | 11 +++++ 5 files changed, 51 insertions(+), 24 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index ab3c9a85973..505c30d83c2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx @@ -748,6 +748,7 @@ export function UserInput({ } catch { // Invalid JSON — ignore } + return } files.handleDrop(e) }, diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts index c99dc44c07f..39cac5c9943 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts @@ -24,7 +24,7 @@ import type { import { extractDeletedResourcesFromToolResult, extractResourcesFromToolResult, - isDeleteToolName, + hasDeleteCapability, isResourceToolName, persistChatResources, removeChatResources, @@ -541,7 +541,7 @@ export async function executeToolAndReport( } } - if (isDeleteToolName(toolCall.name)) { + if (hasDeleteCapability(toolCall.name)) { const deleted = extractDeletedResourcesFromToolResult( toolCall.name, toolCall.params, diff --git a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts index 186ebf6bd00..b383252e00c 100644 --- a/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts +++ b/apps/sim/lib/copilot/orchestrator/tool-executor/workflow-tools/mutations.ts @@ -13,6 +13,7 @@ import { createWorkflowRecord, deleteFolderRecord, deleteWorkflowRecord, + listFolders, setWorkflowVariables, updateFolderRecord, updateWorkflowRecord, @@ -574,7 +575,7 @@ export async function executeDeleteWorkflow( export async function executeDeleteFolder( params: DeleteFolderParams, - _context: ExecutionContext + context: ExecutionContext ): Promise { try { const folderId = params.folderId @@ -582,6 +583,16 @@ export async function executeDeleteFolder( return { success: false, error: 'folderId is required' } } + const workspaceId = + context.workspaceId || (await getDefaultWorkspaceId(context.userId)) + await ensureWorkspaceAccess(workspaceId, context.userId, true) + + const folders = await listFolders(workspaceId) + const folder = folders.find((f) => f.folderId === folderId) + if (!folder) { + return { success: false, error: 'Folder not found' } + } + const deleted = await deleteFolderRecord(folderId) if (!deleted) { return { success: false, error: 'Folder not found' } diff --git a/apps/sim/lib/copilot/resources.ts b/apps/sim/lib/copilot/resources.ts index 7f275f0da36..d28d320c820 100644 --- a/apps/sim/lib/copilot/resources.ts +++ b/apps/sim/lib/copilot/resources.ts @@ -2,13 +2,14 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { eq, sql } from 'drizzle-orm' +import type { MothershipResource, MothershipResourceType } from '@/lib/copilot/resource-types' const logger = createLogger('CopilotResources') -export type { - MothershipResource as ChatResource, - MothershipResourceType as ResourceType, -} from '@/lib/copilot/resource-types' +export type { MothershipResource as ChatResource, MothershipResourceType as ResourceType } + +type ChatResource = MothershipResource +type ResourceType = MothershipResourceType const RESOURCE_TOOL_NAMES = new Set([ 'user_table', @@ -167,6 +168,17 @@ export function extractResourcesFromToolResult( } } +const DELETE_CAPABLE_TOOL_RESOURCE_TYPE: Record = { + delete_workflow: 'workflow', + workspace_file: 'file', + user_table: 'table', + knowledge_base: 'knowledgebase', +} + +export function hasDeleteCapability(toolName: string): boolean { + return toolName in DELETE_CAPABLE_TOOL_RESOURCE_TYPE +} + /** * Extracts resource descriptors from a tool execution result when the tool * performed a deletion. Returns one or more deleted resources for tools that @@ -177,6 +189,9 @@ export function extractDeletedResourcesFromToolResult( params: Record | undefined, output: unknown ): ChatResource[] { + const resourceType = DELETE_CAPABLE_TOOL_RESOURCE_TYPE[toolName] + if (!resourceType) return [] + const result = asRecord(output) const data = asRecord(result.data) const args = asRecord(params?.args) @@ -186,7 +201,9 @@ export function extractDeletedResourcesFromToolResult( case 'delete_workflow': { const workflowId = (result.workflowId as string) ?? (params?.workflowId as string) if (workflowId && result.deleted) { - return [{ type: 'workflow', id: workflowId, title: (result.name as string) || 'Workflow' }] + return [ + { type: resourceType, id: workflowId, title: (result.name as string) || 'Workflow' }, + ] } return [] } @@ -195,7 +212,7 @@ export function extractDeletedResourcesFromToolResult( if (operation !== 'delete') return [] const fileId = (data.id as string) ?? (args.fileId as string) if (fileId) { - return [{ type: 'file', id: fileId, title: (data.name as string) || 'File' }] + return [{ type: resourceType, id: fileId, title: (data.name as string) || 'File' }] } return [] } @@ -204,7 +221,7 @@ export function extractDeletedResourcesFromToolResult( if (operation !== 'delete') return [] const tableId = (args.tableId as string) ?? (params?.tableId as string) if (tableId) { - return [{ type: 'table', id: tableId, title: 'Table' }] + return [{ type: resourceType, id: tableId, title: 'Table' }] } return [] } @@ -213,9 +230,7 @@ export function extractDeletedResourcesFromToolResult( if (operation !== 'delete') return [] const kbId = (data.id as string) ?? (args.knowledgeBaseId as string) if (kbId) { - return [ - { type: 'knowledgebase', id: kbId, title: (data.name as string) || 'Knowledge Base' }, - ] + return [{ type: resourceType, id: kbId, title: (data.name as string) || 'Knowledge Base' }] } return [] } @@ -225,17 +240,6 @@ export function extractDeletedResourcesFromToolResult( } } -const DELETE_TOOL_NAMES = new Set([ - 'delete_workflow', - 'workspace_file', - 'user_table', - 'knowledge_base', -]) - -export function isDeleteToolName(toolName: string): boolean { - return DELETE_TOOL_NAMES.has(toolName) -} - /** * Appends resources to a chat's JSONB resources column, deduplicating by type+id. * Updates the title of existing resources if the new title is more specific. 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 6cab39f13e0..f308881c320 100644 --- a/apps/sim/lib/copilot/tools/server/table/user-table.ts +++ b/apps/sim/lib/copilot/tools/server/table/user-table.ts @@ -279,6 +279,17 @@ export const userTableServerTool: BaseServerTool if (!args.tableId) { return { success: false, message: 'Table ID is required' } } + if (!workspaceId) { + return { success: false, message: 'Workspace ID is required' } + } + + const table = await getTableById(args.tableId) + if (!table) { + return { success: false, message: `Table not found: ${args.tableId}` } + } + if (table.workspaceId !== workspaceId) { + return { success: false, message: 'Table not found' } + } const requestId = crypto.randomUUID().slice(0, 8) await deleteTable(args.tableId, requestId) From 3f408e224ce3ed00a8bb2dcb995100e1d3b22887 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 14:15:38 -0700 Subject: [PATCH 6/7] Skip resource_added event when resource is deleted --- .../sse/handlers/tool-execution.ts | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts index 39cac5c9943..8028768830d 100644 --- a/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts +++ b/apps/sim/lib/copilot/orchestrator/sse/handlers/tool-execution.ts @@ -518,28 +518,7 @@ export async function executeToolAndReport( await options?.onEvent?.(resultEvent) if (result.success && execContext.chatId) { - const resources = - result.resources && result.resources.length > 0 - ? result.resources - : isResourceToolName(toolCall.name) - ? extractResourcesFromToolResult(toolCall.name, toolCall.params, result.output) - : [] - - if (resources.length > 0) { - persistChatResources(execContext.chatId, resources).catch((err) => { - logger.warn('Failed to persist chat resources', { - chatId: execContext.chatId, - error: err instanceof Error ? err.message : String(err), - }) - }) - - for (const resource of resources) { - await options?.onEvent?.({ - type: 'resource_added', - resource: { type: resource.type, id: resource.id, title: resource.title }, - }) - } - } + let isDeleteOp = false if (hasDeleteCapability(toolCall.name)) { const deleted = extractDeletedResourcesFromToolResult( @@ -548,6 +527,7 @@ export async function executeToolAndReport( result.output ) if (deleted.length > 0) { + isDeleteOp = true removeChatResources(execContext.chatId, deleted).catch((err) => { logger.warn('Failed to remove chat resources after deletion', { chatId: execContext.chatId, @@ -563,6 +543,31 @@ export async function executeToolAndReport( } } } + + if (!isDeleteOp) { + const resources = + result.resources && result.resources.length > 0 + ? result.resources + : isResourceToolName(toolCall.name) + ? extractResourcesFromToolResult(toolCall.name, toolCall.params, result.output) + : [] + + if (resources.length > 0) { + persistChatResources(execContext.chatId, resources).catch((err) => { + logger.warn('Failed to persist chat resources', { + chatId: execContext.chatId, + error: err instanceof Error ? err.message : String(err), + }) + }) + + for (const resource of resources) { + await options?.onEvent?.({ + type: 'resource_added', + resource: { type: resource.type, id: resource.id, title: resource.title }, + }) + } + } + } } } catch (error) { toolCall.status = 'error' From a21f2ff2d629592a0bc0334e5aaba73074ca1547 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Fri, 13 Mar 2026 14:26:12 -0700 Subject: [PATCH 7/7] Pass workflow id as context --- apps/sim/lib/copilot/process-contents.ts | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/sim/lib/copilot/process-contents.ts b/apps/sim/lib/copilot/process-contents.ts index 112fb68ae25..7fc57a2d116 100644 --- a/apps/sim/lib/copilot/process-contents.ts +++ b/apps/sim/lib/copilot/process-contents.ts @@ -168,8 +168,8 @@ export async function processContextsServer( if (!result) return null return { type: 'table', tag: ctx.label ? `@${ctx.label}` : '@', content: result.content } } - if (ctx.kind === 'file' && ctx.fileId && workspaceId) { - const result = await resolveFileResource(ctx.fileId, workspaceId) + if (ctx.kind === 'file' && ctx.fileId && currentWorkspaceId) { + const result = await resolveFileResource(ctx.fileId, currentWorkspaceId) if (!result) return null return { type: 'file', tag: ctx.label ? `@${ctx.label}` : '@', content: result.content } } @@ -314,6 +314,8 @@ async function processWorkflowFromDb( currentWorkspaceId?: string ): Promise { try { + let workflowName: string | undefined + if (userId) { const authorization = await authorizeWorkflowByWorkspacePermission({ workflowId, @@ -326,6 +328,7 @@ async function processWorkflowFromDb( if (currentWorkspaceId && authorization.workflow?.workspaceId !== currentWorkspaceId) { return null } + workflowName = authorization.workflow?.name ?? undefined } const normalized = await loadWorkflowFromNormalizedTables(workflowId) @@ -333,21 +336,32 @@ async function processWorkflowFromDb( logger.warn('No normalized workflow data found', { workflowId }) return null } + + if (!workflowName) { + const record = await getActiveWorkflowRecord(workflowId) + workflowName = record?.name ?? undefined + } + const workflowState = { blocks: normalized.blocks || {}, edges: normalized.edges || [], loops: normalized.loops || {}, parallels: normalized.parallels || {}, } - // Sanitize workflow state for copilot (remove UI-specific data like positions) const sanitizedState = sanitizeForCopilot(workflowState) - // Match get-user-workflow format: just the workflow state JSON - const content = JSON.stringify(sanitizedState, null, 2) + const content = JSON.stringify( + { + workflowId, + workflowName: workflowName || undefined, + state: sanitizedState, + }, + null, + 2 + ) logger.info('Processed sanitized workflow context', { workflowId, blocks: Object.keys(sanitizedState.blocks || {}).length, }) - // Use the provided kind for the type return { type: kind, tag, content } } catch (error) { logger.error('Error processing workflow context', { workflowId, error })