mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-27 00:17:53 +00:00
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.
This commit is contained in:
parent
eb5eb9c574
commit
cb83edb0da
@ -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({
|
||||
<MessageCircleIcon className="size-3.5" />
|
||||
{t.sidecar.addToConversation}
|
||||
</Button>
|
||||
<Button
|
||||
className="h-8 rounded-full px-2.5 text-xs"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleAskSelectionInSidecar}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<MessageSquarePlusIcon className="size-3.5" />
|
||||
{t.sidecar.askInSideChat}
|
||||
</Button>
|
||||
{!sidecarSurface && (
|
||||
<Button
|
||||
className="h-8 rounded-full px-2.5 text-xs"
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={handleAskSelectionInSidecar}
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
<MessageSquarePlusIcon className="size-3.5" />
|
||||
{t.sidecar.askInSideChat}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
aria-label={t.common.close}
|
||||
className="size-8 rounded-full"
|
||||
|
||||
@ -478,6 +478,7 @@ export function SidecarPanel({ className }: { className?: string }) {
|
||||
loadMoreHistory={loadMoreHistory}
|
||||
isHistoryLoading={isHistoryLoading}
|
||||
tokenUsageInlineMode={tokenUsageInlineMode}
|
||||
sidecarSurface
|
||||
initialScroll="instant"
|
||||
resizeScroll="instant"
|
||||
/>
|
||||
|
||||
@ -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<HTMLButtonElement>(
|
||||
"[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"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user