fix(webui): preserve session drag contracts

This commit is contained in:
Xubin Ren 2026-08-06 14:38:50 +08:00
parent f44a766f98
commit 9af6bb91c7
4 changed files with 40 additions and 3 deletions

View File

@ -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 (

View File

@ -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) => (

View File

@ -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(
<ChatList
@ -99,6 +101,8 @@ describe("ChatList", () => {
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"
/>,

View File

@ -1647,6 +1647,28 @@ describe("ThreadComposer", () => {
});
});
it("rejects session drops that are unavailable to the composer", () => {
render(
<ThreadComposer
onSend={vi.fn()}
placeholder="Type your message..."
sessions={[]}
/>,
);
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(
<ThreadComposer