diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index fead88a404..366255e4c5 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -453,7 +453,13 @@ export const unpinChat = (queryClient: QueryClient) => ({ export const reorderPinnedChat = (queryClient: QueryClient) => ({ mutationFn: ({ chatId, pinOrder }: { chatId: string; pinOrder: number }) => API.experimental.updateChat(chatId, { pin_order: pinOrder }), - onMutate: async ({ chatId }: { chatId: string; pinOrder: number }) => { + onMutate: async ({ + chatId, + pinOrder, + }: { + chatId: string; + pinOrder: number; + }) => { await queryClient.cancelQueries({ queryKey: chatsKey, predicate: isChatListQuery, @@ -462,6 +468,26 @@ export const reorderPinnedChat = (queryClient: QueryClient) => ({ queryKey: chatKey(chatId), exact: true, }); + + // Optimistically reorder pinned chats in the cache so the + // sidebar reflects the new order immediately without waiting + // for the server round-trip. + const allChats = readInfiniteChatsCache(queryClient) ?? []; + const pinned = allChats + .filter((c) => c.pin_order > 0) + .sort((a, b) => a.pin_order - b.pin_order); + const oldIdx = pinned.findIndex((c) => c.id === chatId); + if (oldIdx !== -1) { + const moved = pinned.splice(oldIdx, 1)[0]; + pinned.splice(pinOrder - 1, 0, moved); + const newOrders = new Map(pinned.map((c, i) => [c.id, i + 1])); + updateInfiniteChatsCache(queryClient, (chats) => + chats.map((c) => { + const order = newOrders.get(c.id); + return order !== undefined ? { ...c, pin_order: order } : c; + }), + ); + } }, onSettled: async ( _data: unknown, diff --git a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx index cc811a8c8c..331e55597a 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx @@ -672,8 +672,7 @@ const ChatTreeNode: FC = ({ chat, isChildNode }) => { const SortableChatTreeNode: FC<{ chat: Chat; - recentDragRef: React.RefObject; -}> = ({ chat, recentDragRef }) => { +}> = ({ chat }) => { const { attributes, listeners, @@ -707,18 +706,6 @@ const SortableChatTreeNode: FC<{ className={cn(isDragging && "opacity-50")} {...attributes} {...listeners} - onClickCapture={(e) => { - // After a drag, the browser synthesizes a click from - // pointerup. dnd-kit's sensor calls stopPropagation - // but never preventDefault, so the tag inside - // NavLink still fires its default navigation action. - // Block it here in React's capture phase instead of - // racing with a global document listener + rAF. - if (recentDragRef.current) { - e.stopPropagation(); - e.preventDefault(); - } - }} > @@ -807,11 +794,25 @@ export const AgentsSidebar: FC = (props) => { const pinnedChatIds = sortedPinnedChats.map((chat) => chat.id); - // Ref flag set after drag ends. Checked by SortableChatTreeNode's - // onClickCapture to block the synthetic click that the browser - // fires from the final pointerup. Cleared after 100ms, which - // comfortably exceeds dnd-kit's 50ms sensor cleanup window. - const recentDragRef = useRef(false); + const lastDragEndedAtRef = useRef(0); + + const pinnedContainerRef = useRef(null); + useEffect(() => { + const handler = (e: MouseEvent) => { + const container = pinnedContainerRef.current; + const target = e.target; + if ( + container && + target instanceof Node && + container.contains(target) && + performance.now() - lastDragEndedAtRef.current < 300 + ) { + e.preventDefault(); + } + }; + document.addEventListener("click", handler, true); + return () => document.removeEventListener("click", handler, true); + }, []); const sensors = useSensors( useSensor(MouseSensor, { @@ -828,11 +829,7 @@ export const AgentsSidebar: FC = (props) => { const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; - recentDragRef.current = true; - setTimeout(() => { - recentDragRef.current = false; - }, 100); - + lastDragEndedAtRef.current = performance.now(); if (!over || active.id === over.id) return; const activeId = String(active.id); const overId = String(over.id); @@ -1087,12 +1084,14 @@ export const AgentsSidebar: FC = (props) => { items={pinnedChatIds} strategy={verticalListSortingStrategy} > -
+
{sortedPinnedChats.map((chat) => ( ))}