diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 655156048c1d..c47402f4204b 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -1764,4 +1764,53 @@ describe("MessagesTimeline", () => { expect(markup).toContain("lucide-circle-alert"); expect(markup).toContain("text-destructive"); }); + + it("only withholds an expanded tool-call label click while text is selected", async () => { + vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); + vi.stubGlobal("requestAnimationFrame", () => 0); + vi.stubGlobal("cancelAnimationFrame", () => {}); + let renderer: ReactTestRenderer | undefined; + try { + await act(() => { + renderer = create( + , + ); + }); + await act(() => renderer!.root.findByProps({ "aria-expanded": false }).props.onClick()); + const label = renderer!.root.findAll( + (node) => node.type === "span" && String(node.props.className).includes("select-text"), + )[0]; + const stopPropagation = vi.fn(); + // Only the click that ends a selection may be withheld from the row + // toggle; the plain click has to reach it so the label can collapse. + for (const isCollapsed of [false, true]) { + label!.props.onClick({ + currentTarget: { ownerDocument: { getSelection: () => ({ isCollapsed }) } }, + stopPropagation, + }); + } + expect(stopPropagation).toHaveBeenCalledTimes(1); + } finally { + await act(() => renderer?.unmount()); + } + }); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 8e9635f89e43..52ed8bfaf803 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -3172,6 +3172,18 @@ function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName { const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation(); +/** + * Click handler for expanded row labels, which turn text selection back on. + * Only a click that ends a real selection is withheld from the row toggle, so + * an ordinary click on the label still bubbles and collapses the row it opened. + */ +const stopRowToggleWhileSelectingText = (e: MouseEvent) => { + const selection = e.currentTarget.ownerDocument.getSelection(); + if (selection && !selection.isCollapsed) { + e.stopPropagation(); + } +}; + /** * A1 spawn CTA: one anchored row per workflow run (or per-turn direct-spawn * batch). Live status is derived from the shared agent panel model at render @@ -3407,7 +3419,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { expanded ? "whitespace-pre-wrap break-words select-text" : "truncate", headingClass, )} - onClick={expanded ? stopRowToggle : undefined} + onClick={expanded ? stopRowToggleWhileSelectingText : undefined} onPointerDown={expanded ? stopRowToggle : undefined} > {previewText}