feat(webui): drag sessions into composer mentions

This commit is contained in:
Xubin Ren
2026-08-06 19:11:05 +08:00
parent 5c4c2cb819
commit 2c8e63446f
5 changed files with 164 additions and 10 deletions
+34
View File
@@ -2,6 +2,7 @@ import { fireEvent, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ChatList } from "@/components/ChatList";
import { SESSION_DRAG_TYPE } from "@/lib/session-drag";
import type { ChatSummary } from "@/lib/types";
function session(overrides: Partial<ChatSummary>): ChatSummary {
@@ -47,6 +48,39 @@ describe("ChatList", () => {
vi.unstubAllGlobals();
});
it("exposes inactive chats as session mention drag sources", () => {
const dataTransfer = {
effectAllowed: "",
setData: vi.fn(),
};
render(
<ChatList
sessions={[
session({ chatId: "active", title: "Active chat" }),
session({ chatId: "reference", title: "Reference chat" }),
]}
activeKey="websocket:active"
onSelect={vi.fn()}
onRequestDelete={vi.fn()}
onTogglePin={vi.fn()}
onRequestRename={vi.fn()}
onToggleArchive={vi.fn()}
/>,
);
expect(screen.getByRole("button", { name: "Active chat" }))
.toHaveAttribute("draggable", "false");
const reference = screen.getByRole("button", { name: "Reference chat" });
expect(reference).toHaveAttribute("draggable", "true");
fireEvent.dragStart(reference, { dataTransfer });
expect(dataTransfer.setData).toHaveBeenCalledWith(
SESSION_DRAG_TYPE,
"websocket:reference",
);
});
it("orders chats by latest session activity by default", () => {
const sessions = [
session({
+42
View File
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { ThreadComposer } from "@/components/thread/ThreadComposer";
import { SESSION_DRAG_TYPE } from "@/lib/session-drag";
import type { ChatSummary, CliAppInfo, McpPresetInfo, SlashCommand } from "@/lib/types";
vi.mock("@/lib/imageEncode", () => ({
@@ -1593,6 +1594,47 @@ describe("ThreadComposer", () => {
});
});
it("turns a dropped sidebar session into the shared structured mention", () => {
const onSend = vi.fn();
render(
<ThreadComposer
onSend={onSend}
placeholder="Type your message..."
sessions={[session("pricing", "收费设计", "讨论云存储")]}
/>,
);
const input = screen.getByLabelText("Message input") as HTMLTextAreaElement;
fireEvent.change(input, { target: { value: "Compare notes" } });
input.setSelectionRange(7, 7);
const dataTransfer = {
types: [SESSION_DRAG_TYPE],
effectAllowed: "copy",
dropEffect: "none",
files: [],
getData: (type: string) => (
type === SESSION_DRAG_TYPE ? "websocket:pricing" : ""
),
};
fireEvent.dragEnter(input, { dataTransfer });
fireEvent.dragOver(input, { dataTransfer });
fireEvent.drop(input, { dataTransfer });
expect(input).toHaveValue("Compare @收费设计 notes");
expect(screen.getByTestId("composer-session-mention-收费设计"))
.toHaveTextContent("@收费设计");
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
expect(onSend).toHaveBeenCalledWith("Compare @收费设计 notes", undefined, {
sessionMentions: [{
name: "收费设计",
session_key: "websocket:pricing",
title: "收费设计",
}],
});
});
it("disambiguates duplicate and capability-colliding session names", () => {
render(
<ThreadComposer