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 aec38cbc1b..47018936d3 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
@@ -7,6 +7,7 @@ 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,
@@ -101,12 +102,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,
@@ -197,7 +203,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()
@@ -209,14 +215,14 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
})
}
}}
- 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={!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}
>
{task.name}
- {task.id !== 'new' && (
+ {!isPlaceholderTask(task.id) && (
{isActive && !isCurrentRoute && !isMenuOpen && (
@@ -634,6 +640,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(
() => [
{
@@ -641,6 +652,7 @@ export const Sidebar = memo(function Sidebar() {
label: 'Home',
icon: Home,
href: `/workspace/${workspaceId}/home`,
+ onClick: handleNewTaskFromNav,
},
{
id: 'search',
@@ -649,7 +661,7 @@ export const Sidebar = memo(function Sidebar() {
onClick: openSearchModal,
},
],
- [workspaceId, openSearchModal]
+ [workspaceId, openSearchModal, handleNewTaskFromNav]
)
const workspaceNavItems = useMemo(
@@ -725,24 +737,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)
@@ -784,7 +825,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 })
@@ -1088,9 +1132,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(
@@ -1109,10 +1156,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), [])
@@ -1453,7 +1500,8 @@ export const Sidebar = memo(function Sidebar() {
{tasks.slice(0, visibleTaskCount).map((task) => {
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 0000000000..bd1959b8f4
--- /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' }
+ )
+)