From 9453502498b912101b45686b7c7e7e2bdcb1953a Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:08:10 -0700
Subject: [PATCH 01/12] feat(mothership): draft persistence, new task eager
creation, doc preview fix, and loading polish
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add `useMothershipDraftsStore` (persist) to save and restore draft text, file
attachments, and contexts per chat scope key; cleared on workspace reset
- Add `useCreateTask` mutation that eagerly POSTs a new chat, optimistically
prepends it to the sidebar task list, and navigates to `/task/:id` — removes
the `id:'new'` placeholder pattern entirely
- Fix docx/pptx/pdf preview: remove `!isDocFormat` guards in
`file-preview-adapter` so preview sessions are created and streaming content
is set during agent writes, suppressing intermediate compilation errors
- Replace `Loader2` (lucide) with `Loader` (emcn) across auth, chat, knowledge,
logs, deploy, and tool-input components; add `Clipboard` icon to emcn
- Add `EditorContextMenu` to the Monaco file viewer
- Expand Monaco `SIM_DARK` and `SIM_LIGHT` token rules with string.link,
delimiter, tag, attribute, and Markdown tokens (strong, emphasis, variable)
---
apps/sim/app/(auth)/login/login-form.tsx | 7 +-
apps/sim/app/(auth)/oauth/consent/page.tsx | 6 +-
.../reset-password/reset-password-form.tsx | 8 +-
apps/sim/app/(auth)/signup/signup-form.tsx | 6 +-
apps/sim/app/(auth)/verify/verify-content.tsx | 5 +-
.../components/auth-modal/auth-modal.tsx | 13 +-
.../components/course-progress.tsx | 5 +-
.../chat/components/auth/email/email-auth.tsx | 7 +-
.../auth/password/password-auth.tsx | 6 +-
.../message/components/file-download.tsx | 8 +-
.../[identifier]/components/password-auth.tsx | 6 +-
apps/sim/app/form/[identifier]/form.tsx | 4 +-
.../sim/app/invite/components/status-card.tsx | 6 +-
apps/sim/app/unsubscribe/unsubscribe.tsx | 8 +-
.../file-viewer/editor-context-menu.tsx | 102 ++++++++++++++++
.../components/file-viewer/text-editor.tsx | 104 +++++++++++++++-
.../workspace/[workspaceId]/files/files.tsx | 87 ++++++--------
.../mothership-chat/mothership-chat.tsx | 27 +++--
.../components/attached-files-list.tsx | 6 +-
.../home/components/user-input/user-input.tsx | 73 ++++++++++--
.../app/workspace/[workspaceId]/home/home.tsx | 3 +
.../[workspaceId]/knowledge/[id]/base.tsx | 8 +-
.../add-connector-modal.tsx | 5 +-
.../add-documents-modal.tsx | 5 +-
.../connector-selector-field.tsx | 5 +-
.../connectors-section/connectors-section.tsx | 10 +-
.../edit-connector-modal.tsx | 5 +-
.../create-base-modal/create-base-modal.tsx | 5 +-
.../logs/components/dashboard/dashboard.tsx | 9 +-
.../execution-snapshot/execution-snapshot.tsx | 7 +-
.../file-download/file-download.tsx | 8 +-
.../logs/components/logs-list/logs-list.tsx | 6 +-
.../skills/components/skill-import.tsx | 11 +-
.../deploy-modal/components/chat/chat.tsx | 5 +-
.../deploy-modal/components/form/form.tsx | 5 +-
.../panel/components/deploy/deploy.tsx | 5 +-
.../components/tool-input/tool-input.tsx | 5 +-
.../panel/components/editor/editor.tsx | 5 +-
.../components/permissions-table.tsx | 8 +-
.../w/components/sidebar/sidebar.tsx | 112 +++++++++---------
.../emcn/components/combobox/combobox.tsx | 5 +-
apps/sim/components/emcn/icons/clipboard.tsx | 26 ++++
apps/sim/components/emcn/icons/index.ts | 1 +
apps/sim/ee/sso/components/sso-auth.tsx | 5 +-
apps/sim/ee/sso/components/sso-form.tsx | 5 +-
.../components/whitelabeling-settings.tsx | 8 +-
apps/sim/hooks/queries/tasks.ts | 35 ++++++
.../request/go/file-preview-adapter.ts | 4 +-
.../lib/copilot/tools/client/store-utils.ts | 11 +-
apps/sim/stores/index.ts | 2 +
apps/sim/stores/mothership-drafts/store.ts | 50 ++++++++
51 files changed, 622 insertions(+), 256 deletions(-)
create mode 100644 apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
create mode 100644 apps/sim/components/emcn/icons/clipboard.tsx
create mode 100644 apps/sim/stores/mothership-drafts/store.ts
diff --git a/apps/sim/app/(auth)/login/login-form.tsx b/apps/sim/app/(auth)/login/login-form.tsx
index 27119bf180..60a9c8ab94 100644
--- a/apps/sim/app/(auth)/login/login-form.tsx
+++ b/apps/sim/app/(auth)/login/login-form.tsx
@@ -2,12 +2,13 @@
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { Eye, EyeOff, Loader2 } from 'lucide-react'
+import { Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
import {
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -455,7 +456,7 @@ export default function LoginPage({
-
+
)
@@ -218,7 +218,7 @@ function UnsubscribeContent() {
>
{processing ? (
-
+
Unsubscribing...
) : isAlreadyUnsubscribedFromAll ? (
@@ -301,7 +301,7 @@ export default function Unsubscribe() {
-
+
}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
new file mode 100644
index 0000000000..e0eeab3b7e
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
@@ -0,0 +1,102 @@
+'use client'
+
+import { Scissors } from 'lucide-react'
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuTrigger,
+} from '@/components/emcn'
+import { Clipboard, Copy, Search } from '@/components/emcn/icons'
+
+interface EditorContextMenuProps {
+ isOpen: boolean
+ position: { x: number; y: number }
+ onClose: () => void
+ hasSelection: boolean
+ canEdit: boolean
+ onCut: () => void
+ onCopy: () => void
+ onCopyAll: () => void
+ onPaste: () => void
+ onSelectAll: () => void
+ onFind: () => void
+}
+
+export function EditorContextMenu({
+ isOpen,
+ position,
+ onClose,
+ hasSelection,
+ canEdit,
+ onCut,
+ onCopy,
+ onCopyAll,
+ onPaste,
+ onSelectAll,
+ onFind,
+}: EditorContextMenuProps) {
+ return (
+ !open && onClose()} modal={false}>
+
+
+
+ e.preventDefault()}
+ >
+ {canEdit && (
+
+
+ Cut
+ ⌘X
+
+ )}
+
+
+ Copy
+ ⌘C
+
+
+
+ Copy all
+
+ {canEdit && (
+ <>
+
+
+
+ Paste
+ ⌘V
+
+ >
+ )}
+
+
+ Select all
+ ⌘A
+
+
+
+ Find
+ ⌘F
+
+
+
+ )
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
index 048866dbf1..af75b820c1 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
@@ -13,6 +13,7 @@ import {
useWorkspaceFileContent,
} from '@/hooks/queries/workspace-files'
import { useAutosave } from '@/hooks/use-autosave'
+import { EditorContextMenu } from './editor-context-menu'
import type { PreviewMode } from './file-viewer'
import { PreviewPanel, resolvePreviewType } from './preview-panel'
import {
@@ -23,9 +24,11 @@ import {
} from './text-editor-state'
const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
+ // Core
{ token: 'comment', foreground: '606060', fontStyle: 'italic' },
{ token: 'string', foreground: '3ab872' },
{ token: 'string.escape', foreground: '3ab872' },
+ { token: 'string.link', foreground: '33b4ff' },
{ token: 'number', foreground: 'e8a87c' },
{ token: 'number.float', foreground: 'e8a87c' },
{ token: 'number.hex', foreground: 'e8a87c' },
@@ -36,12 +39,26 @@ const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
{ token: 'type.identifier', foreground: '8fc7f5' },
{ token: 'regexp', foreground: 'ff8a65' },
{ token: 'annotation', foreground: 'ffca28' },
+ { token: 'delimiter', foreground: '555555' },
+ { token: 'tag', foreground: '33b4ff' },
+ { token: 'attribute.name', foreground: '8fc7f5' },
+ { token: 'attribute.value', foreground: '3ab872' },
+ // Markdown — Monaco Monarch emits these (tokenPostfix: ".md")
+ // `keyword.md` covers headings + list markers (already caught by `keyword` above)
+ // `comment.md` covers blockquotes (already caught by `comment` above)
+ { token: 'strong', foreground: 'e6e6e6', fontStyle: 'bold' },
+ { token: 'emphasis', foreground: 'c8c8c8', fontStyle: 'italic' },
+ { token: 'variable', foreground: '3ab872' },
+ { token: 'variable.source', foreground: 'b0b0b0' },
+ { token: 'meta.separator', foreground: '404040' },
]
const SIM_LIGHT_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
+ // Core
{ token: 'comment', foreground: '888888', fontStyle: 'italic' },
{ token: 'string', foreground: '16825d' },
{ token: 'string.escape', foreground: '16825d' },
+ { token: 'string.link', foreground: '0078d4' },
{ token: 'number', foreground: 'c9660c' },
{ token: 'number.float', foreground: 'c9660c' },
{ token: 'number.hex', foreground: 'c9660c' },
@@ -52,6 +69,16 @@ const SIM_LIGHT_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
{ token: 'type.identifier', foreground: '7c4dcc' },
{ token: 'regexp', foreground: 'd7390c' },
{ token: 'annotation', foreground: 'e67700' },
+ { token: 'delimiter', foreground: 'aaaaaa' },
+ { token: 'tag', foreground: '0078d4' },
+ { token: 'attribute.name', foreground: '7c4dcc' },
+ { token: 'attribute.value', foreground: '16825d' },
+ // Markdown — Monaco Monarch emits these (tokenPostfix: ".md")
+ { token: 'strong', foreground: '1a1a1a', fontStyle: 'bold' },
+ { token: 'emphasis', foreground: '444444', fontStyle: 'italic' },
+ { token: 'variable', foreground: '16825d' },
+ { token: 'variable.source', foreground: '555555' },
+ { token: 'meta.separator', foreground: 'cccccc' },
]
const MonacoEditor = dynamic(
@@ -384,6 +411,11 @@ export const TextEditor = memo(function TextEditor({
const [splitPct, setSplitPct] = useState(SPLIT_DEFAULT_PCT)
const [isResizing, setIsResizing] = useState(false)
+ const [contextMenu, setContextMenu] = useState<{
+ x: number
+ y: number
+ hasSelection: boolean
+ } | null>(null)
const {
data: fetchedContent,
@@ -593,6 +625,16 @@ export const TextEditor = memo(function TextEditor({
hasAutoFocusedRef.current = true
editor.focus()
}
+
+ editor.onContextMenu((e) => {
+ e.event.preventDefault()
+ const sel = editor.getSelection()
+ setContextMenu({
+ x: e.event.posx,
+ y: e.event.posy,
+ hasSelection: sel !== null && !sel.isEmpty(),
+ })
+ })
}
const handleEditorChange = useCallback(
@@ -623,6 +665,8 @@ export const TextEditor = memo(function TextEditor({
}
}
+ const closeContextMenu = () => setContextMenu(null)
+
return (
{showEditor && (
@@ -652,7 +696,7 @@ export const TextEditor = memo(function TextEditor({
tabSize: 2,
automaticLayout: true,
renderLineHighlight: 'line',
- occurrencesHighlight: 'off',
+ occurrencesHighlight: 'singleFile',
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
scrollbar: {
@@ -661,14 +705,19 @@ export const TextEditor = memo(function TextEditor({
},
quickSuggestions: false,
suggestOnTriggerCharacters: false,
- wordBasedSuggestions: 'off',
+ wordBasedSuggestions: 'currentDocument',
parameterHints: { enabled: false },
- hover: { enabled: false },
codeLens: false,
lightbulb: {
enabled: 'off' as MonacoEditorTypes.ShowLightbulbIconMode,
},
inlayHints: { enabled: 'off' },
+ contextmenu: false,
+ fixedOverflowWidgets: true,
+ glyphMargin: false,
+ stickyScroll: { enabled: false },
+ bracketPairColorization: { enabled: false },
+ unicodeHighlight: { ambiguousCharacters: false },
}}
onChange={handleEditorChange}
onMount={handleEditorMount}
@@ -708,6 +757,55 @@ export const TextEditor = memo(function TextEditor({
>
)}
+ {contextMenu && (
+ {
+ monacoEditorRef.current?.focus()
+ monacoEditorRef.current?.trigger(
+ 'contextmenu',
+ 'editor.action.clipboardCutAction',
+ null
+ )
+ closeContextMenu()
+ }}
+ onCopy={() => {
+ monacoEditorRef.current?.focus()
+ monacoEditorRef.current?.trigger(
+ 'contextmenu',
+ 'editor.action.clipboardCopyAction',
+ null
+ )
+ closeContextMenu()
+ }}
+ onCopyAll={() => {
+ navigator.clipboard.writeText(monacoEditorRef.current?.getValue() ?? '')
+ closeContextMenu()
+ }}
+ onPaste={() => {
+ monacoEditorRef.current?.focus()
+ monacoEditorRef.current?.trigger(
+ 'contextmenu',
+ 'editor.action.clipboardPasteAction',
+ null
+ )
+ closeContextMenu()
+ }}
+ onSelectAll={() => {
+ monacoEditorRef.current?.focus()
+ monacoEditorRef.current?.trigger('contextmenu', 'editor.action.selectAll', null)
+ closeContextMenu()
+ }}
+ onFind={() => {
+ monacoEditorRef.current?.getAction('actions.find')?.run()
+ closeContextMenu()
+ }}
+ />
+ )}
)
})
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index b22deda22f..31185261b4 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -15,13 +15,13 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
Eye,
+ Loader,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Pencil,
- Skeleton,
Trash,
Upload,
} from '@/components/emcn'
@@ -373,45 +373,42 @@ export function Files() {
filteredFiles,
])
- const uploadFiles = useCallback(
- async (filesToUpload: File[]) => {
- if (!workspaceId || filesToUpload.length === 0) return
+ const uploadFiles = async (filesToUpload: File[]) => {
+ if (!workspaceId || filesToUpload.length === 0) return
- const unsupported: string[] = []
- const allowedFiles = filesToUpload.filter((f) => {
- const ext = getFileExtension(f.name)
- const ok = SUPPORTED_EXTENSIONS.includes(ext as (typeof SUPPORTED_EXTENSIONS)[number])
- if (!ok) unsupported.push(f.name)
- return ok
- })
-
- if (unsupported.length > 0) {
- logger.warn('Unsupported file types skipped:', unsupported)
- }
+ const unsupported: string[] = []
+ const allowedFiles = filesToUpload.filter((f) => {
+ const ext = getFileExtension(f.name)
+ const ok = SUPPORTED_EXTENSIONS.includes(ext as (typeof SUPPORTED_EXTENSIONS)[number])
+ if (!ok) unsupported.push(f.name)
+ return ok
+ })
- if (allowedFiles.length === 0) return
+ if (unsupported.length > 0) {
+ logger.warn('Unsupported file types skipped:', unsupported)
+ }
- try {
- setUploading(true)
- setUploadProgress({ completed: 0, total: allowedFiles.length })
+ if (allowedFiles.length === 0) return
- for (let i = 0; i < allowedFiles.length; i++) {
- try {
- await uploadFile.mutateAsync({ workspaceId, file: allowedFiles[i] })
- setUploadProgress({ completed: i + 1, total: allowedFiles.length })
- } catch (err) {
- logger.error('Error uploading file:', err)
- }
+ try {
+ setUploading(true)
+ setUploadProgress({ completed: 0, total: allowedFiles.length })
+
+ for (let i = 0; i < allowedFiles.length; i++) {
+ try {
+ await uploadFile.mutateAsync({ workspaceId, file: allowedFiles[i] })
+ setUploadProgress({ completed: i + 1, total: allowedFiles.length })
+ } catch (err) {
+ logger.error('Error uploading file:', err)
}
- } catch (err) {
- logger.error('Error uploading file:', err)
- } finally {
- setUploading(false)
- setUploadProgress({ completed: 0, total: 0 })
}
- },
- [workspaceId]
- )
+ } catch (err) {
+ logger.error('Error uploading file:', err)
+ } finally {
+ setUploading(false)
+ setUploadProgress({ completed: 0, total: 0 })
+ }
+ }
const handleFileChange = async (e: React.ChangeEvent) => {
const list = e.target.files
@@ -685,7 +682,7 @@ export function Files() {
if (isNewFile && fileIdFromRoute) {
router.replace(`/workspace/${workspaceId}/files/${fileIdFromRoute}`)
}
- }, [])
+ }, [isNewFile, fileIdFromRoute, router, workspaceId])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -1040,24 +1037,8 @@ export function Files() {
return (
-
- {[0, 1].map((i) => (
-
- ))}
+
+
)
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 ef18d5bc08..c4b8910b12 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
@@ -1,6 +1,6 @@
'use client'
-import { useCallback, useLayoutEffect, useMemo, useRef } from 'react'
+import { useLayoutEffect, useMemo, useRef } from 'react'
import { cn } from '@/lib/core/utils/cn'
import { MessageActions } from '@/app/workspace/[workspaceId]/components'
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
@@ -46,6 +46,7 @@ interface MothershipChatProps {
onContextAdd?: (context: ChatContext) => void
onContextRemove?: (context: ChatContext) => void
onWorkspaceResourceSelect?: (resource: MothershipResource) => void
+ draftScopeKey?: string
layout?: 'mothership-view' | 'copilot-view'
initialScrollBlocked?: boolean
animateInput?: boolean
@@ -93,6 +94,7 @@ export function MothershipChat({
onContextAdd,
onContextRemove,
onWorkspaceResourceSelect,
+ draftScopeKey,
layout = 'mothership-view',
initialScrollBlocked = false,
animateInput = false,
@@ -122,23 +124,23 @@ export function MothershipChat({
}, [messages])
const initialScrollDoneRef = useRef(false)
const userInputRef = useRef
(null)
- const handleSendQueuedHead = useCallback(() => {
+
+ function handleSendQueuedHead() {
const topMessage = messageQueue[0]
if (!topMessage) return
void onSendQueuedMessage(topMessage.id)
- }, [messageQueue, onSendQueuedMessage])
- const handleEditQueued = useCallback(
- (id: string) => {
- const msg = onEditQueuedMessage(id)
- if (msg) userInputRef.current?.loadQueuedMessage(msg)
- },
- [onEditQueuedMessage]
- )
- const handleEditQueuedTail = useCallback(() => {
+ }
+
+ function handleEditQueued(id: string) {
+ const msg = onEditQueuedMessage(id)
+ if (msg) userInputRef.current?.loadQueuedMessage(msg)
+ }
+
+ function handleEditQueuedTail() {
const tail = messageQueue[messageQueue.length - 1]
if (!tail) return
handleEditQueued(tail.id)
- }, [messageQueue, handleEditQueued])
+ }
useLayoutEffect(() => {
if (!hasMessages) {
@@ -250,6 +252,7 @@ export function MothershipChat({
onContextRemove={onContextRemove}
onSendQueuedHead={handleSendQueuedHead}
onEditQueuedTail={handleEditQueuedTail}
+ draftScopeKey={draftScopeKey}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx
index 3ec84a6e3a..6046107e6d 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/attached-files-list.tsx
@@ -1,8 +1,8 @@
'use client'
import React from 'react'
-import { Loader2, X } from 'lucide-react'
-import { Tooltip } from '@/components/emcn'
+import { X } from 'lucide-react'
+import { Loader, Tooltip } from '@/components/emcn'
import { getDocumentIcon } from '@/components/icons/document-icons'
import type { AttachedFile } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-file-attachments'
@@ -49,7 +49,7 @@ export const AttachedFilesList = React.memo(function AttachedFilesList({
)}
{file.uploading && (
-
+
)}
{!file.uploading && (
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 19a8e045b4..0defe05a9c 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
@@ -51,6 +51,7 @@ import {
import { useWorkflowMap } from '@/hooks/queries/workflows'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useSpeechToText } from '@/hooks/use-speech-to-text'
+import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import type { ChatContext } from '@/stores/panel'
export type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
@@ -101,6 +102,7 @@ function getCaretAnchor(
interface UserInputProps {
defaultValue?: string
+ draftScopeKey?: string
onSubmit: (
text: string,
fileAttachments?: FileAttachmentForApi[],
@@ -123,6 +125,7 @@ export interface UserInputHandle {
export const UserInput = forwardRef(function UserInput(
{
defaultValue = '',
+ draftScopeKey,
onSubmit,
isSending,
onStopGeneration,
@@ -139,17 +142,21 @@ export const UserInput = forwardRef(function Us
const { navigateToSettings } = useSettingsNavigation()
const { data: workflowsById = {} } = useWorkflowMap(workspaceId)
const { data: session } = useSession()
- const [value, setValue] = useState(defaultValue)
+ const [value, setValue] = useState(() => {
+ if (defaultValue) return defaultValue
+ if (!draftScopeKey) return ''
+ return useMothershipDraftsStore.getState().drafts[draftScopeKey]?.text ?? ''
+ })
const overlayRef = useRef(null)
const plusMenuRef = useRef(null)
- const [prevDefaultValue, setPrevDefaultValue] = useState(defaultValue)
- if (defaultValue && defaultValue !== prevDefaultValue) {
- setPrevDefaultValue(defaultValue)
+ const prevDefaultValueRef = useRef(defaultValue)
+ useEffect(() => {
+ if (!defaultValue) return
+ if (defaultValue === prevDefaultValueRef.current) return
+ prevDefaultValueRef.current = defaultValue
setValue(defaultValue)
- } else if (!defaultValue && prevDefaultValue) {
- setPrevDefaultValue(defaultValue)
- }
+ }, [defaultValue])
const files = useFileAttachments({
userId: userId || session?.user?.id,
@@ -172,6 +179,55 @@ export const UserInput = forwardRef(function Us
[addContext, onContextAdd]
)
+ const draftScopeKeyRef = useRef(draftScopeKey)
+ draftScopeKeyRef.current = draftScopeKey
+
+ const hasRestoredDraftRef = useRef(false)
+ useEffect(() => {
+ if (hasRestoredDraftRef.current || !draftScopeKey) return
+ hasRestoredDraftRef.current = true
+ const draft = useMothershipDraftsStore.getState().drafts[draftScopeKey]
+ if (!draft) return
+ if (draft.contexts?.length) {
+ contextManagement.setSelectedContexts(draft.contexts)
+ }
+ if (draft.fileAttachments?.length) {
+ files.restoreAttachedFiles(
+ draft.fileAttachments.map((a) => ({
+ id: a.id,
+ name: a.filename,
+ size: a.size,
+ type: a.media_type,
+ path: a.path ?? '',
+ key: a.key,
+ uploading: false,
+ }))
+ )
+ }
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore
+
+ useEffect(() => {
+ if (!draftScopeKeyRef.current) return
+ const fileAttachments = files.attachedFiles
+ .filter((f) => !f.uploading && f.key)
+ .map((f) => ({
+ id: f.id,
+ key: f.key!,
+ filename: f.name,
+ media_type: f.type,
+ size: f.size,
+ ...(f.path ? { path: f.path } : {}),
+ }))
+ useMothershipDraftsStore.getState().setDraft(draftScopeKeyRef.current, {
+ text: value,
+ fileAttachments: fileAttachments.length > 0 ? fileAttachments : undefined,
+ contexts:
+ contextManagement.selectedContexts.length > 0
+ ? contextManagement.selectedContexts
+ : undefined,
+ })
+ }, [value, files.attachedFiles, contextManagement.selectedContexts])
+
const onContextRemoveRef = useRef(onContextRemove)
onContextRemoveRef.current = onContextRemove
@@ -485,6 +541,9 @@ export const UserInput = forwardRef(function Us
setValue('')
valueRef.current = ''
sttPrefixRef.current = ''
+ if (draftScopeKeyRef.current) {
+ useMothershipDraftsStore.getState().clearDraft(draftScopeKeyRef.current)
+ }
resetTranscript()
currentFiles.clearAttachedFiles()
prevSelectedContextsRef.current = []
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 860307510f..b9af13e8e2 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -282,6 +282,7 @@ export function Home({ chatId }: HomeProps = {}) {
const hasMessages = messages.length > 0
const showChatSkeleton = Boolean(chatId) && !hasMessages && isChatHistoryPending
+ const draftScopeKey = `${workspaceId}:${chatId ?? 'new'}`
useEffect(() => {
if (hasMessages) return
@@ -313,6 +314,7 @@ export function Home({ chatId }: HomeProps = {}) {
setIsInputEntering(false) : undefined}
initialScrollBlocked={resources.length > 0 && isResourceCollapsed}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
index 6665bd6c99..99364fa215 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/base.tsx
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { format } from 'date-fns'
-import { AlertCircle, Loader2, Pencil, Plus, Tag, X } from 'lucide-react'
+import { AlertCircle, Pencil, Plus, Tag, X } from 'lucide-react'
import { useParams, usePathname, useRouter, useSearchParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import {
@@ -15,6 +15,7 @@ import {
DatePicker,
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -25,7 +26,6 @@ import {
} from '@/components/emcn'
import { Database, DatabaseX } from '@/components/emcn/icons'
import { SearchHighlight } from '@/components/ui/search-highlight'
-import { cn } from '@/lib/core/utils/cn'
import { ADD_CONNECTOR_SEARCH_PARAM } from '@/lib/credentials/client-state'
import { ALL_TAG_SLOTS, type AllTagSlot, getFieldTypeForSlot } from '@/lib/knowledge/constants'
import type { DocumentSortField, SortOrder } from '@/lib/knowledge/documents/types'
@@ -99,7 +99,7 @@ interface KnowledgeBaseProps {
}
const AnimatedLoader = ({ className }: { className?: string }) => (
-
+
)
const getStatusBadge = (doc: DocumentData) => {
@@ -926,7 +926,7 @@ export function KnowledgeBase({
className='flex shrink-0 cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-[var(--text-secondary)] text-caption shadow-[inset_0_0_0_1px_var(--border)] transition-colors hover-hover:bg-[var(--surface-3)]'
>
{connector.status === 'syncing' ? (
-
+
) : (
ConnectorIcon &&
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
index fc6f2b0eae..bb46a1fd7a 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-connector-modal/add-connector-modal.tsx
@@ -1,7 +1,7 @@
'use client'
import { useMemo, useState } from 'react'
-import { ArrowLeft, ArrowLeftRight, Loader2, Plus, Search } from 'lucide-react'
+import { ArrowLeft, ArrowLeftRight, Plus, Search } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Button,
@@ -12,6 +12,7 @@ import {
type ComboboxOption,
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -456,7 +457,7 @@ export function AddConnectorModal({
{isCreating ? (
<>
-
+
Connecting...
>
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx
index a731e38e0d..2a7cf162ce 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/add-documents-modal/add-documents-modal.tsx
@@ -2,11 +2,12 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { Loader2, RotateCcw, X } from 'lucide-react'
+import { RotateCcw, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Button,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -302,7 +303,7 @@ export function AddDocumentsModal({
{isProcessing ? (
-
+
) : (
<>
{isFailed && (
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
index b2b14cc536..4925883e7e 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx
@@ -1,8 +1,7 @@
'use client'
import { useMemo } from 'react'
-import { Loader2 } from 'lucide-react'
-import { Combobox, type ComboboxOption } from '@/components/emcn'
+import { Combobox, type ComboboxOption, Loader } from '@/components/emcn'
import { SELECTOR_CONTEXT_FIELDS } from '@/lib/workflows/subblocks/context'
import { getDependsOnFields } from '@/blocks/utils'
import type { ConnectorConfigField } from '@/connectors/types'
@@ -68,7 +67,7 @@ export function ConnectorSelectorField({
if (isLoading && isEnabled) {
return (
-
+
Loading...
)
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
index 1445218a64..fa06611c7d 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connectors-section/connectors-section.tsx
@@ -8,7 +8,6 @@ import {
AlertTriangle,
CheckCircle2,
ChevronDown,
- Loader2,
Pause,
Play,
RefreshCw,
@@ -20,6 +19,7 @@ import {
Badge,
Button,
Checkbox,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -368,7 +368,7 @@ function ConnectorCard({
{connectorDef?.name || connector.connectorType}
{(isSyncPending || connector.status === 'syncing') && (
-
+
)}
@@ -455,7 +455,7 @@ function ConnectorCard({
disabled={isUpdating}
>
{isUpdating ? (
-
+
) : connector.status === 'paused' || connector.status === 'disabled' ? (
) : (
@@ -620,7 +620,7 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
if (isLoading) {
return (
-
+
Loading sync history...
)
@@ -642,7 +642,7 @@ function SyncHistory({ logs, isLoading }: SyncHistoryProps) {
{isRunning ? (
-
+
) : isError ? (
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx
index 097ccfedcc..e54846e6fb 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/edit-connector-modal/edit-connector-modal.tsx
@@ -2,7 +2,7 @@
import { useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { ArrowLeftRight, ExternalLink, Loader2, RotateCcw } from 'lucide-react'
+import { ArrowLeftRight, ExternalLink, RotateCcw } from 'lucide-react'
import {
Button,
ButtonGroup,
@@ -10,6 +10,7 @@ import {
Combobox,
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -200,7 +201,7 @@ export function EditConnectorModal({
{isSaving ? (
<>
-
+
Saving...
>
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx
index 7ebe3b1cc8..4aacb8cf0b 100644
--- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/create-base-modal/create-base-modal.tsx
@@ -3,7 +3,7 @@
import { memo, useEffect, useRef, useState } from 'react'
import { zodResolver } from '@hookform/resolvers/zod'
import { createLogger } from '@sim/logger'
-import { Loader2, RotateCcw, X } from 'lucide-react'
+import { RotateCcw, X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
@@ -13,6 +13,7 @@ import {
type ComboboxOption,
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -581,7 +582,7 @@ export const CreateBaseModal = memo(function CreateBaseModal({
{isProcessing ? (
-
+
) : (
<>
{isFailed && (
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
index e69f66e01b..c4a215be3e 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx
@@ -1,10 +1,9 @@
'use client'
import { memo, useCallback, useMemo, useRef, useState } from 'react'
-import { Loader2 } from 'lucide-react'
import { useParams } from 'next/navigation'
import { useShallow } from 'zustand/react/shallow'
-import { Skeleton } from '@/components/emcn'
+import { Loader, Skeleton } from '@/components/emcn'
import { formatLatency } from '@/app/workspace/[workspaceId]/logs/utils'
import type { DashboardStatsResponse, WorkflowStats } from '@/hooks/queries/logs'
import { useWorkflows } from '@/hooks/queries/workflows'
@@ -500,7 +499,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
/>
) : (
-
+
)}
@@ -527,7 +526,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
/>
) : (
-
+
)}
@@ -554,7 +553,7 @@ function DashboardInner({ stats, isLoading, error }: DashboardProps) {
/>
) : (
-
+
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx
index 3a2c2af0d4..75e6d68f9a 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/execution-snapshot/execution-snapshot.tsx
@@ -2,7 +2,7 @@
import type React from 'react'
import { useRef, useState } from 'react'
-import { AlertCircle, Loader2 } from 'lucide-react'
+import { AlertCircle } from 'lucide-react'
import { createPortal } from 'react-dom'
import {
Copy,
@@ -10,6 +10,7 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -94,7 +95,7 @@ export function ExecutionSnapshot({
style={{ height, width }}
>
-
+
Loading run snapshot...
@@ -122,7 +123,7 @@ export function ExecutionSnapshot({
style={{ height, width }}
>
-
+
Loading run snapshot...
diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx
index dd538f5087..23670cf2c0 100644
--- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/file-download/file-download.tsx
@@ -2,9 +2,9 @@
import { useState } from 'react'
import { createLogger } from '@sim/logger'
-import { ArrowDown, Loader2 } from 'lucide-react'
+import { ArrowDown } from 'lucide-react'
import { useRouter } from 'next/navigation'
-import { Button } from '@/components/emcn'
+import { Button, Loader } from '@/components/emcn'
import { extractWorkspaceIdFromExecutionKey, getViewerUrl } from '@/lib/uploads/utils/file-utils'
const logger = createLogger('FileCards')
@@ -118,7 +118,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
disabled={isDownloading}
>
{isDownloading ? (
-
+
) : (
)}
@@ -225,7 +225,7 @@ export function FileDownload({
disabled={isDownloading}
>
{isDownloading ? (
-
+
) : (
)}
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 5dc4025b2f..e8dd1d912b 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
@@ -2,10 +2,10 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { formatDuration } from '@sim/utils/formatting'
-import { ArrowUpRight, Loader2 } from 'lucide-react'
+import { ArrowUpRight } from 'lucide-react'
import Link from 'next/link'
import { List, type RowComponentProps, useListRef } from 'react-window'
-import { Badge, buttonVariants } from '@/components/emcn'
+import { Badge, buttonVariants, Loader } from '@/components/emcn'
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
import { cn } from '@/lib/core/utils/cn'
import { workflowBorderColor } from '@/lib/workspaces/colors'
@@ -196,7 +196,7 @@ function Row({
{isFetchingNextPage ? (
<>
-
+
Loading more...
>
) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/skills/components/skill-import.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/skills/components/skill-import.tsx
index ed137eba30..0fa9a24223 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/skills/components/skill-import.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/skills/components/skill-import.tsx
@@ -2,8 +2,7 @@
import type { ChangeEvent } from 'react'
import { useCallback, useRef, useState } from 'react'
-import { Loader2 } from 'lucide-react'
-import { Button, Input, Label, Textarea } from '@/components/emcn'
+import { Button, Input, Label, Loader, Textarea } from '@/components/emcn'
import { Upload } from '@/components/emcn/icons'
import { requestJson } from '@/lib/api/client/request'
import { importSkillContract } from '@/lib/api/contracts'
@@ -183,7 +182,7 @@ export function SkillImport({ onImport }: SkillImportProps) {
className='hidden'
/>
{fileState === 'loading' ? (
-
+
) : (
)}
@@ -223,11 +222,7 @@ export function SkillImport({ onImport }: SkillImportProps) {
onClick={handleGithubImport}
disabled={githubState === 'loading' || !githubUrl.trim()}
>
- {githubState === 'loading' ? (
-
- ) : (
- 'Fetch'
- )}
+ {githubState === 'loading' ? : 'Fetch'}
{githubError && {githubError}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
index a4dca9a66d..f3acdc31b6 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx
@@ -2,13 +2,14 @@
import { useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { AlertTriangle, Check, Clipboard, Eye, EyeOff, Loader2, RefreshCw } from 'lucide-react'
+import { AlertTriangle, Check, Clipboard, Eye, EyeOff, RefreshCw } from 'lucide-react'
import {
Button,
ButtonGroup,
ButtonGroupItem,
Input,
Label,
+ Loader,
Modal,
ModalBody,
ModalContent,
@@ -548,7 +549,7 @@ function IdentifierInput({
/>
{isChecking ? (
-
+
) : (
isValid &&
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
index 6adf800473..e6a9f026b4 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx
@@ -2,12 +2,13 @@
import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { Check, Eye, EyeOff, Loader2 } from 'lucide-react'
+import { Check, Eye, EyeOff } from 'lucide-react'
import {
ButtonGroup,
ButtonGroupItem,
Input,
Label,
+ Loader,
Skeleton,
TagInput,
type TagItem,
@@ -378,7 +379,7 @@ export function FormDeploy({
/>
{isCheckingIdentifier ? (
-
+
) : (
identifierValidationPassed &&
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
index 24a0975325..575bf13721 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx
@@ -1,8 +1,7 @@
'use client'
import { useState } from 'react'
-import { Loader2 } from 'lucide-react'
-import { Button, Tooltip } from '@/components/emcn'
+import { Button, Loader, Tooltip } from '@/components/emcn'
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
import {
useChangeDetection,
@@ -94,7 +93,7 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
onClick={onDeployClick}
disabled={isRegistryLoading || isDisabled}
>
- {isDeploying && }
+ {isDeploying && }
{changeDetected ? 'Update' : isDeployed ? 'Live' : 'Deploy'}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx
index c2b9154a52..a07564db3d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx
@@ -1,13 +1,14 @@
import type React from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { ArrowLeft, ChevronRight, Loader2, ServerIcon, WrenchIcon, XIcon } from 'lucide-react'
+import { ArrowLeft, ChevronRight, ServerIcon, WrenchIcon, XIcon } from 'lucide-react'
import { useParams } from 'next/navigation'
import {
Badge,
Combobox,
type ComboboxOption,
type ComboboxOptionGroup,
+ Loader,
Popover,
PopoverContent,
PopoverItem,
@@ -168,7 +169,7 @@ function WorkflowInputMapperInput({
if (isLoading) {
return (
-
+
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
index 2423215f82..52c3a78084 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx
@@ -8,7 +8,6 @@ import {
ChevronDown,
ChevronUp,
ExternalLink,
- Loader2,
Lock,
Pencil,
Unlock,
@@ -17,7 +16,7 @@ import { useParams } from 'next/navigation'
import { usePostHog } from 'posthog-js/react'
import { useShallow } from 'zustand/react/shallow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
-import { Button, Tooltip } from '@/components/emcn'
+import { Button, Loader, Tooltip } from '@/components/emcn'
import { captureEvent } from '@/lib/posthog/client'
import {
buildCanonicalIndex,
@@ -498,7 +497,7 @@ export function Editor() {
{isLoadingChildWorkflow ? (
-
+
) : childWorkflowState ? (
<>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx
index 780ca6ebd1..42850b37bb 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/permissions-table.tsx
@@ -1,6 +1,6 @@
import { useMemo, useRef } from 'react'
-import { Loader2, RotateCw, X } from 'lucide-react'
-import { Badge, Button, Skeleton, Tooltip } from '@/components/emcn'
+import { RotateCw, X } from 'lucide-react'
+import { Badge, Button, Loader, Skeleton, Tooltip } from '@/components/emcn'
import { PermissionSelector } from '@/components/permissions'
import { useSession } from '@/lib/auth/auth-client'
import type { PermissionType } from '@/lib/workspaces/permissions/utils'
@@ -147,7 +147,7 @@ export const PermissionsTable = ({
-
+
Saving permission changes...
@@ -243,7 +243,7 @@ export const PermissionsTable = ({
className='h-[16px] w-[16px] p-0'
>
{resendingInvitationIds?.[user.invitationId!] ? (
-
+
) : (
)}
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 21078b9e4f..f3cb0a35bf 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -90,6 +90,7 @@ import { useFolderMap, useFolders } from '@/hooks/queries/folders'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import { useTablesList } from '@/hooks/queries/tables'
import {
+ useCreateTask,
useDeleteTask,
useDeleteTasks,
useMarkTaskRead,
@@ -197,7 +198,6 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
(isCurrentRoute || isSelected || isMenuOpen) && 'bg-[var(--surface-active)]'
)}
onClick={(e) => {
- if (task.id === 'new') return
if (e.metaKey || e.ctrlKey) return
if (e.shiftKey) {
e.preventDefault()
@@ -206,42 +206,40 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
useFolderStore.getState().selectTaskOnly(task.id)
}
}}
- onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined}
- draggable={task.id !== 'new'}
- onDragStart={task.id !== 'new' ? handleDragStart : undefined}
- onDragEnd={task.id !== 'new' ? handleDragEnd : undefined}
+ onContextMenu={(e) => onContextMenu(e, task.id)}
+ draggable
+ onDragStart={handleDragStart}
+ onDragEnd={handleDragEnd}
>
{task.name}
- {task.id !== 'new' && (
-
- {isActive && !isCurrentRoute && !isMenuOpen && (
-
- )}
- {isActive && !isCurrentRoute && !isMenuOpen && (
-
- )}
- {!isActive && isUnread && !isCurrentRoute && !isMenuOpen && (
-
+
+ {isActive && !isCurrentRoute && !isMenuOpen && (
+
+ )}
+ {isActive && !isCurrentRoute && !isMenuOpen && (
+
+ )}
+ {!isActive && isUnread && !isCurrentRoute && !isMenuOpen && (
+
+ )}
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ onMoreClick(e, task.id)
+ }}
+ className={cn(
+ 'flex h-[18px] w-[18px] items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100',
+ isMenuOpen && 'opacity-100'
)}
- {
- e.preventDefault()
- e.stopPropagation()
- onMoreClick(e, task.id)
- }}
- className={cn(
- 'flex h-[18px] w-[18px] items-center justify-center rounded-sm opacity-0 transition-opacity group-hover:opacity-100',
- isMenuOpen && 'opacity-100'
- )}
- >
-
-
-
- )}
+ >
+
+
+
)
@@ -586,6 +584,7 @@ export const Sidebar = memo(function Sidebar() {
}
}, [activeNavItemHref])
+ const createTaskMutation = useCreateTask(workspaceId)
const deleteTaskMutation = useDeleteTask(workspaceId)
const deleteTasksMutation = useDeleteTasks(workspaceId)
const markTaskReadMutation = useMarkTaskRead(workspaceId)
@@ -796,20 +795,12 @@ export const Sidebar = memo(function Sidebar() {
const tasks = useMemo(
() =>
- fetchedTasks && fetchedTasks.length > 0
+ fetchedTasks
? fetchedTasks.map((t) => ({
...t,
href: `/workspace/${workspaceId}/task/${t.id}`,
}))
- : [
- {
- id: 'new',
- name: 'New task',
- href: `/workspace/${workspaceId}/home`,
- isActive: false,
- isUnread: false,
- },
- ],
+ : [],
[fetchedTasks, workspaceId]
)
@@ -853,7 +844,7 @@ export const Sidebar = memo(function Sidebar() {
[fetchedKnowledgeBases, workspaceId, permissionConfig.hideKnowledgeBaseTab]
)
- const taskIds = useMemo(() => tasks.map((t) => t.id).filter((id) => id !== 'new'), [tasks])
+ const taskIds = useMemo(() => tasks.map((t) => t.id), [tasks])
const { selectedTasks, handleTaskClick } = useTaskSelection({ taskIds })
@@ -1155,14 +1146,6 @@ export const Sidebar = memo(function Sidebar() {
[]
)
- const tasksPrimaryAction = useMemo(
- () => ({
- label: 'New task',
- onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`),
- }),
- [navigateToPage, workspaceId]
- )
-
const workflowsPrimaryAction = useMemo(
() => ({
label: 'New workflow',
@@ -1176,7 +1159,23 @@ export const Sidebar = memo(function Sidebar() {
toggleCollapsed()
}
- const handleNewTask = () => navigateToPage(`/workspace/${workspaceId}/home`)
+ const handleNewTask = useCallback(async () => {
+ if (!workspaceId) return
+ try {
+ const { id } = await createTaskMutation.mutateAsync()
+ navigateToPage(`/workspace/${workspaceId}/task/${id}`)
+ } catch {
+ navigateToPage(`/workspace/${workspaceId}/home`)
+ }
+ }, [workspaceId, navigateToPage])
+
+ const tasksPrimaryAction = useMemo(
+ () => ({
+ label: 'New task',
+ onSelect: handleNewTask,
+ }),
+ [handleNewTask]
+ )
const handleSeeMoreTasks = () => setVisibleTaskCount((prev) => prev + 5)
@@ -1462,6 +1461,7 @@ export const Sidebar = memo(function Sidebar() {
variant='ghost'
className='h-[18px] w-[18px] rounded-sm p-0 hover-hover:bg-[var(--surface-hover)]'
onClick={handleNewTask}
+ disabled={createTaskMutation.isPending}
>
@@ -1493,7 +1493,7 @@ export const Sidebar = memo(function Sidebar() {
{tasks.slice(0, visibleTaskCount).map((task) => {
- const isCurrentRoute = task.id !== 'new' && pathname === task.href
+ const isCurrentRoute = pathname === task.href
const isRenaming = taskFlyoutRename.editingId === task.id
- const isSelected = task.id !== 'new' && selectedTasks.has(task.id)
+ const isSelected = selectedTasks.has(task.id)
if (isRenaming) {
return (
diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx
index 7a8d1a03af..c4110ddf33 100644
--- a/apps/sim/components/emcn/components/combobox/combobox.tsx
+++ b/apps/sim/components/emcn/components/combobox/combobox.tsx
@@ -15,8 +15,9 @@ import {
useState,
} from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
-import { Check, ChevronDown, Loader2, Search } from 'lucide-react'
+import { Check, ChevronDown, Search } from 'lucide-react'
import { cn } from '@/lib/core/utils/cn'
+import { Loader } from '../../icons'
import { Input } from '../input/input'
import { Popover, PopoverAnchor, PopoverContent, PopoverScrollArea } from '../popover/popover'
@@ -689,7 +690,7 @@ const Combobox = memo(
{isLoading ? (
-
+
Loading options...
diff --git a/apps/sim/components/emcn/icons/clipboard.tsx b/apps/sim/components/emcn/icons/clipboard.tsx
new file mode 100644
index 0000000000..82281af737
--- /dev/null
+++ b/apps/sim/components/emcn/icons/clipboard.tsx
@@ -0,0 +1,26 @@
+import type { SVGProps } from 'react'
+
+/**
+ * Clipboard icon component
+ * @param props - SVG properties including className, fill, etc.
+ */
+export function Clipboard(props: SVGProps
) {
+ return (
+
+ )
+}
diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts
index 5baf3dd57e..25bc05802a 100644
--- a/apps/sim/components/emcn/icons/index.ts
+++ b/apps/sim/components/emcn/icons/index.ts
@@ -14,6 +14,7 @@ export { Calendar } from './calendar'
export { Card } from './card'
export { Check } from './check'
export { ChevronDown } from './chevron-down'
+export { Clipboard } from './clipboard'
export { ClipboardList } from './clipboard-list'
export { Columns2 } from './columns2'
export { Columns3 } from './columns3'
diff --git a/apps/sim/ee/sso/components/sso-auth.tsx b/apps/sim/ee/sso/components/sso-auth.tsx
index cf83249fe2..43a18d0b85 100644
--- a/apps/sim/ee/sso/components/sso-auth.tsx
+++ b/apps/sim/ee/sso/components/sso-auth.tsx
@@ -2,9 +2,8 @@
import { type KeyboardEvent, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { Loader2 } from 'lucide-react'
import { useRouter } from 'next/navigation'
-import { Input, Label } from '@/components/emcn'
+import { Input, Label, Loader } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import AuthBackground from '@/app/(auth)/components/auth-background'
@@ -156,7 +155,7 @@ export default function SSOAuth({ identifier }: SSOAuthProps) {
{isLoading ? (
-
+
Redirecting to SSO...
) : (
diff --git a/apps/sim/ee/sso/components/sso-form.tsx b/apps/sim/ee/sso/components/sso-form.tsx
index 3352d16e3e..14dc60d9a8 100644
--- a/apps/sim/ee/sso/components/sso-form.tsx
+++ b/apps/sim/ee/sso/components/sso-form.tsx
@@ -2,10 +2,9 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
-import { Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useRouter, useSearchParams } from 'next/navigation'
-import { Button, Input, Label } from '@/components/emcn'
+import { Button, Input, Label, Loader } from '@/components/emcn'
import { client } from '@/lib/auth/auth-client'
import { env, isFalsy } from '@/lib/core/config/env'
import { validateCallbackUrl } from '@/lib/core/security/input-validation'
@@ -185,7 +184,7 @@ export default function SSOForm() {
{isLoading ? (
-
+
Redirecting to SSO provider...
) : (
diff --git a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
index c42b210bea..49c0c762da 100644
--- a/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
+++ b/apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx
@@ -3,10 +3,10 @@
import { useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
-import { Loader2, X } from 'lucide-react'
+import { X } from 'lucide-react'
import Image from 'next/image'
import { useParams } from 'next/navigation'
-import { Button, Input, Label, Skeleton, toast } from '@/components/emcn'
+import { Button, Input, Label, Loader, Skeleton, toast } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
import { HEX_COLOR_REGEX } from '@/lib/branding'
@@ -335,7 +335,7 @@ export function WhitelabelingSettings() {
className='group relative flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50'
>
{logoUpload.isUploading ? (
-
+
) : logoUpload.previewUrl ? (
{wordmarkUpload.isUploading ? (
-
+
) : wordmarkUpload.previewUrl ? (
{
+ const response = await fetch('/api/mothership/chats', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ workspaceId }),
+ })
+ if (!response.ok) throw new Error('Failed to create chat')
+ const data = await response.json()
+ return { id: data.id }
+}
+
+export function useCreateTask(workspaceId?: string) {
+ const queryClient = useQueryClient()
+ return useMutation({
+ mutationFn: () => {
+ if (!workspaceId) throw new Error('workspaceId is required')
+ return createChat(workspaceId)
+ },
+ onSuccess: (data) => {
+ const existing = queryClient.getQueryData(taskKeys.list(workspaceId)) ?? []
+ const newTask: TaskMetadata = {
+ id: data.id,
+ name: 'New task',
+ updatedAt: new Date(),
+ isActive: false,
+ isUnread: false,
+ }
+ queryClient.setQueryData(taskKeys.list(workspaceId), [newTask, ...existing])
+ },
+ onSettled: () => {
+ queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
+ },
+ })
+}
+
async function forkChat(params: {
chatId: string
upToMessageId: string
diff --git a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts
index 53e698a6c1..f9c63d1302 100644
--- a/apps/sim/lib/copilot/request/go/file-preview-adapter.ts
+++ b/apps/sim/lib/copilot/request/go/file-preview-adapter.ts
@@ -331,7 +331,7 @@ export async function processFilePreviewStreamEvent(input: {
...(edit ? { edit } : {}),
}
- if (!isDocFormat(fileName) && isContentOp && previewTargetKind) {
+ if (isContentOp && previewTargetKind) {
let previewBaseContent: string | undefined
if (
execContext.workspaceId &&
@@ -445,7 +445,7 @@ export async function processFilePreviewStreamEvent(input: {
const stateForTool = editContentState.get(toolCallId) ?? { raw: '' }
stateForTool.raw += delta
- if (context.activeFileIntent && !isDocFormat(context.activeFileIntent.target.fileName)) {
+ if (context.activeFileIntent) {
const streamedContent = extractEditContent(stateForTool.raw)
if (streamedContent !== (stateForTool.lastContentSnapshot ?? '')) {
stateForTool.lastContentSnapshot = streamedContent
diff --git a/apps/sim/lib/copilot/tools/client/store-utils.ts b/apps/sim/lib/copilot/tools/client/store-utils.ts
index e1c04abb17..9ec8848967 100644
--- a/apps/sim/lib/copilot/tools/client/store-utils.ts
+++ b/apps/sim/lib/copilot/tools/client/store-utils.ts
@@ -1,5 +1,6 @@
-import type { LucideIcon } from 'lucide-react'
-import { FileText, Loader2 } from 'lucide-react'
+import type { ComponentType } from 'react'
+import { FileText } from 'lucide-react'
+import { Loader } from '@/components/emcn'
import { Read as ReadTool } from '@/lib/copilot/generated/tool-catalog-v1'
import { VFS_DIR_TO_RESOURCE } from '@/lib/copilot/resources/types'
import { isToolHiddenInUi } from '@/lib/copilot/tools/client/hidden-tools'
@@ -11,7 +12,7 @@ const INTERNAL_RESPOND_TOOL = 'respond'
interface ClientToolDisplay {
text: string
- icon: LucideIcon
+ icon: ComponentType<{ className?: string }>
}
export function resolveToolDisplay(
@@ -36,7 +37,7 @@ function specialToolDisplay(
if (toolName === INTERNAL_RESPOND_TOOL || toolName.endsWith(HIDDEN_TOOL_SUFFIX)) {
return {
text: formatRespondLabel(state),
- icon: Loader2,
+ icon: Loader,
}
}
@@ -135,5 +136,5 @@ function humanizedFallback(
: state === ClientToolCallState.rejected || state === ClientToolCallState.aborted
? 'Skipped'
: 'Executing'
- return { text: `${stateVerb} ${formattedName}`, icon: Loader2 }
+ return { text: `${stateVerb} ${formattedName}`, icon: Loader }
}
diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts
index 46535c23a6..71d4c54ba2 100644
--- a/apps/sim/stores/index.ts
+++ b/apps/sim/stores/index.ts
@@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { environmentKeys } from '@/hooks/queries/environment'
import { useExecutionStore } from '@/stores/execution'
+import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import { consolePersistence, useTerminalConsoleStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -37,6 +38,7 @@ export const resetAllStores = () => {
isOpen: false,
})
consolePersistence.persist()
+ useMothershipDraftsStore.setState({ drafts: {} })
}
/**
diff --git a/apps/sim/stores/mothership-drafts/store.ts b/apps/sim/stores/mothership-drafts/store.ts
new file mode 100644
index 0000000000..5e98334b69
--- /dev/null
+++ b/apps/sim/stores/mothership-drafts/store.ts
@@ -0,0 +1,50 @@
+import { create } from 'zustand'
+import { devtools, persist } from 'zustand/middleware'
+import type { FileAttachmentForApi } from '@/app/workspace/[workspaceId]/home/types'
+import type { ChatContext } from '@/stores/panel'
+
+export interface DraftPayload {
+ text: string
+ fileAttachments?: FileAttachmentForApi[]
+ contexts?: ChatContext[]
+}
+
+interface MothershipDraftsState {
+ drafts: Record
+ setDraft: (key: string, payload: DraftPayload) => void
+ clearDraft: (key: string) => void
+}
+
+function isEmpty(payload: DraftPayload): boolean {
+ return !payload.text && !payload.fileAttachments?.length && !payload.contexts?.length
+}
+
+export const useMothershipDraftsStore = create()(
+ devtools(
+ persist(
+ (set) => ({
+ drafts: {},
+ setDraft: (key, payload) =>
+ set((s) => {
+ if (isEmpty(payload)) {
+ if (!(key in s.drafts)) return s
+ const { [key]: _, ...rest } = s.drafts
+ return { drafts: rest }
+ }
+ return { drafts: { ...s.drafts, [key]: payload } }
+ }),
+ clearDraft: (key) =>
+ set((s) => {
+ if (!(key in s.drafts)) return s
+ const { [key]: _, ...rest } = s.drafts
+ return { drafts: rest }
+ }),
+ }),
+ {
+ name: 'mothership-drafts:v1',
+ partialize: (state) => ({ drafts: state.drafts }),
+ }
+ ),
+ { name: 'mothership-drafts-store' }
+ )
+)
From c7f9d8058bb1e8760c202578dbf6c1c0c26f34a2 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:16:01 -0700
Subject: [PATCH 02/12] fix(files): dispose onContextMenu listener and handle
clipboard rejection
---
.../components/file-viewer/text-editor.tsx | 29 ++++++++++---------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
index af75b820c1..bb28310823 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
@@ -174,30 +174,30 @@ const MonacoEditor = dynamic(
colors: {
'editor.background': '#fefefe',
'editor.foreground': '#1a1a1a',
- 'editorLineNumber.foreground': '#cccccc',
+ 'editorLineNumber.foreground': '#bbbbbb',
'editorLineNumber.activeForeground': '#707070',
- 'editor.selectionBackground': '#33b4ff22',
- 'editor.inactiveSelectionBackground': '#33b4ff12',
+ 'editor.selectionBackground': '#0078d430',
+ 'editor.inactiveSelectionBackground': '#0078d418',
'editor.lineHighlightBackground': '#f7f7f7',
'editor.lineHighlightBorder': '#00000000',
'editorGutter.background': '#fefefe',
'editorWidget.background': '#ffffff',
'editorWidget.border': '#dedede',
'editorWidget.foreground': '#1a1a1a',
- 'editor.findMatchBackground': '#33b4ff40',
- 'editor.findMatchHighlightBackground': '#33b4ff1a',
- 'editor.findMatchBorder': '#33b4ff',
+ 'editor.findMatchBackground': '#0078d428',
+ 'editor.findMatchHighlightBackground': '#0078d414',
+ 'editor.findMatchBorder': '#0078d4',
'scrollbar.shadow': '#00000000',
'scrollbarSlider.background': '#dedede80',
'scrollbarSlider.hoverBackground': '#cccccc',
'scrollbarSlider.activeBackground': '#b0b0b0',
- 'editorBracketMatch.background': '#33b4ff1a',
- 'editorBracketMatch.border': '#33b4ff80',
+ 'editorBracketMatch.background': '#0078d418',
+ 'editorBracketMatch.border': '#0078d480',
'editorIndentGuide.background1': '#f0f0f0',
'editorIndentGuide.activeBackground1': '#d8d8d8',
'editorCursor.foreground': '#1a1a1a',
- 'editor.wordHighlightBackground': '#33b4ff14',
- 'editor.wordHighlightBorder': '#33b4ff40',
+ 'editor.wordHighlightBackground': '#0078d414',
+ 'editor.wordHighlightBorder': '#0078d450',
'editorSuggestWidget.background': '#ffffff',
'editorSuggestWidget.border': '#dedede',
'editorSuggestWidget.foreground': '#1a1a1a',
@@ -208,11 +208,11 @@ const MonacoEditor = dynamic(
'editorHoverWidget.foreground': '#1a1a1a',
'minimap.background': '#fefefe',
'minimapSlider.background': '#dedede80',
- focusBorder: '#33b4ff80',
+ focusBorder: '#0078d480',
'input.background': '#ffffff',
'input.border': '#dedede',
'input.foreground': '#1a1a1a',
- 'inputOption.activeBorder': '#33b4ff',
+ 'inputOption.activeBorder': '#0078d4',
},
})
@@ -626,7 +626,7 @@ export const TextEditor = memo(function TextEditor({
editor.focus()
}
- editor.onContextMenu((e) => {
+ const contextMenuDisposable = editor.onContextMenu((e) => {
e.event.preventDefault()
const sel = editor.getSelection()
setContextMenu({
@@ -635,6 +635,7 @@ export const TextEditor = memo(function TextEditor({
hasSelection: sel !== null && !sel.isEmpty(),
})
})
+ editor.onDidDispose(() => contextMenuDisposable.dispose())
}
const handleEditorChange = useCallback(
@@ -783,7 +784,7 @@ export const TextEditor = memo(function TextEditor({
closeContextMenu()
}}
onCopyAll={() => {
- navigator.clipboard.writeText(monacoEditorRef.current?.getValue() ?? '')
+ navigator.clipboard.writeText(monacoEditorRef.current?.getValue() ?? '').catch(() => {})
closeContextMenu()
}}
onPaste={() => {
From 5ed8a8edfae711d41751bb552048186527752b0a Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:17:49 -0700
Subject: [PATCH 03/12] fix(tasks): clear workspaceId:new draft after eager
task creation
---
.../workspace/[workspaceId]/w/components/sidebar/sidebar.tsx | 2 ++
1 file changed, 2 insertions(+)
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 f3cb0a35bf..12a8808a27 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -107,6 +107,7 @@ import { useTaskEvents } from '@/hooks/use-task-events'
import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useFolderStore } from '@/stores/folders/store'
import { useSearchModalStore } from '@/stores/modals/search/store'
+import { useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import { useSidebarStore } from '@/stores/sidebar/store'
const logger = createLogger('Sidebar')
@@ -1163,6 +1164,7 @@ export const Sidebar = memo(function Sidebar() {
if (!workspaceId) return
try {
const { id } = await createTaskMutation.mutateAsync()
+ useMothershipDraftsStore.getState().clearDraft(`${workspaceId}:new`)
navigateToPage(`/workspace/${workspaceId}/task/${id}`)
} catch {
navigateToPage(`/workspace/${workspaceId}/home`)
From 173c1d6edc34d2f3c3cacafb23398594a2d099f4 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:18:03 -0700
Subject: [PATCH 04/12] fix(user-input): always update prevDefaultValueRef on
defaultValue change
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Prevents stale ref when defaultValue transitions through empty — if
defaultValue goes non-empty → empty → same non-empty value, the ref
was not updated on the empty transition, causing setValue to be skipped
on the subsequent change.
Also removes unnecessary useCallback wrapping from handleSendQueuedHead,
handleEditQueued, handleEditQueuedTail in MothershipChat — none have a
reference observer (UserInput stores them via refs, QueuedMessages is
not React.memo-wrapped).
---
.../[workspaceId]/home/components/user-input/user-input.tsx | 3 +--
1 file changed, 1 insertion(+), 2 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 0defe05a9c..4c23549d19 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
@@ -152,10 +152,9 @@ export const UserInput = forwardRef(function Us
const prevDefaultValueRef = useRef(defaultValue)
useEffect(() => {
- if (!defaultValue) return
if (defaultValue === prevDefaultValueRef.current) return
prevDefaultValueRef.current = defaultValue
- setValue(defaultValue)
+ if (defaultValue) setValue(defaultValue)
}, [defaultValue])
const files = useFileAttachments({
From 3d43a4b6aaf1b95d3d5f14749fdac74ec3092a63 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:48:17 -0700
Subject: [PATCH 05/12] fix(files): improve light mode number token contrast to
4.8x
---
.../files/components/file-viewer/text-editor.tsx | 12 +++---------
1 file changed, 3 insertions(+), 9 deletions(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
index bb28310823..c2711bb181 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/text-editor.tsx
@@ -24,7 +24,6 @@ import {
} from './text-editor-state'
const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
- // Core
{ token: 'comment', foreground: '606060', fontStyle: 'italic' },
{ token: 'string', foreground: '3ab872' },
{ token: 'string.escape', foreground: '3ab872' },
@@ -43,9 +42,6 @@ const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
{ token: 'tag', foreground: '33b4ff' },
{ token: 'attribute.name', foreground: '8fc7f5' },
{ token: 'attribute.value', foreground: '3ab872' },
- // Markdown — Monaco Monarch emits these (tokenPostfix: ".md")
- // `keyword.md` covers headings + list markers (already caught by `keyword` above)
- // `comment.md` covers blockquotes (already caught by `comment` above)
{ token: 'strong', foreground: 'e6e6e6', fontStyle: 'bold' },
{ token: 'emphasis', foreground: 'c8c8c8', fontStyle: 'italic' },
{ token: 'variable', foreground: '3ab872' },
@@ -54,14 +50,13 @@ const SIM_DARK_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
]
const SIM_LIGHT_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
- // Core
{ token: 'comment', foreground: '888888', fontStyle: 'italic' },
{ token: 'string', foreground: '16825d' },
{ token: 'string.escape', foreground: '16825d' },
{ token: 'string.link', foreground: '0078d4' },
- { token: 'number', foreground: 'c9660c' },
- { token: 'number.float', foreground: 'c9660c' },
- { token: 'number.hex', foreground: 'c9660c' },
+ { token: 'number', foreground: 'a85500' },
+ { token: 'number.float', foreground: 'a85500' },
+ { token: 'number.hex', foreground: 'a85500' },
{ token: 'keyword', foreground: '0078d4' },
{ token: 'keyword.control', foreground: '0078d4' },
{ token: 'storage', foreground: '0078d4' },
@@ -73,7 +68,6 @@ const SIM_LIGHT_RULES: MonacoEditorTypes.ITokenThemeRule[] = [
{ token: 'tag', foreground: '0078d4' },
{ token: 'attribute.name', foreground: '7c4dcc' },
{ token: 'attribute.value', foreground: '16825d' },
- // Markdown — Monaco Monarch emits these (tokenPostfix: ".md")
{ token: 'strong', foreground: '1a1a1a', fontStyle: 'bold' },
{ token: 'emphasis', foreground: '444444', fontStyle: 'italic' },
{ token: 'variable', foreground: '16825d' },
From f83b43e9f0db859993c4b1f9d563c8c3d9e9712f Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:50:23 -0700
Subject: [PATCH 06/12] fix(tasks): annotate raw fetch in createChat for
boundary check
---
apps/sim/hooks/queries/tasks.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts
index e759e87a02..75257d1427 100644
--- a/apps/sim/hooks/queries/tasks.ts
+++ b/apps/sim/hooks/queries/tasks.ts
@@ -527,6 +527,7 @@ export function useMarkTaskUnread(workspaceId?: string) {
}
async function createChat(workspaceId: string): Promise<{ id: string }> {
+ // boundary-raw-fetch: fire-and-forget POST inside a mutation, no shared request helper for this endpoint
const response = await fetch('/api/mothership/chats', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
From 69b9b40d32510a24645b68da30b7cca132621093 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 12:52:17 -0700
Subject: [PATCH 07/12] fix(tasks): use requestJson with
createMothershipChatContract instead of raw fetch
---
apps/sim/hooks/queries/tasks.ts | 12 +++---------
1 file changed, 3 insertions(+), 9 deletions(-)
diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts
index 75257d1427..44f5a27a2f 100644
--- a/apps/sim/hooks/queries/tasks.ts
+++ b/apps/sim/hooks/queries/tasks.ts
@@ -2,6 +2,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import {
addMothershipChatResourceContract,
+ createMothershipChatContract,
deleteMothershipChatContract,
forkMothershipChatContract,
listMothershipChatsContract,
@@ -527,15 +528,8 @@ export function useMarkTaskUnread(workspaceId?: string) {
}
async function createChat(workspaceId: string): Promise<{ id: string }> {
- // boundary-raw-fetch: fire-and-forget POST inside a mutation, no shared request helper for this endpoint
- const response = await fetch('/api/mothership/chats', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ workspaceId }),
- })
- if (!response.ok) throw new Error('Failed to create chat')
- const data = await response.json()
- return { id: data.id }
+ const { id } = await requestJson(createMothershipChatContract, { body: { workspaceId } })
+ return { id }
}
export function useCreateTask(workspaceId?: string) {
From 57ca584ad7a97804e981df557fece6e9e3caba93 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 13:00:34 -0700
Subject: [PATCH 08/12] fix(cleanup): remove cancelQueries from onSuccess,
remove redundant Scissors className
---
.../files/components/file-viewer/editor-context-menu.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
index e0eeab3b7e..e2ce733629 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/editor-context-menu.tsx
@@ -62,7 +62,7 @@ export function EditorContextMenu({
>
{canEdit && (
-
+
Cut
⌘X
From c6f0195f71d6e9c04ad5263bd4d5f25f9eb4e047 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 13:02:46 -0700
Subject: [PATCH 09/12] fix(tasks): guard handleNewTask against concurrent
calls via ref lock
---
.../[workspaceId]/w/components/sidebar/sidebar.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
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 12a8808a27..8980c5b1a0 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -586,6 +586,7 @@ export const Sidebar = memo(function Sidebar() {
}, [activeNavItemHref])
const createTaskMutation = useCreateTask(workspaceId)
+ const isCreatingTaskRef = useRef(false)
const deleteTaskMutation = useDeleteTask(workspaceId)
const deleteTasksMutation = useDeleteTasks(workspaceId)
const markTaskReadMutation = useMarkTaskRead(workspaceId)
@@ -1161,13 +1162,16 @@ export const Sidebar = memo(function Sidebar() {
}
const handleNewTask = useCallback(async () => {
- if (!workspaceId) return
+ if (!workspaceId || isCreatingTaskRef.current) return
+ isCreatingTaskRef.current = true
try {
const { id } = await createTaskMutation.mutateAsync()
useMothershipDraftsStore.getState().clearDraft(`${workspaceId}:new`)
navigateToPage(`/workspace/${workspaceId}/task/${id}`)
} catch {
navigateToPage(`/workspace/${workspaceId}/home`)
+ } finally {
+ isCreatingTaskRef.current = false
}
}, [workspaceId, navigateToPage])
From 3d11d593b79223c2ed37f603e1abfd4d1a439d02 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 13:05:28 -0700
Subject: [PATCH 10/12] fix(drafts): guard save effect against first-render
race that wipes file-only drafts
On mount the save effect fired before the restore effect's setState calls
propagated, so fileAttachments and contexts were still empty. For a draft
with files but no text this caused isEmpty() to return true and setDraft to
delete the entry. Added isFirstSaveRef to skip the initial run; the restore
triggers a re-render that fires the save with the full, correct payload.
---
.../home/components/user-input/user-input.tsx | 7 +++++++
1 file changed, 7 insertions(+)
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 4c23549d19..6241696e99 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
@@ -205,7 +205,14 @@ export const UserInput = forwardRef(function Us
}
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- intentional mount-only restore
+ // Skip the initial save — restore's setState calls haven't propagated yet, so
+ // files/contexts are still empty and would transiently wipe a file-only draft.
+ const isFirstSaveRef = useRef(true)
useEffect(() => {
+ if (isFirstSaveRef.current) {
+ isFirstSaveRef.current = false
+ return
+ }
if (!draftScopeKeyRef.current) return
const fileAttachments = files.attachedFiles
.filter((f) => !f.uploading && f.key)
From c4e8254aaf6f8dbcbaa6bab9a3509beee84ed172 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 13:10:25 -0700
Subject: [PATCH 11/12] fix(files): only show loading skeleton while fetching,
relax mongo auth validation
- files.tsx: gate loading skeleton on isLoading so navigating to a missing
file ID doesn't permanently show the skeleton after load completes
- database-tools.ts: remove refine() pairing username+password on mongo
connection schema so either field can be provided independently
---
.../workspace/[workspaceId]/files/files.tsx | 2 +-
apps/sim/lib/api/contracts/database-tools.ts | 23 ++++++++-----------
2 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
index 31185261b4..cd1cbdf499 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx
@@ -1033,7 +1033,7 @@ export function Files() {
return tags
}, [typeFilter, sizeFilter, uploadedByFilter, members])
- if (fileIdFromRoute && !selectedFile) {
+ if (fileIdFromRoute && !selectedFile && isLoading) {
return (
diff --git a/apps/sim/lib/api/contracts/database-tools.ts b/apps/sim/lib/api/contracts/database-tools.ts
index 971b3a48f8..66f3c21384 100644
--- a/apps/sim/lib/api/contracts/database-tools.ts
+++ b/apps/sim/lib/api/contracts/database-tools.ts
@@ -120,20 +120,15 @@ export const rdsIntrospectBodySchema = rdsConnectionBodySchema.extend({
engine: z.enum(['aurora-postgresql', 'aurora-mysql']).optional(),
})
-const mongoConnectionBodySchema = z
- .object({
- host: z.string().min(1, 'Host is required'),
- port: z.coerce.number().int().positive('Port must be a positive integer'),
- database: z.string().min(1, 'Database name is required'),
- username: z.string().min(1, 'Username is required').optional(),
- password: z.string().min(1, 'Password is required').optional(),
- authSource: z.string().optional(),
- ssl: sslModeSchema,
- })
- .refine((data) => Boolean(data.username) === Boolean(data.password), {
- message: 'Username and password must be provided together',
- path: ['password'],
- })
+const mongoConnectionBodySchema = z.object({
+ host: z.string().min(1, 'Host is required'),
+ port: z.coerce.number().int().positive('Port must be a positive integer'),
+ database: z.string().min(1, 'Database name is required'),
+ username: z.string().min(1, 'Username is required').optional(),
+ password: z.string().min(1, 'Password is required').optional(),
+ authSource: z.string().optional(),
+ ssl: sslModeSchema,
+})
const mongoJsonStringOrObjectSchema = (message: string) =>
z
From 45c965d902943066c783b149402f6e654f1d4de6 Mon Sep 17 00:00:00 2001
From: waleed
Date: Thu, 30 Apr 2026 13:38:22 -0700
Subject: [PATCH 12/12] revert(database-tools): restore mongo username+password
paired validation
---
apps/sim/lib/api/contracts/database-tools.ts | 23 ++++++++++++--------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/apps/sim/lib/api/contracts/database-tools.ts b/apps/sim/lib/api/contracts/database-tools.ts
index 66f3c21384..971b3a48f8 100644
--- a/apps/sim/lib/api/contracts/database-tools.ts
+++ b/apps/sim/lib/api/contracts/database-tools.ts
@@ -120,15 +120,20 @@ export const rdsIntrospectBodySchema = rdsConnectionBodySchema.extend({
engine: z.enum(['aurora-postgresql', 'aurora-mysql']).optional(),
})
-const mongoConnectionBodySchema = z.object({
- host: z.string().min(1, 'Host is required'),
- port: z.coerce.number().int().positive('Port must be a positive integer'),
- database: z.string().min(1, 'Database name is required'),
- username: z.string().min(1, 'Username is required').optional(),
- password: z.string().min(1, 'Password is required').optional(),
- authSource: z.string().optional(),
- ssl: sslModeSchema,
-})
+const mongoConnectionBodySchema = z
+ .object({
+ host: z.string().min(1, 'Host is required'),
+ port: z.coerce.number().int().positive('Port must be a positive integer'),
+ database: z.string().min(1, 'Database name is required'),
+ username: z.string().min(1, 'Username is required').optional(),
+ password: z.string().min(1, 'Password is required').optional(),
+ authSource: z.string().optional(),
+ ssl: sslModeSchema,
+ })
+ .refine((data) => Boolean(data.username) === Boolean(data.password), {
+ message: 'Username and password must be provided together',
+ path: ['password'],
+ })
const mongoJsonStringOrObjectSchema = (message: string) =>
z