| author | |
| committer | |
| log | 502131adff7494d1a1797b4fe518870f7e8431fd |
| tree | f05d5dc5494f1ecae2c7054d4e333ce4cd8a6793 |
| parent | 60eff99f25dbfc9799043ad979537e2da1aea590 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 { |
| 3172 | 3172 | ||
| 3173 | const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation(); | 3173 | const stopRowToggle = (e: { stopPropagation: () => void }) => e.stopPropagation(); |
| 3174 | 3174 | ||
| 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 | */ | ||
| 3180 | const 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-spawn | 3188 | * 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 render | 3189 | * 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} |