mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user