From 5c741f1585b23f83759db9627fc3458cd8f7bd93 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 8 Apr 2026 16:32:14 -0700 Subject: [PATCH 1/5] Adjust new task button to create new task --- .../w/components/sidebar/sidebar.tsx | 115 +++++++++++++----- apps/sim/stores/draft-tasks/store.ts | 31 +++++ 2 files changed, 114 insertions(+), 32 deletions(-) create mode 100644 apps/sim/stores/draft-tasks/store.ts 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..42689bd3bcc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1,6 +1,7 @@ 'use client' import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { flushSync } from 'react-dom' import { createLogger } from '@sim/logger' import { Compass, MoreHorizontal } from 'lucide-react' import Image from 'next/image' @@ -97,12 +98,17 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' +import { useDraftTaskStore } from '@/stores/draft-tasks/store' import { useFolderStore } from '@/stores/folders/store' import { useSearchModalStore } from '@/stores/modals/search/store' import { useSidebarStore } from '@/stores/sidebar/store' const logger = createLogger('Sidebar') +function isPlaceholderTask(id: string): boolean { + return id === 'new' || id.startsWith('draft-') +} + export function SidebarTooltip({ children, label, @@ -169,7 +175,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ (isCurrentRoute || isSelected || isMenuOpen) && 'bg-[var(--surface-active)]' )} onClick={(e) => { - if (task.id === 'new') return + if (isPlaceholderTask(task.id)) return if (e.metaKey || e.ctrlKey) return if (e.shiftKey) { e.preventDefault() @@ -181,11 +187,11 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ }) } }} - onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined} + onContextMenu={!isPlaceholderTask(task.id) ? (e) => onContextMenu(e, task.id) : undefined} >
{task.name}
- {task.id !== 'new' && ( + {!isPlaceholderTask(task.id) && (
{isActive && !isCurrentRoute && !isMenuOpen && ( @@ -603,6 +609,11 @@ export const Sidebar = memo(function Sidebar() { [workspaces, workspaceId] ) + const handleNewTaskFromNav = useCallback(() => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, [router, workspaceId]) + const topNavItems = useMemo( () => [ { @@ -610,6 +621,7 @@ export const Sidebar = memo(function Sidebar() { label: 'Home', icon: Home, href: `/workspace/${workspaceId}/home`, + onClick: handleNewTaskFromNav, }, { id: 'search', @@ -618,7 +630,7 @@ export const Sidebar = memo(function Sidebar() { onClick: openSearchModal, }, ], - [workspaceId, openSearchModal] + [workspaceId, openSearchModal, handleNewTaskFromNav] ) const workspaceNavItems = useMemo( @@ -694,24 +706,53 @@ export const Sidebar = memo(function Sidebar() { useTaskEvents(workspaceId) - const tasks = useMemo( - () => - fetchedTasks.length > 0 - ? 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] - ) + const draftTaskId = useDraftTaskStore((s) => s.draftTaskId) + const prevFetchedTaskIdsRef = useRef>(new Set(fetchedTasks.map((t) => t.id))) + + useEffect(() => { + const currentIds = new Set(fetchedTasks.map((t) => t.id)) + if (draftTaskId) { + const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) + if (hasNewTask) { + useDraftTaskStore.getState().removeDraft() + } + } + prevFetchedTaskIdsRef.current = currentIds + }, [draftTaskId, fetchedTasks]) + + const tasks = useMemo(() => { + const mapped = fetchedTasks.map((t) => ({ + ...t, + href: `/workspace/${workspaceId}/task/${t.id}`, + })) + + if (draftTaskId) { + const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) + if (!hasNewTask) { + mapped.unshift({ + id: draftTaskId, + name: 'New task', + href: `/workspace/${workspaceId}/home`, + isActive: false, + isUnread: false, + updatedAt: new Date(), + }) + } + } + + if (mapped.length === 0) { + mapped.push({ + id: 'new', + name: 'New task', + href: `/workspace/${workspaceId}/home`, + isActive: false, + isUnread: false, + updatedAt: new Date(), + }) + } + + return mapped + }, [fetchedTasks, workspaceId, draftTaskId]) const { data: fetchedTables = [] } = useTablesList(workspaceId) const { data: fetchedFiles = [] } = useWorkspaceFiles(workspaceId) @@ -753,7 +794,10 @@ 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).filter((id) => !isPlaceholderTask(id)), + [tasks] + ) const { selectedTasks, handleTaskClick } = useTaskSelection({ taskIds }) @@ -1057,9 +1101,12 @@ export const Sidebar = memo(function Sidebar() { const tasksPrimaryAction = useMemo( () => ({ label: 'New task', - onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`), + onSelect: () => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, }), - [navigateToPage, workspaceId] + [router, workspaceId] ) const workflowsPrimaryAction = useMemo( @@ -1078,10 +1125,10 @@ export const Sidebar = memo(function Sidebar() { [toggleCollapsed] ) - const handleNewTask = useCallback( - () => navigateToPage(`/workspace/${workspaceId}/home`), - [navigateToPage, workspaceId] - ) + const handleNewTask = useCallback(() => { + flushSync(() => useDraftTaskStore.getState().createDraft()) + router.push(`/workspace/${workspaceId}/home`) + }, [router, workspaceId]) const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), []) @@ -1388,7 +1435,9 @@ export const Sidebar = memo(function Sidebar() { {tasks.slice(0, visibleTaskCount).map((task) => { - const isCurrentRoute = task.id !== 'new' && pathname === task.href + const isCurrentRoute = + task.id !== 'new' && pathname === task.href const isRenaming = taskFlyoutRename.editingId === task.id - const isSelected = task.id !== 'new' && selectedTasks.has(task.id) + const isSelected = + !isPlaceholderTask(task.id) && selectedTasks.has(task.id) if (isRenaming) { return ( diff --git a/apps/sim/stores/draft-tasks/store.ts b/apps/sim/stores/draft-tasks/store.ts new file mode 100644 index 00000000000..bd1959b8f4d --- /dev/null +++ b/apps/sim/stores/draft-tasks/store.ts @@ -0,0 +1,31 @@ +import { create } from 'zustand' +import { devtools } from 'zustand/middleware' +import { generateShortId } from '@/lib/core/utils/uuid' + +interface DraftTaskState { + /** ID of the current draft task, or null if none exists */ + draftTaskId: string | null + /** Creates a draft task (reuses existing if one exists). Returns the draft ID. */ + createDraft: () => string + /** Removes the current draft task */ + removeDraft: () => void +} + +export const useDraftTaskStore = create()( + devtools( + (set, get) => ({ + draftTaskId: null, + + createDraft: () => { + const existing = get().draftTaskId + if (existing) return existing + const id = `draft-${generateShortId(8)}` + set({ draftTaskId: id }) + return id + }, + + removeDraft: () => set({ draftTaskId: null }), + }), + { name: 'draft-task-store' } + ) +) From ff6bfad1066662ce314b15807ff1d9e90aba651b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 8 Apr 2026 16:34:12 -0700 Subject: [PATCH 2/5] Fix lint --- .../[workspaceId]/w/components/sidebar/sidebar.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 42689bd3bcc..b0fdd6ca5f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1,13 +1,13 @@ 'use client' import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { flushSync } from 'react-dom' import { createLogger } from '@sim/logger' import { Compass, MoreHorizontal } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' +import { flushSync } from 'react-dom' import { Blimp, Button, @@ -1435,9 +1435,7 @@ export const Sidebar = memo(function Sidebar() { {tasks.slice(0, visibleTaskCount).map((task) => { - const isCurrentRoute = - task.id !== 'new' && pathname === task.href + const isCurrentRoute = task.id !== 'new' && pathname === task.href const isRenaming = taskFlyoutRename.editingId === task.id const isSelected = !isPlaceholderTask(task.id) && selectedTasks.has(task.id) From f4dd51a27463f207a7c30f138811625e7abe7132 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 9 Apr 2026 15:59:33 -0700 Subject: [PATCH 3/5] Handle create task draft on backend --- .../app/workspace/[workspaceId]/home/home.tsx | 3 +- .../[workspaceId]/home/hooks/use-chat.ts | 48 ++--- .../w/components/sidebar/sidebar.tsx | 168 ++++++------------ apps/sim/hooks/queries/tasks.ts | 34 ++++ apps/sim/stores/draft-tasks/store.ts | 31 ---- 5 files changed, 108 insertions(+), 176 deletions(-) delete mode 100644 apps/sim/stores/draft-tasks/store.ts diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx index 132d87b2a9e..754b92ff524 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx @@ -142,6 +142,7 @@ export function Home({ chatId }: HomeProps = {}) { const { messages, + isHistoryReady, isSending, isReconnecting, sendMessage, @@ -317,7 +318,7 @@ export function Home({ chatId }: HomeProps = {}) { return () => ro.disconnect() }, [hasMessages]) - if (!hasMessages && !chatId) { + if (!hasMessages && isHistoryReady) { return (
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index ac5bf1ab167..84c549de257 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useQueryClient } from '@tanstack/react-query' -import { usePathname } from 'next/navigation' +import { useRouter } from 'next/navigation' import { cancelRunToolExecution, executeRunToolOnClient, @@ -65,6 +65,7 @@ import type { WorkflowMetadata } from '@/stores/workflows/registry/types' export interface UseChatReturn { messages: ChatMessage[] + isHistoryReady: boolean isSending: boolean isReconnecting: boolean error: string | null @@ -410,7 +411,9 @@ export function useChat( initialChatId?: string, options?: UseChatOptions ): UseChatReturn { - const pathname = usePathname() + const router = useRouter() + const routerRef = useRef(router) + routerRef.current = router const queryClient = useQueryClient() const [messages, setMessages] = useState([]) const [isSending, setIsSending] = useState(false) @@ -506,7 +509,6 @@ export function useChat( const streamingBlocksRef = useRef([]) const clientExecutionStartedRef = useRef>(new Set()) const executionStream = useExecutionStream() - const isHomePage = pathname.endsWith('/home') const { data: chatHistory } = useChatHistory(initialChatId) @@ -595,32 +597,6 @@ export function useChat( setPendingRecoveryMessage(null) }, [initialChatId, queryClient]) - useEffect(() => { - if (workflowIdRef.current) return - if (!isHomePage || !chatIdRef.current) return - streamGenRef.current++ - chatIdRef.current = undefined - setResolvedChatId(undefined) - appliedChatIdRef.current = undefined - abortControllerRef.current = null - sendingRef.current = false - setMessages([]) - setError(null) - setIsSending(false) - setIsReconnecting(false) - setResources([]) - setActiveResourceId(null) - setStreamingFile(null) - streamingFileRef.current = null - genericResourceDataRef.current = { entries: [] } - setGenericResourceData({ entries: [] }) - setMessageQueue([]) - lastEventIdRef.current = 0 - clientExecutionStartedRef.current.clear() - pendingRecoveryMessageRef.current = null - setPendingRecoveryMessage(null) - }, [isHomePage]) - const fetchStreamBatch = useCallback( async ( streamId: string, @@ -895,7 +871,10 @@ export function useChat( if (isNewChat) { applyChatHistorySnapshot(chatHistory, { preserveActiveStreamingMessage: true }) - } else if (!activeStreamId || sendingRef.current) { + } else if (sendingRef.current) { + return + } else if (!activeStreamId) { + applyChatHistorySnapshot(chatHistory) return } @@ -1119,11 +1098,11 @@ export function useChat( }) } if (!workflowIdRef.current) { - window.history.replaceState( - null, - '', + routerRef.current.replace( `/workspace/${workspaceId}/task/${parsed.chatId}` ) + abortControllerRef.current?.abort() + streamGenRef.current++ } } } @@ -2282,8 +2261,11 @@ export function useChat( } }, []) + const isHistoryReady = !initialChatId || chatHistory !== undefined + return { messages, + isHistoryReady, isSending, isReconnecting, error, 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 47018936d3b..8d6f3238e01 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -7,7 +7,6 @@ import Image from 'next/image' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' import { usePostHog } from 'posthog-js/react' -import { flushSync } from 'react-dom' import { Blimp, Button, @@ -89,6 +88,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, @@ -102,17 +102,12 @@ import { usePermissionConfig } from '@/hooks/use-permission-config' import { useSettingsNavigation } from '@/hooks/use-settings-navigation' import { useTaskEvents } from '@/hooks/use-task-events' import { SIDEBAR_WIDTH } from '@/stores/constants' -import { useDraftTaskStore } from '@/stores/draft-tasks/store' import { useFolderStore } from '@/stores/folders/store' import { useSearchModalStore } from '@/stores/modals/search/store' import { useSidebarStore } from '@/stores/sidebar/store' const logger = createLogger('Sidebar') -function isPlaceholderTask(id: string): boolean { - return id === 'new' || id.startsWith('draft-') -} - export function SidebarTooltip({ children, label, @@ -203,7 +198,6 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ (isCurrentRoute || isSelected || isMenuOpen) && 'bg-[var(--surface-active)]' )} onClick={(e) => { - if (isPlaceholderTask(task.id)) return if (e.metaKey || e.ctrlKey) return if (e.shiftKey) { e.preventDefault() @@ -215,42 +209,40 @@ const SidebarTaskItem = memo(function SidebarTaskItem({ }) } }} - onContextMenu={!isPlaceholderTask(task.id) ? (e) => onContextMenu(e, task.id) : undefined} - draggable={!isPlaceholderTask(task.id)} - onDragStart={!isPlaceholderTask(task.id) ? handleDragStart : undefined} - onDragEnd={!isPlaceholderTask(task.id) ? handleDragEnd : undefined} + onContextMenu={(e) => onContextMenu(e, task.id)} + draggable + onDragStart={handleDragStart} + onDragEnd={handleDragEnd} >
{task.name}
- {!isPlaceholderTask(task.id) && ( -
- {isActive && !isCurrentRoute && !isMenuOpen && ( - - )} - {isActive && !isCurrentRoute && !isMenuOpen && ( - - )} - {!isActive && isUnread && !isCurrentRoute && !isMenuOpen && ( - +
+ {isActive && !isCurrentRoute && !isMenuOpen && ( + + )} + {isActive && !isCurrentRoute && !isMenuOpen && ( + + )} + {!isActive && isUnread && !isCurrentRoute && !isMenuOpen && ( + + )} + -
- )} + > + + +
) @@ -530,6 +522,7 @@ export const Sidebar = memo(function Sidebar() { } }, [activeNavItemHref]) + const createTaskMutation = useCreateTask() const deleteTaskMutation = useDeleteTask(workspaceId) const deleteTasksMutation = useDeleteTasks(workspaceId) const markTaskReadMutation = useMarkTaskRead(workspaceId) @@ -640,11 +633,6 @@ export const Sidebar = memo(function Sidebar() { [workspaces, workspaceId] ) - const handleNewTaskFromNav = useCallback(() => { - flushSync(() => useDraftTaskStore.getState().createDraft()) - router.push(`/workspace/${workspaceId}/home`) - }, [router, workspaceId]) - const topNavItems = useMemo( () => [ { @@ -652,7 +640,6 @@ export const Sidebar = memo(function Sidebar() { label: 'Home', icon: Home, href: `/workspace/${workspaceId}/home`, - onClick: handleNewTaskFromNav, }, { id: 'search', @@ -661,7 +648,7 @@ export const Sidebar = memo(function Sidebar() { onClick: openSearchModal, }, ], - [workspaceId, openSearchModal, handleNewTaskFromNav] + [workspaceId, openSearchModal] ) const workspaceNavItems = useMemo( @@ -737,53 +724,14 @@ export const Sidebar = memo(function Sidebar() { useTaskEvents(workspaceId) - const draftTaskId = useDraftTaskStore((s) => s.draftTaskId) - const prevFetchedTaskIdsRef = useRef>(new Set(fetchedTasks.map((t) => t.id))) - - useEffect(() => { - const currentIds = new Set(fetchedTasks.map((t) => t.id)) - if (draftTaskId) { - const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) - if (hasNewTask) { - useDraftTaskStore.getState().removeDraft() - } - } - prevFetchedTaskIdsRef.current = currentIds - }, [draftTaskId, fetchedTasks]) - - const tasks = useMemo(() => { - const mapped = fetchedTasks.map((t) => ({ - ...t, - href: `/workspace/${workspaceId}/task/${t.id}`, - })) - - if (draftTaskId) { - const hasNewTask = fetchedTasks.some((t) => !prevFetchedTaskIdsRef.current.has(t.id)) - if (!hasNewTask) { - mapped.unshift({ - id: draftTaskId, - name: 'New task', - href: `/workspace/${workspaceId}/home`, - isActive: false, - isUnread: false, - updatedAt: new Date(), - }) - } - } - - if (mapped.length === 0) { - mapped.push({ - id: 'new', - name: 'New task', - href: `/workspace/${workspaceId}/home`, - isActive: false, - isUnread: false, - updatedAt: new Date(), - }) - } - - return mapped - }, [fetchedTasks, workspaceId, draftTaskId]) + const tasks = useMemo( + () => + fetchedTasks.map((t) => ({ + ...t, + href: `/workspace/${workspaceId}/task/${t.id}`, + })), + [fetchedTasks, workspaceId] + ) const { data: fetchedTables = [] } = useTablesList(workspaceId) const { data: fetchedFiles = [] } = useWorkspaceFiles(workspaceId) @@ -825,10 +773,7 @@ export const Sidebar = memo(function Sidebar() { [fetchedKnowledgeBases, workspaceId, permissionConfig.hideKnowledgeBaseTab] ) - const taskIds = useMemo( - () => tasks.map((t) => t.id).filter((id) => !isPlaceholderTask(id)), - [tasks] - ) + const taskIds = useMemo(() => tasks.map((t) => t.id), [tasks]) const { selectedTasks, handleTaskClick } = useTaskSelection({ taskIds }) @@ -1129,15 +1074,22 @@ export const Sidebar = memo(function Sidebar() { [workflowIconStyle] ) + const handleNewTask = useCallback(async () => { + try { + const { id } = await createTaskMutation.mutateAsync({ workspaceId }) + router.push(`/workspace/${workspaceId}/task/${id}`) + } catch (err) { + logger.error('Failed to create task', err) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router, workspaceId]) + const tasksPrimaryAction = useMemo( () => ({ label: 'New task', - onSelect: () => { - flushSync(() => useDraftTaskStore.getState().createDraft()) - router.push(`/workspace/${workspaceId}/home`) - }, + onSelect: handleNewTask, }), - [router, workspaceId] + [handleNewTask] ) const workflowsPrimaryAction = useMemo( @@ -1156,11 +1108,6 @@ export const Sidebar = memo(function Sidebar() { [toggleCollapsed] ) - const handleNewTask = useCallback(() => { - flushSync(() => useDraftTaskStore.getState().createDraft()) - router.push(`/workspace/${workspaceId}/home`) - }, [router, workspaceId]) - const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), []) const handleCloseTaskDeleteModal = useCallback(() => setIsTaskDeleteModalOpen(false), []) @@ -1475,7 +1422,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 = - !isPlaceholderTask(task.id) && selectedTasks.has(task.id) + const isSelected = selectedTasks.has(task.id) if (isRenaming) { return ( diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index 9cd1eab999a..47fa563cf03 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/tasks.ts @@ -181,6 +181,40 @@ export function useChatHistory(chatId: string | undefined) { }) } +async function createTask(workspaceId: string): Promise<{ id: string }> { + 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 task') + } + const { id } = (await response.json()) as { id: string } + return { id } +} + +/** + * Creates an empty mothership chat task and invalidates the task list. + * Pre-warms the chat detail cache so the new chat page renders instantly with no fetch. + */ +export function useCreateTask() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ workspaceId }: { workspaceId: string }) => createTask(workspaceId), + onSuccess: ({ id }, { workspaceId }) => { + queryClient.setQueryData(taskKeys.detail(id), { + id, + title: null, + messages: [], + activeStreamId: null, + resources: [], + }) + queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) }) + }, + }) +} + async function deleteTask(chatId: string): Promise { const response = await fetch(`/api/mothership/chats/${chatId}`, { method: 'DELETE', diff --git a/apps/sim/stores/draft-tasks/store.ts b/apps/sim/stores/draft-tasks/store.ts deleted file mode 100644 index bd1959b8f4d..00000000000 --- a/apps/sim/stores/draft-tasks/store.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { create } from 'zustand' -import { devtools } from 'zustand/middleware' -import { generateShortId } from '@/lib/core/utils/uuid' - -interface DraftTaskState { - /** ID of the current draft task, or null if none exists */ - draftTaskId: string | null - /** Creates a draft task (reuses existing if one exists). Returns the draft ID. */ - createDraft: () => string - /** Removes the current draft task */ - removeDraft: () => void -} - -export const useDraftTaskStore = create()( - devtools( - (set, get) => ({ - draftTaskId: null, - - createDraft: () => { - const existing = get().draftTaskId - if (existing) return existing - const id = `draft-${generateShortId(8)}` - set({ draftTaskId: id }) - return id - }, - - removeDraft: () => set({ draftTaskId: null }), - }), - { name: 'draft-task-store' } - ) -) From 7780dc70c56726d9b47a8643401c4eeaf51d907f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 9 Apr 2026 16:04:58 -0700 Subject: [PATCH 4/5] Skip creating new task if existing new task exists --- apps/sim/app/api/mothership/chats/route.ts | 3 ++- .../sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts | 4 +--- .../[workspaceId]/w/components/sidebar/sidebar.tsx | 7 ++++++- apps/sim/hooks/queries/tasks.ts | 3 +++ 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/apps/sim/app/api/mothership/chats/route.ts b/apps/sim/app/api/mothership/chats/route.ts index bc694d1d9fe..d5cc199c546 100644 --- a/apps/sim/app/api/mothership/chats/route.ts +++ b/apps/sim/app/api/mothership/chats/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { copilotChats } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, desc, eq } from 'drizzle-orm' +import { and, desc, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { @@ -41,6 +41,7 @@ export async function GET(request: NextRequest) { updatedAt: copilotChats.updatedAt, conversationId: copilotChats.conversationId, lastSeenAt: copilotChats.lastSeenAt, + messageCount: sql`jsonb_array_length(${copilotChats.messages})`.as('message_count'), }) .from(copilotChats) .where( diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index 84c549de257..d8b852c4cf6 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -1098,9 +1098,7 @@ export function useChat( }) } if (!workflowIdRef.current) { - routerRef.current.replace( - `/workspace/${workspaceId}/task/${parsed.chatId}` - ) + routerRef.current.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`) abortControllerRef.current?.abort() streamGenRef.current++ } 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 8d6f3238e01..24b24460e2a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -1075,6 +1075,11 @@ export const Sidebar = memo(function Sidebar() { ) const handleNewTask = useCallback(async () => { + const existingEmpty = fetchedTasks.find((t) => t.isEmpty) + if (existingEmpty) { + router.push(`/workspace/${workspaceId}/task/${existingEmpty.id}`) + return + } try { const { id } = await createTaskMutation.mutateAsync({ workspaceId }) router.push(`/workspace/${workspaceId}/task/${id}`) @@ -1082,7 +1087,7 @@ export const Sidebar = memo(function Sidebar() { logger.error('Failed to create task', err) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [router, workspaceId]) + }, [router, workspaceId, fetchedTasks]) const tasksPrimaryAction = useMemo( () => ({ diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index 47fa563cf03..671af0485e9 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/tasks.ts @@ -7,6 +7,7 @@ export interface TaskMetadata { updatedAt: Date isActive: boolean isUnread: boolean + isEmpty: boolean } export interface StreamSnapshot { @@ -91,6 +92,7 @@ interface TaskResponse { updatedAt: string conversationId: string | null lastSeenAt: string | null + messageCount: number } function mapTask(chat: TaskResponse): TaskMetadata { @@ -103,6 +105,7 @@ function mapTask(chat: TaskResponse): TaskMetadata { isUnread: chat.conversationId === null && (chat.lastSeenAt === null || updatedAt > new Date(chat.lastSeenAt)), + isEmpty: chat.messageCount === 0, } } From 2ba89a7b458897570273936bb17363b217b8180a Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 9 Apr 2026 17:42:38 -0700 Subject: [PATCH 5/5] Fix lint --- apps/sim/app/api/mothership/chats/route.ts | 4 +++- apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts | 5 ++--- apps/sim/hooks/queries/tasks.ts | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/sim/app/api/mothership/chats/route.ts b/apps/sim/app/api/mothership/chats/route.ts index d5cc199c546..eaac1a4001f 100644 --- a/apps/sim/app/api/mothership/chats/route.ts +++ b/apps/sim/app/api/mothership/chats/route.ts @@ -41,7 +41,9 @@ export async function GET(request: NextRequest) { updatedAt: copilotChats.updatedAt, conversationId: copilotChats.conversationId, lastSeenAt: copilotChats.lastSeenAt, - messageCount: sql`jsonb_array_length(${copilotChats.messages})`.as('message_count'), + messageCount: sql`jsonb_array_length(${copilotChats.messages})` + .mapWith(Number) + .as('message_count'), }) .from(copilotChats) .where( diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts index d8b852c4cf6..dac0cf4d8d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts +++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts @@ -412,8 +412,6 @@ export function useChat( options?: UseChatOptions ): UseChatReturn { const router = useRouter() - const routerRef = useRef(router) - routerRef.current = router const queryClient = useQueryClient() const [messages, setMessages] = useState([]) const [isSending, setIsSending] = useState(false) @@ -1098,7 +1096,7 @@ export function useChat( }) } if (!workflowIdRef.current) { - routerRef.current.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`) + router.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`) abortControllerRef.current?.abort() streamGenRef.current++ } @@ -2083,6 +2081,7 @@ export function useChat( [ workspaceId, queryClient, + router, processSSEStream, finalize, resumeOrFinalize, diff --git a/apps/sim/hooks/queries/tasks.ts b/apps/sim/hooks/queries/tasks.ts index 671af0485e9..a80adbace7d 100644 --- a/apps/sim/hooks/queries/tasks.ts +++ b/apps/sim/hooks/queries/tasks.ts @@ -198,8 +198,7 @@ async function createTask(workspaceId: string): Promise<{ id: string }> { } /** - * Creates an empty mothership chat task and invalidates the task list. - * Pre-warms the chat detail cache so the new chat page renders instantly with no fetch. + * Pre-warms the detail cache so navigation to the new task renders without a loading flash. */ export function useCreateTask() { const queryClient = useQueryClient()