mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-21 03:56:20 +00:00
* feat(frontend): reference conversations from the composer
Adds a "Reference a conversation" button next to the attachment button,
shown only while GET /api/features reports read_conversation enabled. It
opens a picker over the recent-conversation list (current thread excluded,
capped at max_references) and shows removable chips in the composer.
On send the thread IDs ride SendMessageOptions.conversationReferences into
run context.conversation_references, which the Gateway consumes at
admission; the LangGraph SDK drops unknown top-level body fields. A
display-only copy ({thread_id, title}) on the visible human message lets
the transcript render read-only chips linking to the source.
References are per message: not persisted with the draft and cleared on
send or thread switch; regenerating or editing a turn runs without them
unless they are attached again.
Related to #5398. Depends on #5463.
* fix(frontend): pin the run-context contract and finish the picker states
Both thread.submit paths now build their run context through one exported
buildRunContext helper, tested directly: attached references travel as a
plain string[] under context.conversation_references only when the caller
passed them, a stray key in local settings is dropped instead of forwarded,
and the regenerate/edit replay path never carries references.
The picker shows a loading row while the conversation list is still in
flight instead of claiming there are no conversations, and the transcript
chip group is labelled with the previously unused referencedConversations
translation.
* fix(frontend): route conversation-reference chips to custom-agent sources
The picker offered custom-agent conversations but kept only the thread ID
and title, so transcript chips always linked to /workspace/chats/{id} and
dropped the source's custom-agent context on navigation.
Preserve the agent identity end to end: the picker now attaches
agentNameOfThread() (context first, then metadata.agent_name, mirroring
pathOfThread) to the selection, the display-only additional_kwargs metadata
round-trips it as agent_name, and the transcript chip passes it to
pathOfThread so custom-agent sources resolve to
/workspace/agents/{agent}/chats/{id}.
Tests: agent_name metadata round-trip and malformed-entry tolerance, picker
toggle carrying the metadata agent with run context winning, and a
picker-to-transcript regression pinning the /workspace/agents/writer/chats/
source-1 href.
---------
Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
import type { Message } from "@langchain/langgraph-sdk";
|
|
import { afterEach, beforeAll, describe, expect, it, rs } from "@rstest/core";
|
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
|
|
import { ConversationReferenceList } from "@/components/workspace/conversation-references/conversation-reference-picker";
|
|
import { MessageListItem } from "@/components/workspace/messages/message-list-item";
|
|
import {
|
|
buildConversationReferenceMetadata,
|
|
type ConversationReference,
|
|
} from "@/core/conversation-references";
|
|
import { enUS } from "@/core/i18n/locales/en-US";
|
|
import type { AgentThread } from "@/core/threads/types";
|
|
|
|
// The source conversation belongs to a custom agent; metadata.agent_name is
|
|
// what the sidebar's thread list carries for it.
|
|
const threads = [
|
|
{
|
|
thread_id: "t-current",
|
|
updated_at: "2026-09-16T09:00:00Z",
|
|
values: { title: "This conversation" },
|
|
metadata: {},
|
|
},
|
|
{
|
|
thread_id: "source-1",
|
|
updated_at: "2026-09-16T08:00:00Z",
|
|
values: { title: "Writer brief" },
|
|
metadata: { agent_name: "writer" },
|
|
},
|
|
] as unknown as AgentThread[];
|
|
|
|
rs.mock("@/core/threads/hooks", () => ({
|
|
useThreads: () => ({ data: threads, isPending: false, isError: false }),
|
|
}));
|
|
|
|
rs.mock("@/core/i18n/hooks", () => ({
|
|
useI18n: () => ({
|
|
locale: "en-US",
|
|
setLocale: () => undefined,
|
|
t: enUS,
|
|
}),
|
|
}));
|
|
|
|
// The transcript body is not under test; stubbing the markdown pipeline also
|
|
// avoids its first-render suspension.
|
|
rs.mock("@/components/workspace/messages/markdown-content", () => ({
|
|
MarkdownContent: () => null,
|
|
}));
|
|
|
|
beforeAll(() => {
|
|
// cmdk measures its list and scrolls the active item; happy-dom has neither.
|
|
class ResizeObserverStub {
|
|
observe = rs.fn();
|
|
unobserve = rs.fn();
|
|
disconnect = rs.fn();
|
|
}
|
|
globalThis.ResizeObserver ??=
|
|
ResizeObserverStub as unknown as typeof ResizeObserver;
|
|
if (!("scrollIntoView" in Element.prototype)) {
|
|
Object.defineProperty(Element.prototype, "scrollIntoView", {
|
|
configurable: true,
|
|
value: rs.fn(),
|
|
writable: true,
|
|
});
|
|
}
|
|
});
|
|
|
|
afterEach(cleanup);
|
|
|
|
describe("conversation reference picker-to-transcript flow", () => {
|
|
it("links the transcript chip to a custom-agent source conversation", () => {
|
|
// 1. Select the custom-agent conversation in the picker.
|
|
let selected: ConversationReference | undefined;
|
|
render(
|
|
<ConversationReferenceList
|
|
currentThreadId="t-current"
|
|
maxReferences={3}
|
|
onToggle={(reference) => {
|
|
selected = reference;
|
|
}}
|
|
selected={[]}
|
|
/>,
|
|
);
|
|
fireEvent.click(screen.getByText("Writer brief"));
|
|
expect(selected).toEqual({
|
|
threadId: "source-1",
|
|
title: "Writer brief",
|
|
agentName: "writer",
|
|
});
|
|
|
|
// 2. The send path stores the display-only metadata on the human message.
|
|
const message = {
|
|
id: "human-1",
|
|
type: "human",
|
|
content: "Summarize the referenced brief",
|
|
additional_kwargs: buildConversationReferenceMetadata([selected!]),
|
|
} as unknown as Message;
|
|
|
|
// 3. The transcript chip routes back to the custom-agent conversation.
|
|
cleanup();
|
|
render(
|
|
<MessageListItem
|
|
message={message}
|
|
threadId="t-current"
|
|
showCopyButton={false}
|
|
isLoading={false}
|
|
/>,
|
|
);
|
|
const chip = screen.getByTestId("conversation-reference-chip");
|
|
expect(chip.getAttribute("href")).toBe(
|
|
"/workspace/agents/writer/chats/source-1",
|
|
);
|
|
});
|
|
|
|
it("links a default-agent source to the plain chats path", () => {
|
|
const message = {
|
|
id: "human-2",
|
|
type: "human",
|
|
content: "Summarize the referenced chat",
|
|
additional_kwargs: buildConversationReferenceMetadata([
|
|
{ threadId: "source-2", title: "Plain chat" },
|
|
]),
|
|
} as unknown as Message;
|
|
|
|
render(
|
|
<MessageListItem
|
|
message={message}
|
|
threadId="t-current"
|
|
showCopyButton={false}
|
|
isLoading={false}
|
|
/>,
|
|
);
|
|
const chip = screen.getByTestId("conversation-reference-chip");
|
|
expect(chip.getAttribute("href")).toBe("/workspace/chats/source-2");
|
|
});
|
|
});
|