From 2a8d23d5a5179d82d38ebfa1d8366a524f45f45d Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 17:16:25 -0700 Subject: [PATCH 01/15] feat(chat): drag workflows and folders from sidebar into chat input --- .../resource-tabs/resource-tabs.tsx | 3 +- .../home/components/user-input/user-input.tsx | 30 +++++++++++++++---- .../components/folder-item/folder-item.tsx | 10 ++++++- .../workflow-item/workflow-item.tsx | 10 ++++++- .../w/components/sidebar/utils.ts | 28 +++++++++++++++++ apps/sim/lib/copilot/resource-types.ts | 6 ++++ 6 files changed, 79 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 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/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index d2415318b31..fa05cefb4c4 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 @@ -6,6 +6,7 @@ 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 { useAvailableResources } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/add-resource-dropdown' @@ -247,15 +248,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 +284,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,7 +298,21 @@ 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) + } + } catch { + // Invalid JSON — ignore + } + return + } + const resourceJson = e.dataTransfer.getData(SIM_RESOURCE_DRAG_TYPE) if (resourceJson) { e.preventDefault() e.stopPropagation() 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..7dd06451d14 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,7 @@ import { useSidebarDragContext, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' +import { buildDragResources } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useCanDelete, useDeleteFolder, @@ -197,9 +199,15 @@ export function FolderItem({ e.dataTransfer.setData('sidebar-selection', JSON.stringify(selection)) e.dataTransfer.effectAllowed = 'move' + + const resources = buildDragResources(selection, workspaceId) + if (resources.length > 0) { + e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources)) + } + onDragStartProp?.() }, - [folder.id, onDragStartProp] + [folder.id, workspaceId, onDragStartProp] ) const { 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..88d95cb8c9a 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,7 @@ 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 { 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 +17,7 @@ import { useItemRename, useSidebarDragContext, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' +import { buildDragResources } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useCanDelete, useDeleteSelection, @@ -338,9 +340,15 @@ export function WorkflowItem({ e.dataTransfer.setData('sidebar-selection', JSON.stringify(selection)) e.dataTransfer.effectAllowed = 'move' + + const resources = buildDragResources(selection, workspaceId) + if (resources.length > 0) { + e.dataTransfer.setData(SIM_RESOURCES_DRAG_TYPE, JSON.stringify(resources)) + } + onDragStartProp?.() }, - [workflow.id, onDragStartProp] + [workflow.id, workspaceId, onDragStartProp] ) const { 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..e30f3d67395 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,33 @@ +import type { MothershipResource } from '@/lib/copilot/resource-types' +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 function compareByOrder( a: T, b: T diff --git a/apps/sim/lib/copilot/resource-types.ts b/apps/sim/lib/copilot/resource-types.ts index c0e83fe8a46..1538551659b 100644 --- a/apps/sim/lib/copilot/resource-types.ts +++ b/apps/sim/lib/copilot/resource-types.ts @@ -19,3 +19,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 From c0dd8304c35967854159414f1d50c8b877ed05d2 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 17:46:56 -0700 Subject: [PATCH 02/15] fix(chat): fix effectAllowed, stale atInsertPosRef, and drag-enter overlay for resource drags --- .../home/components/user-input/user-input.tsx | 13 +++++++++++-- .../components/folder-item/folder-item.tsx | 2 +- .../components/workflow-item/workflow-item.tsx | 2 +- 3 files changed, 13 insertions(+), 4 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 fa05cefb4c4..9f4046c41b8 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 @@ -307,6 +307,8 @@ export function UserInput({ 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 } @@ -319,6 +321,7 @@ export function UserInput({ try { const resource = JSON.parse(resourceJson) as MothershipResource handleResourceSelect(resource) + atInsertPosRef.current = null } catch { // Invalid JSON — ignore } @@ -330,11 +333,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) => { 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 7dd06451d14..665c65109ee 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 @@ -198,7 +198,7 @@ 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) { 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 88d95cb8c9a..827fb607f26 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 @@ -339,7 +339,7 @@ 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) { From 9cfd1ee5f97df6e39416ed0aa6ad366bb7917583 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 17:58:15 -0700 Subject: [PATCH 03/15] feat(chat): add task dragging and visible drag ghost for sidebar items --- .../add-resource-dropdown.tsx | 12 ++++++- .../resource-registry/resource-registry.tsx | 14 ++++++++ .../user-input/components/constants.ts | 2 ++ .../components/folder-item/folder-item.tsx | 19 +++++++++-- .../workflow-item/workflow-item.tsx | 20 +++++++++-- .../w/components/sidebar/sidebar.tsx | 33 ++++++++++++++++++- .../w/components/sidebar/utils.ts | 32 ++++++++++++++++++ apps/sim/lib/copilot/resource-types.ts | 1 + 8 files changed, 127 insertions(+), 6 deletions(-) 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-registry/resource-registry.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-registry.tsx index e10c31b9a61..dccc55e6d0e 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, @@ -19,6 +20,7 @@ import type { } 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' @@ -151,6 +153,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 +196,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/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]/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 665c65109ee..10191b710c3 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 @@ -19,7 +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 } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' +import { + buildDragResources, + createSidebarDragGhost, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useCanDelete, useDeleteFolder, @@ -138,6 +141,7 @@ export function FolderItem({ }) const isEditingRef = useRef(false) + const dragGhostRef = useRef(null) const handleCreateWorkflowInFolder = useCallback(() => { const name = generateCreativeWorkflowName() @@ -205,9 +209,16 @@ export function FolderItem({ 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 ghost = createSidebarDragGhost(ghostLabel) + void ghost.offsetHeight + e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) + dragGhostRef.current = ghost + onDragStartProp?.() }, - [folder.id, workspaceId, onDragStartProp] + [folder.id, folder.name, workspaceId, onDragStartProp] ) const { @@ -220,6 +231,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 827fb607f26..f0592d3b937 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 @@ -17,7 +17,10 @@ import { useItemRename, useSidebarDragContext, } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks' -import { buildDragResources } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' +import { + buildDragResources, + createSidebarDragGhost, +} from '@/app/workspace/[workspaceId]/w/components/sidebar/utils' import { useCanDelete, useDeleteSelection, @@ -200,6 +203,7 @@ export function WorkflowItem({ }, [isActiveWorkflow, isWorkflowLocked]) const isEditingRef = useRef(false) + const dragGhostRef = useRef(null) const { isOpen: isContextMenuOpen, @@ -346,9 +350,17 @@ export function WorkflowItem({ 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 ghost = createSidebarDragGhost(ghostLabel) + // 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, workspaceId, onDragStartProp] + [workflow.id, workflow.name, workspaceId, onDragStartProp] ) const { @@ -361,6 +373,10 @@ export function WorkflowItem({ }) 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/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 02f0bca42e8..279355e35a4 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) + 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 e30f3d67395..9d7e2110cc8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts @@ -28,6 +28,38 @@ export function buildDragResources( ] } +/** + * Creates a lightweight drag ghost element showing the label of the item(s) being dragged. + * Append to `document.body`, pass to `e.dataTransfer.setDragImage`, then remove on dragend. + */ +export function createSidebarDragGhost(label: string): HTMLElement { + const ghost = document.createElement('div') + ghost.style.cssText = ` + position: fixed; + top: -500px; + left: 0; + display: inline-flex; + align-items: center; + 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; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + pointer-events: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.4); + z-index: 9999; + ` + ghost.textContent = label + document.body.appendChild(ghost) + return ghost +} + export function compareByOrder( a: T, b: T diff --git a/apps/sim/lib/copilot/resource-types.ts b/apps/sim/lib/copilot/resource-types.ts index 1538551659b..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 { From 3bdedad7c1fec105acdb9f19054b09e2734e32b9 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:07:50 -0700 Subject: [PATCH 04/15] feat(sidebar): add drag ghost with icons and task icon to context chips --- .../home/components/user-input/user-input.tsx | 5 ++- .../user-message-content.tsx | 5 ++- .../components/folder-item/folder-item.tsx | 3 +- .../workflow-item/workflow-item.tsx | 3 +- .../w/components/sidebar/sidebar.tsx | 2 +- .../w/components/sidebar/utils.ts | 42 ++++++++++++++++--- 6 files changed, 49 insertions(+), 11 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 9f4046c41b8..bbe85d2da7e 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,7 +3,7 @@ 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 { Blimp, 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' @@ -706,6 +706,9 @@ export function UserInput({ case 'folder': mentionIconNode = break + case 'past_chat': + mentionIconNode = + break } } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx index 7e72bdf54f4..1b9a9c76a57 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-message-content/user-message-content.tsx @@ -2,7 +2,7 @@ import { useMemo } from 'react' import { useParams } from 'next/navigation' -import { Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons' +import { Blimp, Database, Folder as FolderIcon, Table as TableIcon } from '@/components/emcn/icons' import { getDocumentIcon } from '@/components/icons/document-icons' import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types' import { useWorkflows } from '@/hooks/queries/workflows' @@ -84,6 +84,9 @@ function MentionHighlight({ context }: { context: ChatMessageContext }) { case 'folder': icon = break + case 'past_chat': + icon = + break } return ( 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 10191b710c3..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 @@ -211,7 +211,8 @@ export function FolderItem({ const total = selection.folderIds.length + selection.workflowIds.length const ghostLabel = total > 1 ? `${folder.name} +${total - 1} more` : folder.name - const ghost = createSidebarDragGhost(ghostLabel) + 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 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 f0592d3b937..ee44cb79b11 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 @@ -352,7 +352,8 @@ export function WorkflowItem({ const total = selection.workflowIds.length + selection.folderIds.length const ghostLabel = total > 1 ? `${workflow.name} +${total - 1} more` : workflow.name - const ghost = createSidebarDragGhost(ghostLabel) + 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) 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 279355e35a4..1a9dcb4765c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -172,7 +172,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ SIM_RESOURCES_DRAG_TYPE, JSON.stringify([{ type: 'task', id: task.id, title: task.name }]) ) - const ghost = createSidebarDragGhost(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 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 9d7e2110cc8..417f502a780 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/utils.ts @@ -28,11 +28,20 @@ export function buildDragResources( ] } +export type SidebarDragGhostIcon = + | { kind: 'workflow'; color: string } + | { kind: 'folder' } + | { kind: 'task' } + +const FOLDER_SVG = `` + +const BLIMP_SVG = `` + /** - * Creates a lightweight drag ghost element showing the label of the item(s) being dragged. + * 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): HTMLElement { +export function createSidebarDragGhost(label: string, icon?: SidebarDragGhostIcon): HTMLElement { const ghost = document.createElement('div') ghost.style.cssText = ` position: fixed; @@ -40,6 +49,7 @@ export function createSidebarDragGhost(label: string): HTMLElement { 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); @@ -48,14 +58,34 @@ export function createSidebarDragGhost(label: string): HTMLElement { font-size: 13px; color: var(--text-body); white-space: nowrap; - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; pointer-events: none; box-shadow: 0 4px 12px rgba(0,0,0,0.4); z-index: 9999; ` - ghost.textContent = label + + 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 ${icon.color}60; + 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 } From 0ebee18ef68eee617c96c491def48b07f3b9b192 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:22:33 -0700 Subject: [PATCH 05/15] refactor(types): narrow ChatMessageContext.kind to ChatContextKind union and add workflowBorderColor utility --- .../features/components/features-preview.tsx | 9 ++-- .../landing-preview-logs.tsx | 3 +- .../landing-preview-sidebar.tsx | 3 +- .../home/components/context-mention-icon.tsx | 45 +++++++++++++++++ .../[workspaceId]/home/components/index.ts | 1 + .../resource-content/resource-content.tsx | 3 +- .../resource-registry/resource-registry.tsx | 5 +- .../home/components/user-input/user-input.tsx | 49 +++---------------- .../user-message-content.tsx | 45 +++-------------- .../app/workspace/[workspaceId]/home/types.ts | 5 +- .../workflows-list/workflows-list.tsx | 3 +- .../components/log-details/log-details.tsx | 3 +- .../logs/components/logs-list/logs-list.tsx | 3 +- .../components/logs-toolbar/logs-toolbar.tsx | 7 +-- .../app/workspace/[workspaceId]/logs/logs.tsx | 7 +-- .../recently-deleted/recently-deleted.tsx | 3 +- .../collapsed-sidebar-menu.tsx | 3 +- .../search-modal/components/command-items.tsx | 3 +- .../workflow-item/workflow-item.tsx | 3 +- apps/sim/hooks/queries/tasks.ts | 4 +- apps/sim/lib/workspaces/colors.ts | 10 ++++ 21 files changed, 112 insertions(+), 105 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/home/components/context-mention-icon.tsx 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-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 dccc55e6d0e..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 @@ -14,6 +14,7 @@ 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, @@ -50,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', }} /> @@ -65,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', }} /> 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 bbe85d2da7e..dd8c6e5f534 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,12 +3,11 @@ import type React from 'react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { useParams } from 'next/navigation' -import { Blimp, 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, @@ -672,45 +671,13 @@ 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 - case 'past_chat': - mentionIconNode = - break - } - } + 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 - case 'past_chat': - icon = - break - } - return ( - {icon && {icon}} + {context.label} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index d4f812cc25a..f1389213643 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,7 +263,7 @@ export interface ChatMessageAttachment { } export interface ChatMessageContext { - kind: string + kind: ChatContextKind label: string workflowId?: string knowledgeId?: string 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/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx index ee44cb79b11..dd91230be27 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 @@ -6,6 +6,7 @@ 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' @@ -439,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/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index 2c38f339558..b99f2322590 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,7 +42,7 @@ export interface TaskStoredFileAttachment { } export interface TaskStoredMessageContext { - kind: string + kind: ChatContextKind label: string workflowId?: string knowledgeId?: string diff --git a/apps/sim/lib/workspaces/colors.ts b/apps/sim/lib/workspaces/colors.ts index 3a0b80af93a..74c16680395 100644 --- a/apps/sim/lib/workspaces/colors.ts +++ b/apps/sim/lib/workspaces/colors.ts @@ -77,6 +77,16 @@ 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. + * + * @param color - A hex color string (e.g. `#2ABBF8`) + * @returns The color string with `60` appended as the hex alpha channel + */ +export function workflowBorderColor(color: string): string { + return `${color}60` +} + function buildGradient(fromColor: string, toColor: string, rotationSeed: number): string { const rotation = (rotationSeed * 25) % 360 return `linear-gradient(${rotation}deg, ${fromColor}, ${toColor})` From 86e7c9e70abaeb81b8aa266c5af873fb05461cf3 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:23:25 -0700 Subject: [PATCH 06/15] feat(user-input): support Tab to select resource in mention dropdown --- .../user-input/components/plus-menu-dropdown.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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() + } } }, []) From 4e1fa16086e5d9e0136df6880cb5118b80aa037a Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:36:42 -0700 Subject: [PATCH 07/15] fix(user-input): narrow ChatContext discriminated union before accessing workflowId --- .../[workspaceId]/home/components/user-input/user-input.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 dd8c6e5f534..968cfe5c71a 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 @@ -671,10 +671,14 @@ export function UserInput({ : range.token const matchingCtx = contexts.find((c) => c.label === mentionLabel) + const wfId = + matchingCtx?.kind === 'workflow' || matchingCtx?.kind === 'current_workflow' + ? matchingCtx.workflowId + : undefined const mentionIconNode = matchingCtx ? ( ) : null From fccf98db19109ef53249153db171c340ee391484 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:49:24 -0700 Subject: [PATCH 08/15] fix(colors): overload workflowBorderColor to accept string | undefined --- apps/sim/lib/workspaces/colors.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/workspaces/colors.ts b/apps/sim/lib/workspaces/colors.ts index 74c16680395..c1499717b64 100644 --- a/apps/sim/lib/workspaces/colors.ts +++ b/apps/sim/lib/workspaces/colors.ts @@ -83,8 +83,10 @@ function withAlpha(hexColor: string, alpha: number): string { * @param color - A hex color string (e.g. `#2ABBF8`) * @returns The color string with `60` appended as the hex alpha channel */ -export function workflowBorderColor(color: string): string { - return `${color}60` +export function workflowBorderColor(color: string): string +export function workflowBorderColor(color: string | undefined): string | undefined +export function workflowBorderColor(color: string | undefined): string | undefined { + return color ? `${color}60` : undefined } function buildGradient(fromColor: string, toColor: string, rotationSeed: number): string { From 4ff9b845111f39ce58af344956cdf25ce8f72ef0 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 18:53:06 -0700 Subject: [PATCH 09/15] fix(colors): simplify workflowBorderColor to single string | undefined signature --- apps/sim/lib/workspaces/colors.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/sim/lib/workspaces/colors.ts b/apps/sim/lib/workspaces/colors.ts index c1499717b64..b652ebd36d9 100644 --- a/apps/sim/lib/workspaces/colors.ts +++ b/apps/sim/lib/workspaces/colors.ts @@ -79,12 +79,8 @@ function withAlpha(hexColor: string, alpha: number): string { /** * Returns the hex color with 60/ff (~38%) alpha — used for workflow color border accents. - * - * @param color - A hex color string (e.g. `#2ABBF8`) - * @returns The color string with `60` appended as the hex alpha channel + * Returns `undefined` when `color` is undefined so callers can pass it directly to `borderColor`. */ -export function workflowBorderColor(color: string): string -export function workflowBorderColor(color: string | undefined): string | undefined export function workflowBorderColor(color: string | undefined): string | undefined { return color ? `${color}60` : undefined } From 7b8fa0039255de937c7406f2bb2ee66c63a5fb12 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:00:57 -0700 Subject: [PATCH 10/15] fix(chat): remove resource panel tab when context mention is deleted from input --- .../mothership-chat/mothership-chat.tsx | 3 + .../home/components/user-input/user-input.tsx | 14 +++++ .../app/workspace/[workspaceId]/home/home.tsx | 61 ++++++++----------- 3 files changed, 44 insertions(+), 34 deletions(-) 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/user-input/user-input.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/user-input.tsx index 968cfe5c71a..cdc0083fd22 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 @@ -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,18 @@ 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 removed = prev.filter((p) => !curr.some((c) => c.kind === p.kind && c.label === p.label)) + 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) { 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} From 6d31c36f658bbe1523c4cd71598cd65aa5360cf3 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:09:18 -0700 Subject: [PATCH 11/15] fix(chat): use resource ID for context removal identity check --- .../home/components/user-input/user-input.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) 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 cdc0083fd22..785940911d9 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 @@ -179,7 +179,26 @@ export function UserInput({ useEffect(() => { const prev = prevSelectedContextsRef.current const curr = contextManagement.selectedContexts - const removed = prev.filter((p) => !curr.some((c) => c.kind === p.kind && c.label === p.label)) + 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]) From 4d492a4c13cf9cc64eff467ce9c05cc22ab36a88 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:24:59 -0700 Subject: [PATCH 12/15] fix(chat): add folder/task cases to resource resolver, task key to existingResourceKeys, and use workflowBorderColor in drag ghost --- .../[workspaceId]/home/components/user-input/user-input.tsx | 1 + apps/sim/app/workspace/[workspaceId]/home/home.tsx | 4 ++++ .../app/workspace/[workspaceId]/w/components/sidebar/utils.ts | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) 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 785940911d9..4e88ae3ed95 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 @@ -211,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') keys.add(`task:${ctx.chatId}`) } return keys }, [contextManagement.selectedContexts]) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 132d87b2a9e..2faaa11a721 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -273,6 +273,10 @@ export function Home({ chatId }: HomeProps = {}) { return context.tableId ? { type: 'table', id: context.tableId } : null case 'file': return context.fileId ? { type: 'file', id: context.fileId } : null + case 'folder': + return { type: 'folder', id: context.folderId } + case 'past_chat': + return { type: 'task', id: context.chatId } default: return null } 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 417f502a780..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,4 +1,5 @@ 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' @@ -68,7 +69,7 @@ export function createSidebarDragGhost(label: string, icon?: SidebarDragGhostIco const square = document.createElement('div') square.style.cssText = ` width: 14px; height: 14px; flex-shrink: 0; - border-radius: 3px; border: 2px solid ${icon.color}60; + border-radius: 3px; border: 2px solid ${workflowBorderColor(icon.color)}; background: ${icon.color}; background-clip: padding-box; ` ghost.appendChild(square) From abaac1575e8821cb9b20c5b86588edcade184c22 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:26:12 -0700 Subject: [PATCH 13/15] =?UTF-8?q?revert(chat):=20remove=20folder/task=20fr?= =?UTF-8?q?om=20resolveResourceFromContext=20=E2=80=94=20no=20panel=20UI?= =?UTF-8?q?=20for=20these=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/sim/app/workspace/[workspaceId]/home/home.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 2faaa11a721..132d87b2a9e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -273,10 +273,6 @@ export function Home({ chatId }: HomeProps = {}) { return context.tableId ? { type: 'table', id: context.tableId } : null case 'file': return context.fileId ? { type: 'file', id: context.fileId } : null - case 'folder': - return { type: 'folder', id: context.folderId } - case 'past_chat': - return { type: 'task', id: context.chatId } default: return null } From 669f2ac1a228716f5546cdd01907bd0b96518243 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:39:57 -0700 Subject: [PATCH 14/15] fix(chat): add chatId to stored context types and workflow.color to drag callback deps --- apps/sim/app/workspace/[workspaceId]/home/types.ts | 1 + .../workflow-list/components/workflow-item/workflow-item.tsx | 2 +- apps/sim/hooks/queries/tasks.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/home/types.ts b/apps/sim/app/workspace/[workspaceId]/home/types.ts index f1389213643..e6ae3c9f0a9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/types.ts @@ -270,6 +270,7 @@ export interface ChatMessageContext { tableId?: string fileId?: string folderId?: string + chatId?: string } export interface ChatMessage { 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 dd91230be27..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 @@ -362,7 +362,7 @@ export function WorkflowItem({ onDragStartProp?.() }, - [workflow.id, workflow.name, workspaceId, onDragStartProp] + [workflow.id, workflow.name, workflow.color, workspaceId, onDragStartProp] ) const { diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index b99f2322590..9cd1eab999a 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/tasks.ts @@ -49,6 +49,7 @@ export interface TaskStoredMessageContext { tableId?: string fileId?: string folderId?: string + chatId?: string } export interface TaskStoredMessage { From e471b2eb725b378fbb7c561409c860a600e8f76a Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 7 Apr 2026 19:53:16 -0700 Subject: [PATCH 15/15] fix(chat): guard chatId before adding task key to existingResourceKeys --- .../[workspaceId]/home/components/user-input/user-input.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4e88ae3ed95..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 @@ -211,7 +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') keys.add(`task:${ctx.chatId}`) + if (ctx.kind === 'past_chat' && ctx.chatId) keys.add(`task:${ctx.chatId}`) } return keys }, [contextManagement.selectedContexts])