authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-10 15:38:35-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-10 15:38:35-03:00
log502131adff7494d1a1797b4fe518870f7e8431fd
treef05d5dc5494f1ecae2c7054d4e333ce4cd8a6793
parent60eff99f25dbfc9799043ad979537e2da1aea590
signature Signed by PGP key B5690EEEBB952194

fix(web): collapse a tool call by clicking its expanded label (#11017)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

2 files changed, 62 insertions(+), 1 deletions(-)

apps/web/src/components/chat/MessagesTimeline.test.tsx+49
...@@ -1764,4 +1764,53 @@ describe("MessagesTimeline", () => {...@@ -1764,4 +1764,53 @@ describe("MessagesTimeline", () => {
1764 expect(markup).toContain("lucide-circle-alert");1764 expect(markup).toContain("lucide-circle-alert");
1765 expect(markup).toContain("text-destructive");1765 expect(markup).toContain("text-destructive");
1766 });1766 });
1767
1768 it("only withholds an expanded tool-call label click while text is selected", async () => {
1769 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
1770 vi.stubGlobal("requestAnimationFrame", () => 0);
1771 vi.stubGlobal("cancelAnimationFrame", () => {});
1772 let renderer: ReactTestRenderer | undefined;
1773 try {
1774 await act(() => {
1775 renderer = create(
1776 <MessagesTimeline
1777 {...buildProps()}
1778 timelineEntries={[
1779 {
1780 id: "entry-standalone",
1781 kind: "work",
1782 createdAt: MESSAGE_CREATED_AT,
1783 entry: {
1784 id: "work-standalone",
1785 createdAt: MESSAGE_CREATED_AT,
1786 toolCallId: "call-standalone",
1787 label: "Run lint",
1788 tone: "tool",
1789 itemType: "command_execution",
1790 command: "pnpm lint",
1791 toolLifecycleStatus: "completed",
1792 },
1793 },
1794 ]}
1795 />,
1796 );
1797 });
1798 await act(() => renderer!.root.findByProps({ "aria-expanded": false }).props.onClick());
1799 const label = renderer!.root.findAll(
1800 (node) => node.type === "span" && String(node.props.className).includes("select-text"),
1801 )[0];
1802 const stopPropagation = vi.fn();
1803 // Only the click that ends a selection may be withheld from the row
1804 // toggle; the plain click has to reach it so the label can collapse.
1805 for (const isCollapsed of [false, true]) {
1806 label!.props.onClick({
1807 currentTarget: { ownerDocument: { getSelection: () => ({ isCollapsed }) } },
1808 stopPropagation,
1809 });
1810 }
1811 expect(stopPropagation).toHaveBeenCalledTimes(1);
1812 } finally {
1813 await act(() => renderer?.unmount());
1814 }
1815 });
1767});1816});
apps/web/src/components/chat/MessagesTimeline.tsx+13-1
...@@ -3172,6 +3172,18 @@ function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName {...@@ -3172,6 +3172,18 @@ function workEntryIconName(workEntry: TimelineWorkEntry): WorkEntryIconName {
31723172
3173const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation();3173const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation();
31743174
3175/**
3176 * Click handler for expanded row labels, which turn text selection back on.
3177 * Only a click that ends a real selection is withheld from the row toggle, so
3178 * an ordinary click on the label still bubbles and collapses the row it opened.
3179 */
3180const stopRowToggleWhileSelectingText = (e: MouseEvent<HTMLElement>) => {
3181 const selection = e.currentTarget.ownerDocument.getSelection();
3182 if (selection && !selection.isCollapsed) {
3183 e.stopPropagation();
3184 }
3185};
3186
3175/**3187/**
3176 * A1 spawn CTA: one anchored row per workflow run (or per-turn direct-spawn3188 * A1 spawn CTA: one anchored row per workflow run (or per-turn direct-spawn
3177 * batch). Live status is derived from the shared agent panel model at render3189 * batch). Live status is derived from the shared agent panel model at render
...@@ -3407,7 +3419,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {...@@ -3407,7 +3419,7 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
3407 expanded ? "whitespace-pre-wrap break-words select-text" : "truncate",3419 expanded ? "whitespace-pre-wrap break-words select-text" : "truncate",
3408 headingClass,3420 headingClass,
3409 )}3421 )}
3410 onClick={expanded ? stopRowToggle : undefined}3422 onClick={expanded ? stopRowToggleWhileSelectingText : undefined}
3411 onPointerDown={expanded ? stopRowToggle : undefined}3423 onPointerDown={expanded ? stopRowToggle : undefined}
3412 >3424 >
3413 {previewText}3425 {previewText}