diff --git a/apps/sim/app/(landing)/components/features/components/features-preview.tsx b/apps/sim/app/(landing)/components/features/components/features-preview.tsx
index e9a69ae8f9f..e485396a7e6 100644
--- a/apps/sim/app/(landing)/components/features/components/features-preview.tsx
+++ b/apps/sim/app/(landing)/components/features/components/features-preview.tsx
@@ -18,6 +18,7 @@ import {
xAIIcon,
} from '@/components/icons'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
interface FeaturesPreviewProps {
activeTab: number
@@ -383,7 +384,7 @@ function MiniCardIcon({ variant, color }: { variant: CardVariant; color?: string
className='h-[7px] w-[7px] flex-shrink-0 rounded-[1.5px] border'
style={{
backgroundColor: c,
- borderColor: `${c}60`,
+ borderColor: workflowBorderColor(c),
backgroundClip: 'padding-box',
}}
/>
@@ -470,7 +471,7 @@ function WorkflowCardBody({ color }: { color: string }) {
className='absolute top-2.5 left-[40px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
@@ -481,7 +482,7 @@ function WorkflowCardBody({ color }: { color: string }) {
className='absolute top-[36px] left-[68px] h-[14px] w-[14px] rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
opacity: 0.5,
}}
@@ -896,7 +897,7 @@ function MockLogDetailsSidebar({ selectedRow, onPrev, onNext }: MockLogDetailsSi
className='h-[10px] w-[10px] shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx
index 2070fab6468..6b691bfcd3f 100644
--- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx
+++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-logs/landing-preview-logs.tsx
@@ -5,6 +5,7 @@ import { Download } from 'lucide-react'
import { ArrowUpDown, Badge, Library, ListFilter, Search } from '@/components/emcn'
import type { BadgeProps } from '@/components/emcn/components/badge/badge'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
interface LogRow {
id: string
@@ -283,7 +284,7 @@ export function LandingPreviewLogs() {
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: log.workflowColor,
- borderColor: `${log.workflowColor}60`,
+ borderColor: workflowBorderColor(log.workflowColor),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx
index 1b754f65f36..d7707aa0c8d 100644
--- a/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx
+++ b/apps/sim/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar.tsx
@@ -11,6 +11,7 @@ import {
Table,
} from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import type { PreviewWorkflow } from '@/app/(landing)/components/landing-preview/components/landing-preview-workflow/workflow-data'
export type SidebarView =
@@ -211,7 +212,7 @@ export function LandingPreviewSidebar({
className='h-[14px] w-[14px] flex-shrink-0 rounded-[4px] border-[2.5px]'
style={{
backgroundColor: workflow.color,
- borderColor: `${workflow.color}60`,
+ borderColor: workflowBorderColor(workflow.color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/context-mention-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/context-mention-icon.tsx
new file mode 100644
index 00000000000..1e7a2514620
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/context-mention-icon.tsx
@@ -0,0 +1,45 @@
+import { Blimp, Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons'
+import { getDocumentIcon } from '@/components/icons/document-icons'
+import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
+import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
+
+interface ContextMentionIconProps {
+ context: ChatMessageContext
+ /** Only used when context.kind is 'workflow' or 'current_workflow'; ignored otherwise. */
+ workflowColor?: string | null
+ /** Applied to every icon element. Include sizing and positional classes (e.g. h-[12px] w-[12px]). */
+ className: string
+}
+
+/** Renders the icon for a context mention chip. Returns null when no icon applies. */
+export function ContextMentionIcon({ context, workflowColor, className }: ContextMentionIconProps) {
+ switch (context.kind) {
+ case 'workflow':
+ case 'current_workflow':
+ return workflowColor ? (
+
+ ) : null
+ case 'knowledge':
+ return
+ case 'table':
+ return
+ case 'file': {
+ const FileDocIcon = getDocumentIcon('', context.label)
+ return
+ }
+ case 'folder':
+ return
+ case 'past_chat':
+ return
+ default:
+ return null
+ }
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts
index 209ca78170d..38debef7f14 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/index.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/index.ts
@@ -1,4 +1,5 @@
export { ChatMessageAttachments } from './chat-message-attachments'
+export { ContextMentionIcon } from './context-mention-icon'
export {
assistantMessageHasRenderableContent,
MessageContent,
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
index 36e7d1348e6..fe8510c1990 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx
@@ -37,6 +37,7 @@ interface MothershipChatProps {
userId?: string
chatId?: string
onContextAdd?: (context: ChatContext) => void
+ onContextRemove?: (context: ChatContext) => void
editValue?: string
onEditValueConsumed?: () => void
layout?: 'mothership-view' | 'copilot-view'
@@ -83,6 +84,7 @@ export function MothershipChat({
userId,
chatId,
onContextAdd,
+ onContextRemove,
editValue,
onEditValueConsumed,
layout = 'mothership-view',
@@ -207,6 +209,7 @@ export function MothershipChat({
isInitialView={false}
userId={userId}
onContextAdd={onContextAdd}
+ onContextRemove={onContextRemove}
editValue={editValue}
onEditValueConsumed={onEditValueConsumed}
onEnterWhileEmpty={handleEnterWhileEmpty}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx
index 02bdf37822c..7345277cf6a 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown/add-resource-dropdown.tsx
@@ -27,6 +27,7 @@ import type {
import { useFolders } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import { useTablesList } from '@/hooks/queries/tables'
+import { useTasks } from '@/hooks/queries/tasks'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
@@ -53,6 +54,7 @@ export function useAvailableResources(
const { data: files = [] } = useWorkspaceFiles(workspaceId)
const { data: knowledgeBases } = useKnowledgeBasesQuery(workspaceId)
const { data: folders = [] } = useFolders(workspaceId)
+ const { data: tasks = [] } = useTasks(workspaceId)
return useMemo(
() => [
@@ -97,8 +99,16 @@ export function useAvailableResources(
isOpen: existingKeys.has(`knowledgebase:${kb.id}`),
})),
},
+ {
+ type: 'task' as const,
+ items: tasks.map((t) => ({
+ id: t.id,
+ name: t.name,
+ isOpen: existingKeys.has(`task:${t.id}`),
+ })),
+ },
],
- [workflows, folders, tables, files, knowledgeBases, existingKeys]
+ [workflows, folders, tables, files, knowledgeBases, tasks, existingKeys]
)
}
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 e9fb56844ed..ab2655ed48d 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
@@ -22,6 +22,7 @@ import {
getFileExtension,
getMimeTypeFromExtension,
} from '@/lib/uploads/utils/file-utils'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import {
FileViewer,
type PreviewMode,
@@ -514,7 +515,7 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) {
className='h-[12px] w-[12px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: w.color,
- borderColor: `${w.color}60`,
+ borderColor: workflowBorderColor(w.color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
index e10c31b9a61..59628a41237 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx
@@ -4,6 +4,7 @@ import { type ElementType, type ReactNode, useMemo } from 'react'
import type { QueryClient } from '@tanstack/react-query'
import { useParams } from 'next/navigation'
import {
+ Blimp,
Database,
File as FileIcon,
Folder as FolderIcon,
@@ -13,12 +14,14 @@ import {
import { WorkflowIcon } from '@/components/icons'
import { getDocumentIcon } from '@/components/icons/document-icons'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import type {
MothershipResource,
MothershipResourceType,
} from '@/app/workspace/[workspaceId]/home/types'
import { knowledgeKeys } from '@/hooks/queries/kb/knowledge'
import { tableKeys } from '@/hooks/queries/tables'
+import { taskKeys } from '@/hooks/queries/tasks'
import { folderKeys } from '@/hooks/queries/utils/folder-keys'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -48,7 +51,7 @@ function WorkflowTabSquare({ workflowId, className }: { workflowId: string; clas
className={cn('flex-shrink-0 rounded-[3px] border-[2px]', className)}
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
@@ -63,7 +66,7 @@ function WorkflowDropdownItem({ item }: DropdownItemRenderProps) {
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
@@ -151,6 +154,15 @@ export const RESOURCE_REGISTRY: Record ,
},
+ task: {
+ type: 'task',
+ label: 'Tasks',
+ icon: Blimp,
+ renderTabIcon: (_resource, className) => (
+
+ ),
+ renderDropdownItem: (props) => ,
+ },
} as const
export const RESOURCE_TYPES = Object.values(RESOURCE_REGISTRY)
@@ -185,6 +197,9 @@ const RESOURCE_INVALIDATORS: Record<
folder: (qc) => {
qc.invalidateQueries({ queryKey: folderKeys.lists() })
},
+ task: (qc, wId) => {
+ qc.invalidateQueries({ queryKey: taskKeys.list(wId) })
+ },
}
/**
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 a3ef9628d03..2809070fde2 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
@@ -10,6 +10,7 @@ import {
import { Button, Tooltip } from '@/components/emcn'
import { Columns3, Eye, PanelLeft, Pencil } from '@/components/emcn/icons'
import { isEphemeralResource } from '@/lib/copilot/resource-extraction'
+import { SIM_RESOURCE_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { cn } from '@/lib/core/utils/cn'
import type { PreviewMode } from '@/app/workspace/[workspaceId]/files/components/file-viewer'
import { AddResourceDropdown } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
@@ -164,7 +165,7 @@ export function ResourceTabs({
const resource = resources[idx]
if (resource) {
e.dataTransfer.setData(
- 'application/x-sim-resource',
+ SIM_RESOURCE_DRAG_TYPE,
JSON.stringify({ type: resource.type, id: resource.id, title: resource.title })
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
index d619aed9102..8c2516d5181 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts
@@ -89,6 +89,8 @@ export function mapResourceToContext(resource: MothershipResource): ChatContext
return { kind: 'file', fileId: resource.id, label: resource.title }
case 'folder':
return { kind: 'folder', folderId: resource.id, label: resource.title }
+ case 'task':
+ return { kind: 'past_chat', chatId: resource.id, label: resource.title }
default:
return { kind: 'docs', label: resource.title }
}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx
index 42882beafa0..d5656d4cf23 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/plus-menu-dropdown.tsx
@@ -81,7 +81,7 @@ export const PlusMenuDropdown = React.memo(
e.preventDefault()
const firstItem = contentRef.current?.querySelector('[role="menuitem"]')
firstItem?.focus()
- } else if (e.key === 'Enter') {
+ } else if (e.key === 'Enter' || e.key === 'Tab') {
e.preventDefault()
const first = filteredItemsRef.current?.[0]
if (first) handleSelect({ type: first.type, id: first.item.id, title: first.item.name })
@@ -99,6 +99,12 @@ export const PlusMenuDropdown = React.memo(
e.preventDefault()
searchRef.current?.focus()
}
+ } else if (e.key === 'Tab') {
+ const focused = document.activeElement as HTMLElement | null
+ if (focused?.getAttribute('role') === 'menuitem') {
+ e.preventDefault()
+ focused.click()
+ }
}
}, [])
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 d2415318b31..dc5969881c6 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
@@ -3,11 +3,11 @@
import type React from 'react'
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'next/navigation'
-import { Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons'
-import { getDocumentIcon } from '@/components/icons/document-icons'
import { useSession } from '@/lib/auth/auth-client'
+import { SIM_RESOURCE_DRAG_TYPE, SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { cn } from '@/lib/core/utils/cn'
import { CHAT_ACCEPT_ATTRIBUTE } from '@/lib/uploads/utils/validation'
+import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown'
import type {
PlusMenuHandle,
@@ -108,6 +108,7 @@ interface UserInputProps {
isInitialView?: boolean
userId?: string
onContextAdd?: (context: ChatContext) => void
+ onContextRemove?: (context: ChatContext) => void
onEnterWhileEmpty?: () => boolean
}
@@ -121,6 +122,7 @@ export function UserInput({
isInitialView = true,
userId,
onContextAdd,
+ onContextRemove,
onEnterWhileEmpty,
}: UserInputProps) {
const { workspaceId } = useParams<{ workspaceId: string }>()
@@ -170,6 +172,37 @@ export function UserInput({
[addContext, onContextAdd]
)
+ const onContextRemoveRef = useRef(onContextRemove)
+ onContextRemoveRef.current = onContextRemove
+
+ const prevSelectedContextsRef = useRef([])
+ useEffect(() => {
+ const prev = prevSelectedContextsRef.current
+ const curr = contextManagement.selectedContexts
+ const contextId = (ctx: ChatContext): string => {
+ switch (ctx.kind) {
+ case 'workflow':
+ case 'current_workflow':
+ return `${ctx.kind}:${ctx.workflowId}`
+ case 'knowledge':
+ return `knowledge:${ctx.knowledgeId ?? ''}`
+ case 'table':
+ return `table:${ctx.tableId}`
+ case 'file':
+ return `file:${ctx.fileId}`
+ case 'folder':
+ return `folder:${ctx.folderId}`
+ case 'past_chat':
+ return `past_chat:${ctx.chatId}`
+ default:
+ return `${ctx.kind}:${ctx.label}`
+ }
+ }
+ const removed = prev.filter((p) => !curr.some((c) => contextId(c) === contextId(p)))
+ if (removed.length > 0) removed.forEach((ctx) => onContextRemoveRef.current?.(ctx))
+ prevSelectedContextsRef.current = curr
+ }, [contextManagement.selectedContexts])
+
const existingResourceKeys = useMemo(() => {
const keys = new Set()
for (const ctx of contextManagement.selectedContexts) {
@@ -178,6 +211,7 @@ export function UserInput({
if (ctx.kind === 'table' && ctx.tableId) keys.add(`table:${ctx.tableId}`)
if (ctx.kind === 'file' && ctx.fileId) keys.add(`file:${ctx.fileId}`)
if (ctx.kind === 'folder' && ctx.folderId) keys.add(`folder:${ctx.folderId}`)
+ if (ctx.kind === 'past_chat' && ctx.chatId) keys.add(`task:${ctx.chatId}`)
}
return keys
}, [contextManagement.selectedContexts])
@@ -247,15 +281,17 @@ export function UserInput({
if (textarea) {
const currentValue = valueRef.current
const insertAt = atInsertPosRef.current ?? textarea.selectionStart ?? currentValue.length
- atInsertPosRef.current = null
-
const needsSpaceBefore = insertAt > 0 && !/\s/.test(currentValue.charAt(insertAt - 1))
const insertText = `${needsSpaceBefore ? ' ' : ''}@${resource.title} `
const before = currentValue.slice(0, insertAt)
const after = currentValue.slice(insertAt)
+ const newValue = `${before}${insertText}${after}`
const newPos = before.length + insertText.length
pendingCursorRef.current = newPos
- setValue(`${before}${insertText}${after}`)
+ // Eagerly sync refs so successive drop-handler iterations see the updated position
+ valueRef.current = newValue
+ atInsertPosRef.current = newPos
+ setValue(newValue)
}
const context = mapResourceToContext(resource)
@@ -281,7 +317,10 @@ export function UserInput({
}, [])
const handleContainerDragOver = useCallback((e: React.DragEvent) => {
- if (e.dataTransfer.types.includes('application/x-sim-resource')) {
+ if (
+ e.dataTransfer.types.includes(SIM_RESOURCE_DRAG_TYPE) ||
+ e.dataTransfer.types.includes(SIM_RESOURCES_DRAG_TYPE)
+ ) {
e.preventDefault()
e.stopPropagation()
e.dataTransfer.dropEffect = 'copy'
@@ -292,13 +331,30 @@ export function UserInput({
const handleContainerDrop = useCallback(
(e: React.DragEvent) => {
- const resourceJson = e.dataTransfer.getData('application/x-sim-resource')
+ const resourcesJson = e.dataTransfer.getData(SIM_RESOURCES_DRAG_TYPE)
+ if (resourcesJson) {
+ e.preventDefault()
+ e.stopPropagation()
+ try {
+ const resources = JSON.parse(resourcesJson) as MothershipResource[]
+ for (const resource of resources) {
+ handleResourceSelect(resource)
+ }
+ // Reset after batch so the next non-drop insert uses the cursor position
+ atInsertPosRef.current = null
+ } catch {
+ // Invalid JSON — ignore
+ }
+ return
+ }
+ const resourceJson = e.dataTransfer.getData(SIM_RESOURCE_DRAG_TYPE)
if (resourceJson) {
e.preventDefault()
e.stopPropagation()
try {
const resource = JSON.parse(resourceJson) as MothershipResource
handleResourceSelect(resource)
+ atInsertPosRef.current = null
} catch {
// Invalid JSON — ignore
}
@@ -310,11 +366,17 @@ export function UserInput({
)
const handleDragEnter = useCallback((e: React.DragEvent) => {
- filesRef.current.handleDragEnter(e)
+ const isResourceDrag =
+ e.dataTransfer.types.includes(SIM_RESOURCE_DRAG_TYPE) ||
+ e.dataTransfer.types.includes(SIM_RESOURCES_DRAG_TYPE)
+ if (!isResourceDrag) filesRef.current.handleDragEnter(e)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
- filesRef.current.handleDragLeave(e)
+ const isResourceDrag =
+ e.dataTransfer.types.includes(SIM_RESOURCE_DRAG_TYPE) ||
+ e.dataTransfer.types.includes(SIM_RESOURCES_DRAG_TYPE)
+ if (!isResourceDrag) filesRef.current.handleDragLeave(e)
}, [])
const handleFileChange = useCallback((e: React.ChangeEvent) => {
@@ -643,42 +705,17 @@ export function UserInput({
: range.token
const matchingCtx = contexts.find((c) => c.label === mentionLabel)
- let mentionIconNode: React.ReactNode = null
- if (matchingCtx) {
- const iconClasses = 'absolute inset-0 m-auto h-[12px] w-[12px] text-[var(--text-icon)]'
- switch (matchingCtx.kind) {
- case 'workflow':
- case 'current_workflow': {
- const wfId = (matchingCtx as { workflowId: string }).workflowId
- const wfColor = workflowsById[wfId]?.color ?? '#888'
- mentionIconNode = (
-
- )
- break
- }
- case 'knowledge':
- mentionIconNode =
- break
- case 'table':
- mentionIconNode =
- break
- case 'file': {
- const FileDocIcon = getDocumentIcon('', mentionLabel)
- mentionIconNode =
- break
- }
- case 'folder':
- mentionIconNode =
- break
- }
- }
+ const wfId =
+ matchingCtx?.kind === 'workflow' || matchingCtx?.kind === 'current_workflow'
+ ? matchingCtx.workflowId
+ : undefined
+ const mentionIconNode = matchingCtx ? (
+
+ ) : null
elements.push(
w.id === context.workflowId)?.color ?? null
}, [workflowList, context.kind, context.workflowId])
- let icon: React.ReactNode = null
- const iconClasses = 'h-[12px] w-[12px] flex-shrink-0 text-[var(--text-icon)]'
-
- switch (context.kind) {
- case 'workflow':
- case 'current_workflow':
- icon = workflowColor ? (
-
- ) : null
- break
- case 'knowledge':
- icon =
- break
- case 'table':
- icon =
- break
- case 'file': {
- const FileDocIcon = getDocumentIcon('', context.label)
- icon =
- break
- }
- case 'folder':
- icon =
- break
- }
-
return (
- {icon && {icon}}
+
{context.label}
)
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 38367339197..132d87b2a9e 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -17,7 +17,7 @@ import { useChatHistory, useMarkTaskRead } from '@/hooks/queries/tasks'
import type { ChatContext } from '@/stores/panel'
import { MothershipChat, MothershipView, TemplatePrompts, UserInput } from './components'
import { getMothershipUseChatOptions, useChat, useMothershipResize } from './hooks'
-import type { FileAttachmentForApi, MothershipResource, MothershipResourceType } from './types'
+import type { FileAttachmentForApi, MothershipResourceType } from './types'
const logger = createLogger('Home')
@@ -261,51 +261,42 @@ export function Home({ chatId }: HomeProps = {}) {
return () => window.removeEventListener('mothership-send-message', handler)
}, [sendMessage])
- const handleContextAdd = useCallback(
- (context: ChatContext) => {
- let resourceType: MothershipResourceType | null = null
- let resourceId: string | null = null
- const resourceTitle: string = context.label
-
+ const resolveResourceFromContext = useCallback(
+ (context: ChatContext): { type: MothershipResourceType; id: string } | null => {
switch (context.kind) {
case 'workflow':
case 'current_workflow':
- resourceType = 'workflow'
- resourceId = context.workflowId
- break
+ return context.workflowId ? { type: 'workflow', id: context.workflowId } : null
case 'knowledge':
- if (context.knowledgeId) {
- resourceType = 'knowledgebase'
- resourceId = context.knowledgeId
- }
- break
+ return context.knowledgeId ? { type: 'knowledgebase', id: context.knowledgeId } : null
case 'table':
- if (context.tableId) {
- resourceType = 'table'
- resourceId = context.tableId
- }
- break
+ return context.tableId ? { type: 'table', id: context.tableId } : null
case 'file':
- if (context.fileId) {
- resourceType = 'file'
- resourceId = context.fileId
- }
- break
+ return context.fileId ? { type: 'file', id: context.fileId } : null
default:
- break
+ return null
}
+ },
+ []
+ )
- if (resourceType && resourceId) {
- const resource: MothershipResource = {
- type: resourceType,
- id: resourceId,
- title: resourceTitle,
- }
- addResource(resource)
+ const handleContextAdd = useCallback(
+ (context: ChatContext) => {
+ const resolved = resolveResourceFromContext(context)
+ if (resolved) {
+ addResource({ ...resolved, title: context.label })
handleResourceEvent()
}
},
- [addResource, handleResourceEvent]
+ [resolveResourceFromContext, addResource, handleResourceEvent]
+ )
+
+ const handleContextRemove = useCallback(
+ (context: ChatContext) => {
+ const resolved = resolveResourceFromContext(context)
+ if (resolved) removeResource(resolved.type, resolved.id)
+ },
+ [resolveResourceFromContext, removeResource]
)
const hasMessages = messages.length > 0
@@ -345,6 +336,7 @@ export function Home({ chatId }: HomeProps = {}) {
onStopGeneration={handleStopGeneration}
userId={session?.user?.id}
onContextAdd={handleContextAdd}
+ onContextRemove={handleContextRemove}
/>
@@ -375,6 +367,7 @@ export function Home({ chatId }: HomeProps = {}) {
userId={session?.user?.id}
chatId={resolvedChatId}
onContextAdd={handleContextAdd}
+ onContextRemove={handleContextRemove}
editValue={editingInputValue}
onEditValueConsumed={clearEditingValue}
animateInput={isInputEntering}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts
index d4f812cc25a..e6ae3c9f0a9 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/types.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts
@@ -6,6 +6,9 @@ export type {
MothershipResourceType,
} from '@/lib/copilot/resource-types'
+/** Union of all valid context kind strings, derived from {@link ChatContext}. */
+export type ChatContextKind = ChatContext['kind']
+
export interface FileAttachmentForApi {
id: string
key: string
@@ -260,13 +263,14 @@ export interface ChatMessageAttachment {
}
export interface ChatMessageContext {
- kind: string
+ kind: ChatContextKind
label: string
workflowId?: string
knowledgeId?: string
tableId?: string
fileId?: string
folderId?: string
+ chatId?: string
}
export interface ChatMessage {
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx
index ee52e0fa0bc..2f7b7f38cd9 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/workflows-list/workflows-list.tsx
@@ -1,6 +1,7 @@
import { memo } from 'react'
import { useParams } from 'next/navigation'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import {
DELETED_WORKFLOW_COLOR,
DELETED_WORKFLOW_LABEL,
@@ -93,7 +94,7 @@ function WorkflowsListInner({
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: workflowColor,
- borderColor: `${workflowColor}60`,
+ borderColor: workflowBorderColor(workflowColor),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
index 0289e4d9280..4307d40b5c5 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx
@@ -20,6 +20,7 @@ import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-spans'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import {
ExecutionSnapshot,
FileCards,
@@ -431,7 +432,7 @@ export const LogDetails = memo(function LogDetails({
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: c,
- borderColor: c ? `${c}60` : undefined,
+ borderColor: c ? workflowBorderColor(c) : undefined,
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx
index c99a59988fa..3d1aba93508 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-list/logs-list.tsx
@@ -8,6 +8,7 @@ import { Badge, buttonVariants } from '@/components/emcn'
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
import { cn } from '@/lib/core/utils/cn'
import { formatDuration } from '@/lib/core/utils/formatting'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import {
DELETED_WORKFLOW_COLOR,
DELETED_WORKFLOW_LABEL,
@@ -90,7 +91,7 @@ const LogRow = memo(
className='h-[10px] w-[10px] flex-shrink-0 rounded-[3px] border-[1.5px]'
style={{
backgroundColor: workflowColor,
- borderColor: `${workflowColor}60`,
+ borderColor: workflowBorderColor(workflowColor),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx
index bcaf5d3019a..518ad67654b 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/logs-toolbar/logs-toolbar.tsx
@@ -20,6 +20,7 @@ import { cn } from '@/lib/core/utils/cn'
import { hasActiveFilters } from '@/lib/logs/filters'
import { getTriggerOptions } from '@/lib/logs/get-trigger-options'
import { captureEvent } from '@/lib/posthog/client'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import { type LogStatus, STATUS_CONFIG } from '@/app/workspace/[workspaceId]/logs/utils'
import { getBlock } from '@/blocks/registry'
import { useFolderMap } from '@/hooks/queries/folders'
@@ -124,7 +125,7 @@ function getColorIcon(
width: 10,
height: 10,
...(withRing && {
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box' as const,
}),
}}
@@ -604,7 +605,7 @@ export const LogsToolbar = memo(function LogsToolbar({
className='h-[8px] w-[8px] flex-shrink-0 rounded-xs border-[1.5px]'
style={{
backgroundColor: selectedWorkflow.color,
- borderColor: `${selectedWorkflow.color}60`,
+ borderColor: workflowBorderColor(selectedWorkflow.color),
backgroundClip: 'padding-box',
}}
/>
@@ -735,7 +736,7 @@ export const LogsToolbar = memo(function LogsToolbar({
className='h-[8px] w-[8px] flex-shrink-0 rounded-xs border-[1.5px]'
style={{
backgroundColor: selectedWorkflow.color,
- borderColor: `${selectedWorkflow.color}60`,
+ borderColor: workflowBorderColor(selectedWorkflow.color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
index a939a859c1b..f8708263c76 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx
@@ -33,6 +33,7 @@ import {
type TriggerData,
type WorkflowData,
} from '@/lib/logs/search-suggestions'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import type {
FilterTag,
HeaderAction,
@@ -157,7 +158,7 @@ function getColorIcon(
width: 10,
height: 10,
...(withRing && {
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box' as const,
}),
}}
@@ -742,7 +743,7 @@ export default function Logs() {
className='h-[10px] w-[10px] rounded-[3px] border-[1.5px]'
style={{
backgroundColor: workflowColor,
- borderColor: `${workflowColor}60`,
+ borderColor: workflowBorderColor(workflowColor),
backgroundClip: 'padding-box',
}}
/>
@@ -1441,7 +1442,7 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
className='h-[8px] w-[8px] flex-shrink-0 rounded-xs border-[1.5px]'
style={{
backgroundColor: selectedWorkflow.color,
- borderColor: `${selectedWorkflow.color}60`,
+ borderColor: workflowBorderColor(selectedWorkflow.color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
index 3cd6d051ca7..48c5dbbc5fd 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/recently-deleted/recently-deleted.tsx
@@ -6,6 +6,7 @@ import { useParams, useRouter } from 'next/navigation'
import { Button, Combobox, SModalTabs, SModalTabsList, SModalTabsTrigger } from '@/components/emcn'
import { Input } from '@/components/ui'
import { formatDate } from '@/lib/core/utils/formatting'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import { RESOURCE_REGISTRY } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry'
import type { MothershipResourceType } from '@/app/workspace/[workspaceId]/home/types'
import { DeletedItemSkeleton } from '@/app/workspace/[workspaceId]/settings/components/recently-deleted/deleted-item-skeleton'
@@ -97,7 +98,7 @@ function ResourceIcon({ resource }: { resource: DeletedResource }) {
className='h-[14px] w-[14px] shrink-0 rounded-[3px] border-[2px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
index 7a64c8e0392..e5f869a8057 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx
@@ -14,6 +14,7 @@ import {
} from '@/components/emcn'
import { Pencil, SquareArrowUpRight } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import { ConversationListItem } from '@/app/workspace/[workspaceId]/components'
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import type { FolderTreeNode } from '@/stores/folders/types'
@@ -131,7 +132,7 @@ function WorkflowColorSwatch({ color }: { color: string }) {
className='h-[16px] w-[16px] flex-shrink-0 rounded-sm border-[2.5px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx
index 7cdffe5bd57..d5c81dcf55a 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/components/command-items.tsx
@@ -5,6 +5,7 @@ import { memo } from 'react'
import { Command } from 'cmdk'
import { Blimp } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import type { CommandItemProps } from '../utils'
import { COMMAND_ITEM_CLASSNAME } from '../utils'
@@ -64,7 +65,7 @@ export const MemoizedWorkflowItem = memo(
className='h-[14px] w-[14px] flex-shrink-0 rounded-sm border-[2px]'
style={{
backgroundColor: color,
- borderColor: `${color}60`,
+ borderColor: workflowBorderColor(color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
index afae818c6ac..d179568c316 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
@@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { ChevronRight, Folder, FolderOpen, MoreHorizontal } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
+import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { generateId } from '@/lib/core/utils/uuid'
import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -18,6 +19,10 @@ import {
useSidebarDragContext,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
+import {
+ buildDragResources,
+ createSidebarDragGhost,
+} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
import {
useCanDelete,
useDeleteFolder,
@@ -136,6 +141,7 @@ export function FolderItem({
})
const isEditingRef = useRef(false)
+ const dragGhostRef = useRef(null)
const handleCreateWorkflowInFolder = useCallback(() => {
const name = generateCreativeWorkflowName()
@@ -196,10 +202,24 @@ export function FolderItem({
}
e.dataTransfer.setData('sidebar-selection', JSON.stringify(selection))
- e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.effectAllowed = 'copyMove'
+
+ const resources = buildDragResources(selection, workspaceId)
+ if (resources.length > 0) {
+ e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources))
+ }
+
+ const total = selection.folderIds.length + selection.workflowIds.length
+ const ghostLabel = total > 1 ? `${folder.name} +${total - 1} more` : folder.name
+ const icon = total === 1 ? { kind: 'folder' as const } : undefined
+ const ghost = createSidebarDragGhost(ghostLabel, icon)
+ void ghost.offsetHeight
+ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
+ dragGhostRef.current = ghost
+
onDragStartProp?.()
},
- [folder.id, onDragStartProp]
+ [folder.id, folder.name, workspaceId, onDragStartProp]
)
const {
@@ -212,6 +232,10 @@ export function FolderItem({
})
const handleDragEnd = useCallback(() => {
+ if (dragGhostRef.current) {
+ dragGhostRef.current.remove()
+ dragGhostRef.current = null
+ }
handleDragEndBase()
onDragEndProp?.()
}, [handleDragEndBase, onDragEndProp])
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
index 32a58343029..3727779a43b 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
@@ -5,6 +5,8 @@ import clsx from 'clsx'
import { MoreHorizontal } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
+import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { getWorkflowLockToggleIds } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
@@ -16,6 +18,10 @@ import {
useItemRename,
useSidebarDragContext,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
+import {
+ buildDragResources,
+ createSidebarDragGhost,
+} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
import {
useCanDelete,
useDeleteSelection,
@@ -198,6 +204,7 @@ export function WorkflowItem({
}, [isActiveWorkflow, isWorkflowLocked])
const isEditingRef = useRef(false)
+ const dragGhostRef = useRef(null)
const {
isOpen: isContextMenuOpen,
@@ -337,10 +344,25 @@ export function WorkflowItem({
}
e.dataTransfer.setData('sidebar-selection', JSON.stringify(selection))
- e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.effectAllowed = 'copyMove'
+
+ const resources = buildDragResources(selection, workspaceId)
+ if (resources.length > 0) {
+ e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources))
+ }
+
+ const total = selection.workflowIds.length + selection.folderIds.length
+ const ghostLabel = total > 1 ? `${workflow.name} +${total - 1} more` : workflow.name
+ const icon = total === 1 ? { kind: 'workflow' as const, color: workflow.color } : undefined
+ const ghost = createSidebarDragGhost(ghostLabel, icon)
+ // Force reflow so the browser can capture the rendered element
+ void ghost.offsetHeight
+ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
+ dragGhostRef.current = ghost
+
onDragStartProp?.()
},
- [workflow.id, onDragStartProp]
+ [workflow.id, workflow.name, workflow.color, workspaceId, onDragStartProp]
)
const {
@@ -353,6 +375,10 @@ export function WorkflowItem({
})
const handleDragEnd = useCallback(() => {
+ if (dragGhostRef.current) {
+ dragGhostRef.current.remove()
+ dragGhostRef.current = null
+ }
handleDragEndBase()
onDragEndProp?.()
}, [handleDragEndBase, onDragEndProp])
@@ -414,7 +440,7 @@ export function WorkflowItem({
className='h-[16px] w-[16px] flex-shrink-0 rounded-sm border-[2.5px]'
style={{
backgroundColor: workflow.color,
- borderColor: `${workflow.color}60`,
+ borderColor: workflowBorderColor(workflow.color),
backgroundClip: 'padding-box',
}}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
index 02f0bca42e8..1a9dcb4765c 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -37,6 +37,7 @@ import {
Wordmark,
} from '@/components/emcn/icons'
import { useSession } from '@/lib/auth/auth-client'
+import { SIM_RESOURCES_DRAG_TYPE } from '@/lib/copilot/resource-types'
import { cn } from '@/lib/core/utils/cn'
import { isMacPlatform } from '@/lib/core/utils/platform'
import { buildFolderTree } from '@/lib/folders/tree'
@@ -72,7 +73,10 @@ import {
useWorkflowOperations,
useWorkspaceManagement,
} from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
-import { groupWorkflowsByFolder } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
+import {
+ createSidebarDragGhost,
+ groupWorkflowsByFolder,
+} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
import {
useDuplicateWorkspace,
useExportWorkspace,
@@ -159,6 +163,30 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
onMorePointerDown: () => void
onMoreClick: (e: React.MouseEvent, taskId: string) => void
}) {
+ const dragGhostRef = useRef(null)
+
+ const handleDragStart = useCallback(
+ (e: React.DragEvent) => {
+ e.dataTransfer.effectAllowed = 'copyMove'
+ e.dataTransfer.setData(
+ SIM_RESOURCES_DRAG_TYPE,
+ JSON.stringify([{ type: 'task', id: task.id, title: task.name }])
+ )
+ const ghost = createSidebarDragGhost(task.name, { kind: 'task' })
+ void ghost.offsetHeight
+ e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2)
+ dragGhostRef.current = ghost
+ },
+ [task.id, task.name]
+ )
+
+ const handleDragEnd = useCallback(() => {
+ if (dragGhostRef.current) {
+ dragGhostRef.current.remove()
+ dragGhostRef.current = null
+ }
+ }, [])
+
return (
onContextMenu(e, task.id) : undefined}
+ draggable={task.id !== 'new'}
+ onDragStart={task.id !== 'new' ? handleDragStart : undefined}
+ onDragEnd={task.id !== 'new' ? handleDragEnd : undefined}
>
{task.name}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts
index ecabf89b452..848f0771cb0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts
@@ -1,5 +1,96 @@
+import type { MothershipResource } from '@/lib/copilot/resource-types'
+import { workflowBorderColor } from '@/lib/workspaces/colors'
+import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
+import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
+/**
+ * Builds a `MothershipResource` array from a sidebar drag selection so it can
+ * be set as `application/x-sim-resources` drag data and dropped into the chat.
+ */
+export function buildDragResources(
+ selection: { workflowIds: string[]; folderIds: string[] },
+ workspaceId: string
+): MothershipResource[] {
+ const allWorkflows = getWorkflows(workspaceId)
+ const workflowMap = Object.fromEntries(allWorkflows.map((w) => [w.id, w]))
+ const folderMap = getFolderMap(workspaceId)
+ return [
+ ...selection.workflowIds.map((id) => ({
+ type: 'workflow' as const,
+ id,
+ title: workflowMap[id]?.name ?? id,
+ })),
+ ...selection.folderIds.map((id) => ({
+ type: 'folder' as const,
+ id,
+ title: folderMap[id]?.name ?? id,
+ })),
+ ]
+}
+
+export type SidebarDragGhostIcon =
+ | { kind: 'workflow'; color: string }
+ | { kind: 'folder' }
+ | { kind: 'task' }
+
+const FOLDER_SVG = ``
+
+const BLIMP_SVG = ``
+
+/**
+ * Creates a lightweight drag ghost pill showing an icon and label for the item(s) being dragged.
+ * Append to `document.body`, pass to `e.dataTransfer.setDragImage`, then remove on dragend.
+ */
+export function createSidebarDragGhost(label: string, icon?: SidebarDragGhostIcon): HTMLElement {
+ const ghost = document.createElement('div')
+ ghost.style.cssText = `
+ position: fixed;
+ top: -500px;
+ left: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ background: var(--surface-active);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 8px;
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 13px;
+ color: var(--text-body);
+ white-space: nowrap;
+ pointer-events: none;
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
+ z-index: 9999;
+ `
+
+ if (icon) {
+ if (icon.kind === 'workflow') {
+ const square = document.createElement('div')
+ square.style.cssText = `
+ width: 14px; height: 14px; flex-shrink: 0;
+ border-radius: 3px; border: 2px solid ${workflowBorderColor(icon.color)};
+ background: ${icon.color}; background-clip: padding-box;
+ `
+ ghost.appendChild(square)
+ } else {
+ const iconWrapper = document.createElement('div')
+ iconWrapper.style.cssText =
+ 'display: flex; align-items: center; flex-shrink: 0; color: var(--text-icon);'
+ iconWrapper.innerHTML = icon.kind === 'folder' ? FOLDER_SVG : BLIMP_SVG
+ ghost.appendChild(iconWrapper)
+ }
+ }
+
+ const text = document.createElement('span')
+ text.style.cssText = 'max-width: 200px; overflow: hidden; text-overflow: ellipsis;'
+ text.textContent = label
+ ghost.appendChild(text)
+
+ document.body.appendChild(ghost)
+ return ghost
+}
+
export function compareByOrder(
a: T,
b: T
diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts
index 2c38f339558..9cd1eab999a 100644
--- a/apps/sim/hooks/queries/tasks.ts
+++ b/apps/sim/hooks/queries/tasks.ts
@@ -1,5 +1,5 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
-import type { MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
+import type { ChatContextKind, MothershipResource } from '@/app/workspace/[workspaceId]/home/types'
export interface TaskMetadata {
id: string
@@ -42,13 +42,14 @@ export interface TaskStoredFileAttachment {
}
export interface TaskStoredMessageContext {
- kind: string
+ kind: ChatContextKind
label: string
workflowId?: string
knowledgeId?: string
tableId?: string
fileId?: string
folderId?: string
+ chatId?: string
}
export interface TaskStoredMessage {
diff --git a/apps/sim/lib/copilot/resource-types.ts b/apps/sim/lib/copilot/resource-types.ts
index c0e83fe8a46..a223536925c 100644
--- a/apps/sim/lib/copilot/resource-types.ts
+++ b/apps/sim/lib/copilot/resource-types.ts
@@ -4,6 +4,7 @@ export type MothershipResourceType =
| 'workflow'
| 'knowledgebase'
| 'folder'
+ | 'task'
| 'generic'
export interface MothershipResource {
@@ -19,3 +20,9 @@ export const VFS_DIR_TO_RESOURCE: Record = {
knowledgebases: 'knowledgebase',
folders: 'folder',
} as const
+
+/** MIME type for a single dragged resource (used by resource-tabs internal reordering). */
+export const SIM_RESOURCE_DRAG_TYPE = 'application/x-sim-resource' as const
+
+/** MIME type for an array of dragged resources (used by sidebar drag-to-chat). */
+export const SIM_RESOURCES_DRAG_TYPE = 'application/x-sim-resources' as const
diff --git a/apps/sim/lib/workspaces/colors.ts b/apps/sim/lib/workspaces/colors.ts
index 3a0b80af93a..b652ebd36d9 100644
--- a/apps/sim/lib/workspaces/colors.ts
+++ b/apps/sim/lib/workspaces/colors.ts
@@ -77,6 +77,14 @@ function withAlpha(hexColor: string, alpha: number): string {
return `rgba(${r}, ${g}, ${b}, ${Math.min(Math.max(alpha, 0), 1)})`
}
+/**
+ * Returns the hex color with 60/ff (~38%) alpha — used for workflow color border accents.
+ * Returns `undefined` when `color` is undefined so callers can pass it directly to `borderColor`.
+ */
+export function workflowBorderColor(color: string | undefined): string | undefined {
+ return color ? `${color}60` : undefined
+}
+
function buildGradient(fromColor: string, toColor: string, rotationSeed: number): string {
const rotation = (rotationSeed * 25) % 360
return `linear-gradient(${rotation}deg, ${fromColor}, ${toColor})`