Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/sim/app/api/mothership/chats/route.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -41,6 +41,9 @@ export async function GET(request: NextRequest) {
updatedAt: copilotChats.updatedAt,
conversationId: copilotChats.conversationId,
lastSeenAt: copilotChats.lastSeenAt,
messageCount: sql<number>`jsonb_array_length(${copilotChats.messages})`
.mapWith(Number)
.as('message_count'),
})
.from(copilotChats)
.where(
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export function Home({ chatId }: HomeProps = {}) {

const {
messages,
isHistoryReady,
isSending,
isReconnecting,
sendMessage,
Expand Down Expand Up @@ -317,7 +318,7 @@ export function Home({ chatId }: HomeProps = {}) {
return () => ro.disconnect()
}, [hasMessages])

if (!hasMessages && !chatId) {
if (!hasMessages && isHistoryReady) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Greeting screen flashes on task page navigation

Medium Severity

The condition changed from !hasMessages && !chatId to !hasMessages && isHistoryReady. The messages state is initialized as [] via useState and only populated from chatHistory via a useEffect that runs after the render. When navigating to a task page with existing messages, isHistoryReady becomes true (cache hit) before the effect populates messages, so for at least one render frame !hasMessages && isHistoryReady evaluates to true, causing the "What should we get done?" greeting to flash. The old !chatId guard prevented this because chatId is always set on the task page.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2ba89a7. Configure here.

return (
<div className='h-full overflow-y-auto bg-[var(--bg)] [scrollbar-gutter:stable_both-edges]'>
<div className='flex min-h-full flex-col items-center justify-center px-6 pb-[2vh]'>
Expand Down
49 changes: 14 additions & 35 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -410,7 +411,7 @@ export function useChat(
initialChatId?: string,
options?: UseChatOptions
): UseChatReturn {
const pathname = usePathname()
const router = useRouter()
const queryClient = useQueryClient()
const [messages, setMessages] = useState<ChatMessage[]>([])
const [isSending, setIsSending] = useState(false)
Expand Down Expand Up @@ -506,7 +507,6 @@ export function useChat(
const streamingBlocksRef = useRef<ContentBlock[]>([])
const clientExecutionStartedRef = useRef<Set<string>>(new Set())
const executionStream = useExecutionStream()
const isHomePage = pathname.endsWith('/home')

const { data: chatHistory } = useChatHistory(initialChatId)

Expand Down Expand Up @@ -595,32 +595,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,
Expand Down Expand Up @@ -895,7 +869,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
}

Expand Down Expand Up @@ -1119,11 +1096,9 @@ export function useChat(
})
}
if (!workflowIdRef.current) {
window.history.replaceState(
null,
'',
`/workspace/${workspaceId}/task/${parsed.chatId}`
)
router.replace(`/workspace/${workspaceId}/task/${parsed.chatId}`)
abortControllerRef.current?.abort()
streamGenRef.current++
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Router navigation aborts active SSE message stream

Medium Severity

Replacing window.history.replaceState with router.replace causes a full Next.js route transition that unmounts the component during active streaming. The stream is explicitly aborted and the generation counter incremented, interrupting the assistant's response mid-generation. The task page must then reconnect from event 0, replaying the entire response from scratch, causing a visible disruption where the partial response disappears and then rapidly replays.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2ba89a7. Configure here.

}
}
}
Expand Down Expand Up @@ -2106,6 +2081,7 @@ export function useChat(
[
workspaceId,
queryClient,
router,
processSSEStream,
finalize,
resumeOrFinalize,
Expand Down Expand Up @@ -2282,8 +2258,11 @@ export function useChat(
}
}, [])

const isHistoryReady = !initialChatId || chatHistory !== undefined

return {
messages,
isHistoryReady,
isSending,
isReconnecting,
error,
Expand Down
115 changes: 57 additions & 58 deletions apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,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,
Expand Down Expand Up @@ -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()
Expand All @@ -209,42 +209,40 @@ 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={(e) => onContextMenu(e, task.id)}
draggable
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<Blimp className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
<div className='min-w-0 flex-1 truncate font-base text-[var(--text-body)]'>{task.name}</div>
{task.id !== 'new' && (
<div className='relative flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center'>
{isActive && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] animate-ping rounded-full bg-amber-400 opacity-30 group-hover:hidden' />
)}
{isActive && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] rounded-full bg-amber-400 group-hover:hidden' />
)}
{!isActive && isUnread && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] rounded-full bg-[var(--brand-accent)] group-hover:hidden' />
<div className='relative flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center'>
{isActive && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] animate-ping rounded-full bg-amber-400 opacity-30 group-hover:hidden' />
)}
{isActive && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] rounded-full bg-amber-400 group-hover:hidden' />
)}
{!isActive && isUnread && !isCurrentRoute && !isMenuOpen && (
<span className='absolute h-[7px] w-[7px] rounded-full bg-[var(--brand-accent)] group-hover:hidden' />
)}
<button
type='button'
aria-label='Task options'
onPointerDown={onMorePointerDown}
onClick={(e) => {
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'
)}
<button
type='button'
aria-label='Task options'
onPointerDown={onMorePointerDown}
onClick={(e) => {
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'
)}
>
<MoreHorizontal className='h-[16px] w-[16px] text-[var(--text-icon)]' />
</button>
</div>
)}
>
<MoreHorizontal className='h-[16px] w-[16px] text-[var(--text-icon)]' />
</button>
</div>
</Link>
</SidebarTooltip>
)
Expand Down Expand Up @@ -524,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)
Expand Down Expand Up @@ -727,20 +726,10 @@ export const Sidebar = memo(function Sidebar() {

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.map((t) => ({
...t,
href: `/workspace/${workspaceId}/task/${t.id}`,
})),
[fetchedTasks, workspaceId]
)

Expand Down Expand Up @@ -784,7 +773,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 })

Expand Down Expand Up @@ -1085,12 +1074,27 @@ export const Sidebar = memo(function Sidebar() {
[workflowIconStyle]
)

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}`)
} catch (err) {
logger.error('Failed to create task', err)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router, workspaceId, fetchedTasks])

const tasksPrimaryAction = useMemo(
() => ({
label: 'New task',
onSelect: () => navigateToPage(`/workspace/${workspaceId}/home`),
onSelect: handleNewTask,
}),
[navigateToPage, workspaceId]
[handleNewTask]
)

const workflowsPrimaryAction = useMemo(
Expand All @@ -1109,11 +1113,6 @@ export const Sidebar = memo(function Sidebar() {
[toggleCollapsed]
)

const handleNewTask = useCallback(
() => navigateToPage(`/workspace/${workspaceId}/home`),
[navigateToPage, workspaceId]
)

const handleSeeMoreTasks = useCallback(() => setVisibleTaskCount((prev) => prev + 5), [])

const handleCloseTaskDeleteModal = useCallback(() => setIsTaskDeleteModalOpen(false), [])
Expand Down Expand Up @@ -1428,7 +1427,7 @@ export const Sidebar = memo(function Sidebar() {
<CollapsedTaskFlyoutItem
key={task.id}
task={task}
isCurrentRoute={task.id !== 'new' && pathname === task.href}
isCurrentRoute={pathname === task.href}
isMenuOpen={menuOpenTaskId === task.id}
isEditing={task.id === taskFlyoutRename.editingId}
editValue={taskFlyoutRename.value}
Expand All @@ -1451,9 +1450,9 @@ 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 (
Expand Down
Loading
Loading