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>
This commit is contained in:
Waleed
2026-03-12 12:46:50 -07:00
committed by GitHub
parent 0aeb860f6e
commit 5d57faf050
2 changed files with 62 additions and 45 deletions

View File

@@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation'
import { executeRunToolOnClient } from '@/lib/copilot/client-sse/run-tool-execution'
import { MOTHERSHIP_CHAT_API_PATH } from '@/lib/copilot/constants'
import { isWorkflowToolName } from '@/lib/copilot/workflow-tools'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import { tableKeys, useTablesList } from '@/hooks/queries/tables'
import {
@@ -16,8 +17,10 @@ import {
taskKeys,
useChatHistory,
} from '@/hooks/queries/tasks'
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
import { useWorkflows, workflowKeys } from '@/hooks/queries/workflows'
import { useWorkspaceFiles, workspaceFilesKeys } from '@/hooks/queries/workspace-files'
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { FileAttachmentForApi } from '../components/user-input/user-input'
import type {
@@ -197,6 +200,34 @@ function getPayloadData(payload: SSEPayload): SSEPayloadData | undefined {
return typeof payload.data === 'object' ? payload.data : undefined
}
/** Adds a workflow to the registry with a top-insertion sort order if it doesn't already exist. */
function ensureWorkflowInRegistry(resourceId: string, title: string, workspaceId: string): boolean {
const registry = useWorkflowRegistry.getState()
if (registry.workflows[resourceId]) return false
const sortOrder = getTopInsertionSortOrder(
registry.workflows,
useFolderStore.getState().folders,
workspaceId,
null
)
useWorkflowRegistry.setState((state) => ({
workflows: {
...state.workflows,
[resourceId]: {
id: resourceId,
name: title,
lastModified: new Date(),
createdAt: new Date(),
color: getNextWorkflowColor(),
workspaceId,
folderId: null,
sortOrder,
},
},
}))
return true
}
export function useChat(workspaceId: string, initialChatId?: string): UseChatReturn {
const pathname = usePathname()
const queryClient = useQueryClient()
@@ -349,24 +380,7 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
for (const resource of restored) {
if (resource.type !== 'workflow') continue
const registry = useWorkflowRegistry.getState()
if (!registry.workflows[resource.id]) {
useWorkflowRegistry.setState((state) => ({
workflows: {
...state.workflows,
[resource.id]: {
id: resource.id,
name: resource.title,
lastModified: new Date(),
createdAt: new Date(),
color: '#7F2FFF',
workspaceId,
folderId: null,
sortOrder: 0,
},
},
}))
}
ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)
}
}
}, [chatHistory, workspaceId])
@@ -649,28 +663,12 @@ export function useChat(workspaceId: string, initialChatId?: string): UseChatRet
resource = extractWorkflowResource(parsed, lastWorkflowId, storedArgs)
if (resource) {
lastWorkflowId = resource.id
queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
const registry = useWorkflowRegistry.getState()
if (!registry.workflows[resource.id]) {
useWorkflowRegistry.setState((state) => ({
workflows: {
...state.workflows,
[resource!.id]: {
id: resource!.id,
name: resource!.title,
lastModified: new Date(),
createdAt: new Date(),
color: '#7F2FFF',
workspaceId,
folderId: null,
sortOrder: 0,
},
},
}))
registry.setActiveWorkflow(resource.id)
if (ensureWorkflowInRegistry(resource.id, resource.title, workspaceId)) {
useWorkflowRegistry.getState().setActiveWorkflow(resource.id)
} else {
registry.loadWorkflowState(resource.id)
useWorkflowRegistry.getState().loadWorkflowState(resource.id)
}
queryClient.invalidateQueries({ queryKey: workflowKeys.list(workspaceId) })
}
} else if (toolName === 'knowledge_base') {
resource = extractKnowledgeBaseResource(parsed, storedArgs)

View File

@@ -2,7 +2,7 @@ import crypto from 'crypto'
import { db } from '@sim/db'
import { permissions, userStats, workflowFolder, workflow as workflowTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { and, asc, eq, inArray, isNull, max } from 'drizzle-orm'
import { and, asc, eq, inArray, isNull, max, min } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
@@ -353,14 +353,33 @@ export async function createWorkflowRecord(params: CreateWorkflowInput) {
const workflowId = crypto.randomUUID()
const now = new Date()
const folderCondition = folderId
const workflowParentCondition = folderId
? eq(workflowTable.folderId, folderId)
: isNull(workflowTable.folderId)
const [maxResult] = await db
.select({ maxOrder: max(workflowTable.sortOrder) })
.from(workflowTable)
.where(and(eq(workflowTable.workspaceId, workspaceId), folderCondition))
const sortOrder = (maxResult?.maxOrder ?? 0) + 1
const folderParentCondition = folderId
? eq(workflowFolder.parentId, folderId)
: isNull(workflowFolder.parentId)
const [[workflowMinResult], [folderMinResult]] = await Promise.all([
db
.select({ minOrder: min(workflowTable.sortOrder) })
.from(workflowTable)
.where(and(eq(workflowTable.workspaceId, workspaceId), workflowParentCondition)),
db
.select({ minOrder: min(workflowFolder.sortOrder) })
.from(workflowFolder)
.where(and(eq(workflowFolder.workspaceId, workspaceId), folderParentCondition)),
])
const minSortOrder = [workflowMinResult?.minOrder, folderMinResult?.minOrder].reduce<
number | null
>((currentMin, candidate) => {
if (candidate == null) return currentMin
if (currentMin == null) return candidate
return Math.min(currentMin, candidate)
}, null)
const sortOrder = minSortOrder != null ? minSortOrder - 1 : 0
await db.insert(workflowTable).values({
id: workflowId,