Skip to content

Commit 5d57faf

Browse files
waleedlatif1claude
andauthored
fix(mothership): insert copilot-created workflows at top of list (#3537)
* feat(mothership): remove resource-level delete tools from copilot Remove delete operations for workflows, folders, tables, and files from the mothership copilot to prevent destructive actions via AI. Row-level and column-level deletes are preserved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(mothership): insert copilot-created workflows at top of list * fix(mothership): server-side top-insertion sort order and deduplicate registry logic * fix(mothership): include folder sort orders when computing top-insertion position * fix(mothership): use getNextWorkflowColor instead of hardcoded color --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0aeb860 commit 5d57faf

File tree

2 files changed

+62
-45
lines changed

2 files changed

+62
-45
lines changed

apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
55
import { executeRunToolOnClient } from '@/lib/copilot/client-sse/run-tool-execution'
66
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
77
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
8+
import { getNextWorkflowColor } from '@/lib/workflows/colors'
89
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
910
import { tableKeys, useTablesList } from '@/hooks/queries/tables'
1011
import {
@@ -16,8 +17,10 @@ import {
1617
taskKeys,
1718
useChatHistory,
1819
} from '@/hooks/queries/tasks'
20+
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
1921
import { useWorkflows, workflowKeys } from '@/hooks/queries/workflows'
2022
import { useWorkspaceFiles, workspaceFilesKeys } from '@/hooks/queries/workspace-files'
23+
import { useFolderStore } from '@/stores/folders/store'
2124
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
2225
import type { FileAttachmentForApi } from '../components/user-input/user-input'
2326
import type {
@@ -197,6 +200,34 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
197200
return typeof payload.data === 'object' ? payload.data : undefined
198201
}
199202

203+
/** Adds a workflow to the registry with a top-insertion sort order if it doesn't already exist. */
204+
function ensureWorkflowInRegistry(resourceId: string, title: string, workspaceId: string): boolean {
205+
const registry = useWorkflowRegistry.getState()
206+
if (registry.workflows[resourceId]) return false
207+
const sortOrder = getTopInsertionSortOrder(
208+
registry.workflows,
209+
useFolderStore.getState().folders,
210+
workspaceId,
211+
null
212+
)
213+
useWorkflowRegistry.setState((state) => ({
214+
workflows: {
215+
...state.workflows,
216+
[resourceId]: {
217+
id: resourceId,
218+
name: title,
219+
lastModified: new Date(),
220+
createdAt: new Date(),
221+
color: getNextWorkflowColor(),
222+
workspaceId,
223+
folderId: null,
224+
sortOrder,
225+
},
226+
},
227+
}))
228+
return true
229+
}
230+
200231
export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn {
201232
const pathname = usePathname()
202233
const queryClient = useQueryClient()
@@ -349,24 +380,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
349380

350381
for (const resource of restored) {
351382
if (resource.type !== 'workflow') continue
352-
const registry = useWorkflowRegistry.getState()
353-
if (!registry.workflows[resource.id]) {
354-
useWorkflowRegistry.setState((state) => ({
355-
workflows: {
356-
...state.workflows,
357-
[resource.id]: {
358-
id: resource.id,
359-
name: resource.title,
360-
lastModified: new Date(),
361-
createdAt: new Date(),
362-
color: '#7F2FFF',
363-
workspaceId,
364-
folderId: null,
365-
sortOrder: 0,
366-
},
367-
},
368-
}))
369-
}
383+
ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)
370384
}
371385
}
372386
}, [chatHistory, workspaceId])
@@ -649,28 +663,12 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
649663
resource = extractWorkflowResource(parsed, lastWorkflowId, storedArgs)
650664
if (resource) {
651665
lastWorkflowId = resource.id
652-
queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
653-
const registry = useWorkflowRegistry.getState()
654-
if (!registry.workflows[resource.id]) {
655-
useWorkflowRegistry.setState((state) => ({
656-
workflows: {
657-
...state.workflows,
658-
[resource!.id]: {
659-
id: resource!.id,
660-
name: resource!.title,
661-
lastModified: new Date(),
662-
createdAt: new Date(),
663-
color: '#7F2FFF',
664-
workspaceId,
665-
folderId: null,
666-
sortOrder: 0,
667-
},
668-
},
669-
}))
670-
registry.setActiveWorkflow(resource.id)
666+
if (ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)) {
667+
useWorkflowRegistry.getState().setActiveWorkflow(resource.id)
671668
} else {
672-
registry.loadWorkflowState(resource.id)
669+
useWorkflowRegistry.getState().loadWorkflowState(resource.id)
673670
}
671+
queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
674672
}
675673
} else if (toolName === 'knowledge_base') {
676674
resource = extractKnowledgeBaseResource(parsed, storedArgs)

apps/sim/lib/workflows/utils.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import crypto from 'crypto'
22
import { db } from '@sim/db'
33
import { permissions, userStats, workflowFolder, workflow as workflowTable } from '@sim/db/schema'
44
import { createLogger } from '@sim/logger'
5-
import { and, asc, eq, inArray, isNull, max } from 'drizzle-orm'
5+
import { and, asc, eq, inArray, isNull, max, min } from 'drizzle-orm'
66
import { NextResponse } from 'next/server'
77
import { getSession } from '@/lib/auth'
88
import { getNextWorkflowColor } from '@/lib/workflows/colors'
@@ -353,14 +353,33 @@ export async function createWorkflowRecord(params: CreateWorkflowInput) {
353353
const workflowId = crypto.randomUUID()
354354
const now = new Date()
355355

356-
const folderCondition = folderId
356+
const workflowParentCondition = folderId
357357
? eq(workflowTable.folderId, folderId)
358358
: isNull(workflowTable.folderId)
359-
const [maxResult] = await db
360-
.select({ maxOrder: max(workflowTable.sortOrder) })
361-
.from(workflowTable)
362-
.where(and(eq(workflowTable.workspaceId, workspaceId), folderCondition))
363-
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
359+
const folderParentCondition = folderId
360+
? eq(workflowFolder.parentId, folderId)
361+
: isNull(workflowFolder.parentId)
362+
363+
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
364+
db
365+
.select({ minOrder: min(workflowTable.sortOrder) })
366+
.from(workflowTable)
367+
.where(and(eq(workflowTable.workspaceId, workspaceId), workflowParentCondition)),
368+
db
369+
.select({ minOrder: min(workflowFolder.sortOrder) })
370+
.from(workflowFolder)
371+
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
372+
])
373+
374+
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
375+
number | null
376+
>((currentMin, candidate) => {
377+
if (candidate == null) return currentMin
378+
if (currentMin == null) return candidate
379+
return Math.min(currentMin, candidate)
380+
}, null)
381+
382+
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
364383

365384
await db.insert(workflowTable).values({
366385
id: workflowId,

0 commit comments

Comments
 (0)