diff --git a/webui/src/components/ChatList.tsx b/webui/src/components/ChatList.tsx index e7fdea0e8..ceadeee33 100644 --- a/webui/src/components/ChatList.tsx +++ b/webui/src/components/ChatList.tsx @@ -221,7 +221,11 @@ export const ChatList = memo(function ChatList({ const targetIndex = reordered.indexOf(targetKey); if (targetIndex < 0) return; reordered.splice(targetIndex + (edge === "after" ? 1 : 0), 0, draggedSessionKey); - onReorderSessions(reordered); + const groupedKeys = new Set(keys); + onReorderSessions([ + ...reordered, + ...sessionOrder.filter((key) => !groupedKeys.has(key)), + ]); }; return ( diff --git a/webui/src/components/thread/ThreadComposer.tsx b/webui/src/components/thread/ThreadComposer.tsx index 6dc4745ca..a8a75382b 100644 --- a/webui/src/components/thread/ThreadComposer.tsx +++ b/webui/src/components/thread/ThreadComposer.tsx @@ -1730,9 +1730,8 @@ export function ThreadComposer({ const previewSessionDrop = useCallback((event: React.DragEvent) => { if (!hasDraggedSession(event.dataTransfer)) return false; - event.preventDefault(); - event.dataTransfer.dropEffect = "copy"; if (disabled) { + event.dataTransfer.dropEffect = "none"; setSessionDragPreview(null); return true; } @@ -1744,9 +1743,12 @@ export function ThreadComposer({ (candidate) => candidate.session_key === mention.session_key, ); if (!mention || (!alreadySelected && activeSessionMentions.length >= SESSION_MENTIONS_LIMIT)) { + event.dataTransfer.dropEffect = "none"; setSessionDragPreview(null); return true; } + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; const start = textareaRef.current?.selectionStart ?? value.length; const end = textareaRef.current?.selectionEnd ?? start; setSessionDragPreview((current) => ( diff --git a/webui/src/tests/chat-list.test.tsx b/webui/src/tests/chat-list.test.tsx index 1d0d870bb..862fe1cc3 100644 --- a/webui/src/tests/chat-list.test.tsx +++ b/webui/src/tests/chat-list.test.tsx @@ -88,6 +88,8 @@ describe("ChatList", () => { session({ chatId: "alpha", title: "Alpha" }), session({ chatId: "bravo", title: "Bravo" }), session({ chatId: "charlie", title: "Charlie" }), + session({ chatId: "old-a", title: "Old A" }), + session({ chatId: "old-b", title: "Old B" }), ]; const { rerender } = render( { onRequestRename={vi.fn()} onToggleArchive={vi.fn()} onReorderSessions={onReorderSessions} + archivedKeys={["websocket:old-a", "websocket:old-b"]} + sessionOrder={sessions.map((item) => item.key)} />, ); const dataTransfer = { @@ -117,6 +121,8 @@ describe("ChatList", () => { "websocket:bravo", "websocket:charlie", "websocket:alpha", + "websocket:old-a", + "websocket:old-b", ]); rerender( @@ -129,10 +135,13 @@ describe("ChatList", () => { onRequestRename={vi.fn()} onToggleArchive={vi.fn()} onReorderSessions={onReorderSessions} + archivedKeys={["websocket:old-a", "websocket:old-b"]} sessionOrder={[ "websocket:bravo", "websocket:charlie", "websocket:alpha", + "websocket:old-a", + "websocket:old-b", ]} sort="manual" />, diff --git a/webui/src/tests/thread-composer.test.tsx b/webui/src/tests/thread-composer.test.tsx index 796bfd03e..bd63ab114 100644 --- a/webui/src/tests/thread-composer.test.tsx +++ b/webui/src/tests/thread-composer.test.tsx @@ -1647,6 +1647,28 @@ describe("ThreadComposer", () => { }); }); + it("rejects session drops that are unavailable to the composer", () => { + render( + , + ); + const input = screen.getByLabelText("Message input"); + const dataTransfer = { + types: [SESSION_DRAG_TYPE], + effectAllowed: "copyMove", + dropEffect: "copy", + files: [], + getData: () => "websocket:current", + }; + + expect(fireEvent.dragEnter(input, { dataTransfer })).toBe(true); + expect(fireEvent.dragOver(input, { dataTransfer })).toBe(true); + expect(screen.queryByTestId("composer-session-drag-preview")).not.toBeInTheDocument(); + }); + it("disambiguates duplicate and capability-colliding session names", () => { render(