fix(mothership): stabilize task sidebar ordering on selection (#4309)

This commit is contained in:
Waleed
2026-04-27 12:42:20 -07:00
committed by GitHub
parent 65e17de065
commit f62d274478
4 changed files with 13 additions and 19 deletions

View File

@@ -206,10 +206,7 @@ const SidebarTaskItem = memo(function SidebarTaskItem({
e.preventDefault()
onMultiSelectClick(task.id, true)
} else {
useFolderStore.setState({
selectedTasks: new Set<string>(),
lastSelectedTaskId: task.id,
})
useFolderStore.getState().selectTaskOnly(task.id)
}
}}
onContextMenu={task.id !== 'new' ? (e) => onContextMenu(e, task.id) : undefined}

View File

@@ -1,4 +1,4 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import type { PersistedMessage } from '@/lib/copilot/chat/persisted-message'
import { normalizeMessage } from '@/lib/copilot/chat/persisted-message'
import {
@@ -254,7 +254,6 @@ export function useTasks(workspaceId?: string) {
queryKey: taskKeys.list(workspaceId),
queryFn: ({ signal }) => fetchTasks(workspaceId as string, signal),
enabled: Boolean(workspaceId),
placeholderData: keepPreviousData,
staleTime: 60 * 1000,
})
}
@@ -535,6 +534,10 @@ async function markTaskUnread(chatId: string): Promise<void> {
/**
* Marks a task as read with optimistic update.
*
* The server only updates `lastSeenAt`, never `updatedAt`, so we deliberately
* do not invalidate the list cache — that would trigger a refetch that can
* reorder the sidebar if any unrelated server-side update landed in between.
*/
export function useMarkTaskRead(workspaceId?: string) {
const queryClient = useQueryClient()
@@ -556,14 +559,14 @@ export function useMarkTaskRead(workspaceId?: string) {
queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
},
})
}
/**
* Marks a task as unread with optimistic update.
*
* Same rationale as `useMarkTaskRead` — no list invalidation, since the server
* only flips `lastSeenAt` and the optimistic update fully reflects the change.
*/
export function useMarkTaskUnread(workspaceId?: string) {
const queryClient = useQueryClient()
@@ -585,8 +588,5 @@ export function useMarkTaskUnread(workspaceId?: string) {
queryClient.setQueryData(taskKeys.list(workspaceId), context.previousTasks)
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
},
})
}

View File

@@ -50,12 +50,9 @@ describe('handleTaskStatusEvent', () => {
})
})
it('preserves list invalidation when task event payload is invalid', () => {
it('does not invalidate when task event payload is invalid', () => {
handleTaskStatusEvent(queryClient, 'ws-1', '{')
expect(queryClient.invalidateQueries).toHaveBeenCalledTimes(1)
expect(queryClient.invalidateQueries).toHaveBeenCalledWith({
queryKey: taskKeys.list('ws-1'),
})
expect(queryClient.invalidateQueries).not.toHaveBeenCalled()
})
})

View File

@@ -41,13 +41,13 @@ export function handleTaskStatusEvent(
workspaceId: string,
data: unknown
): void {
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
const payload = parseTaskStatusEventPayload(data)
if (!payload) {
logger.warn('Received invalid task_status payload')
return
}
queryClient.invalidateQueries({ queryKey: taskKeys.list(workspaceId) })
}
/**