From cb83edb0da39f5cb85c3bc1d02d56d1cf0762657 Mon Sep 17 00:00:00 2001
From: Ryker_Feng <90562015+18062706139fcz@users.noreply.github.com>
Date: Mon, 6 Jul 2026 14:37:31 +0800
Subject: [PATCH] fix(sidecar): correct text-selection toolbar actions inside
the side chat (#3959)
* fix(sidecar): correct text-selection toolbar actions inside the side chat
The sidecar panel reuses MessageList, but it did not distinguish the side
chat surface from the main conversation, so selecting text inside the side
chat behaved as if it were the main list:
- The "Ask in side chat" action was shown even though the user is already
in the side chat, which is a no-op interaction.
- "Add to conversation" routed the snippet to the main composer's quotes
(conversationQuotes) instead of the side chat's own composer, so the
reference landed in the wrong input box.
Add a `sidecarSurface` prop to MessageList. On the sidecar surface, hide
"Ask in side chat" and route "Add to conversation" to `sidecar.openContext`
so the snippet attaches to the side chat's own composer (activeReferences).
Main-list behavior is unchanged.
* docs(sidecar): drop AGENTS.md note for the toolbar surface change
The sidecarSurface behavior is self-evident from the code; no dedicated
Interaction Ownership entry is needed.
---
.../workspace/messages/message-list.tsx | 38 ++++++++++-----
.../workspace/sidecar/sidecar-panel.tsx | 1 +
frontend/tests/e2e/sidecar-chat.spec.ts | 48 ++++++++++++-------
3 files changed, 56 insertions(+), 31 deletions(-)
diff --git a/frontend/src/components/workspace/messages/message-list.tsx b/frontend/src/components/workspace/messages/message-list.tsx
index b5a5cff19..36565a31e 100644
--- a/frontend/src/components/workspace/messages/message-list.tsx
+++ b/frontend/src/components/workspace/messages/message-list.tsx
@@ -212,6 +212,7 @@ export function MessageList({
canRegenerate = false,
canBranch = false,
enableSidecarActions = true,
+ sidecarSurface = false,
initialScroll = "smooth",
resizeScroll = "smooth",
}: {
@@ -235,6 +236,7 @@ export function MessageList({
canRegenerate?: boolean;
canBranch?: boolean;
enableSidecarActions?: boolean;
+ sidecarSurface?: boolean;
initialScroll?: ConversationProps["initial"];
resizeScroll?: ConversationProps["resize"];
}) {
@@ -417,10 +419,18 @@ export function MessageList({
if (!selectionToolbar) {
return;
}
- sidecar?.addContextToConversation(selectionToolbar.context);
+ // On the sidecar surface, "add to conversation" targets the side chat's
+ // own composer (activeReferences) rather than the main composer's quotes,
+ // so the selected snippet is attached to the conversation the user is
+ // actually reading.
+ if (sidecarSurface) {
+ sidecar?.openContext(selectionToolbar.context);
+ } else {
+ sidecar?.addContextToConversation(selectionToolbar.context);
+ }
window.getSelection()?.removeAllRanges();
setSelectionToolbar(null);
- }, [selectionToolbar, sidecar]);
+ }, [selectionToolbar, sidecar, sidecarSurface]);
const handleAskSelectionInSidecar = useCallback(() => {
if (!selectionToolbar) {
@@ -873,17 +883,19 @@ export function MessageList({
{t.sidecar.addToConversation}
-
+ {!sidecarSurface && (
+
+ )}
diff --git a/frontend/tests/e2e/sidecar-chat.spec.ts b/frontend/tests/e2e/sidecar-chat.spec.ts
index b40264608..cc8e7823a 100644
--- a/frontend/tests/e2e/sidecar-chat.spec.ts
+++ b/frontend/tests/e2e/sidecar-chat.spec.ts
@@ -105,6 +105,32 @@ async function selectTextAndClickToolbarButton(
throw lastError;
}
+async function expectSidecarSelectionToolbarActions(page: Page, text: string) {
+ let lastError: unknown;
+ for (let attempt = 0; attempt < 3; attempt += 1) {
+ try {
+ await selectTextOnPage(page, text, "sidecar-message-list");
+ const labels = await page.evaluate(() =>
+ Array.from(
+ document.querySelectorAll(
+ "[data-sidecar-selection-toolbar] button",
+ ),
+ ).map((button) => button.textContent?.trim() ?? ""),
+ );
+ expect(labels.some((label) => /add to conversation/i.test(label))).toBe(
+ true,
+ );
+ expect(labels.some((label) => /ask in side chat/i.test(label))).toBe(
+ false,
+ );
+ return;
+ } catch (error) {
+ lastError = error;
+ }
+ }
+ throw lastError;
+}
+
async function expectComposerHeightsEqual(page: Page) {
const metrics = await page.evaluate(() => {
const findFormByPlaceholder = (pattern: RegExp) => {
@@ -943,30 +969,16 @@ test.describe("Side chat", () => {
.first(),
).toBeVisible();
+ // Selecting text inside the side chat itself only offers "Add to
+ // conversation" (no "Ask in side chat"), and the snippet attaches to the
+ // side chat's own composer rather than the main composer's quotes.
+ await expectSidecarSelectionToolbarActions(page, "Hello from DeerFlow!");
await selectTextAndClickToolbarButton(
page,
"Hello from DeerFlow!",
"Add to conversation",
"sidecar-message-list",
);
- await expect(
- mainInputForm.getByTestId("conversation-quote-attachment"),
- ).toContainText("1 selected text fragment");
- await expect(sidecarReference).toBeHidden();
- await mainInputForm
- .getByTestId("conversation-quote-attachment")
- .getByRole("button", { name: /clear selected references/i })
- .click();
- await expect(
- mainInputForm.getByTestId("conversation-quote-attachment"),
- ).toBeHidden();
-
- await selectTextAndClickToolbarButton(
- page,
- "Hello from DeerFlow!",
- "Ask in side chat",
- "sidecar-message-list",
- );
await expect(sidecarReference).toContainText("1 selected text fragment");
await expect(
mainInputForm.getByTestId("conversation-quote-attachment"),