authorgravatar for github@jayz.usJay Meistrich <github@jayz.us> 2026-08-07 11:12:19+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-07 12:12:19+02:00
logb792ed9f7b9c7c6386a14a1697c4393567812546
treee3a7152594fd3ddae0bb9a5f737432379c97c28d
parent2e66b1fdfc63d39822cd58948bcf43e8fbc239b6
signature Signed by PGP key B5690EEEBB952194

fix(web): stabilize chat timeline positioning (#5449)

Co-authored-by: codex <codex@users.noreply.github.com>

5 files changed, 133 insertions(+), 202 deletions(-)

apps/web/package.json+1-1
......@@ -21,7 +21,7 @@
2121 "@dnd-kit/utilities": "^3.2.2",
2222 "@effect/atom-react": "catalog:",
2323 "@formkit/auto-animate": "^0.9.0",
24 "@legendapp/list": "3.2.0",
24 "@legendapp/list": "3.3.3",
2525 "@lexical/react": "^0.41.0",
2626 "@pierre/diffs": "catalog:",
2727 "@pierre/trees": "1.0.0-beta.4",
apps/web/src/components/ChatView.tsx+32-104
......@@ -3532,12 +3532,8 @@ function ChatViewContent(props: ChatViewProps) {
35323532 const activeTimelineAnchorIndexRef = useRef<number | null>(null);
35333533 const anchorUserScrollGenerationRef = useRef(0);
35343534 const liveFollowUserScrollGenerationRef = useRef<number | null>(0);
3535 const pendingAnchorScrollRestoreRef = useRef<{
3536 readonly messageId: MessageId;
3537 readonly offset: number;
3538 readonly userScrollGeneration: number;
3539 } | null>(null);
3540 const anchorScrollRestoreFrameRef = useRef<number | null>(null);
3535 // Manual navigation stops live-follow without removing anchored end space.
3536 // Collapsing that space during a gesture clamps the viewport back to the end.
35413537 const cancelTimelineLiveFollowForUserNavigation = useCallback(() => {
35423538 anchorUserScrollGenerationRef.current += 1;
35433539 timelineScrollModeRef.current = "free-scrolling";
......@@ -3547,11 +3543,6 @@ function ChatViewContent(props: ChatViewProps) {
35473543 positionedTimelineAnchorRef.current = null;
35483544 settledTimelineAnchorRef.current = null;
35493545 activeTimelineAnchorIndexRef.current = null;
3550 pendingAnchorScrollRestoreRef.current = null;
3551 if (anchorScrollRestoreFrameRef.current !== null) {
3552 cancelAnimationFrame(anchorScrollRestoreFrameRef.current);
3553 anchorScrollRestoreFrameRef.current = null;
3554 }
35553546 }, []);
35563547 const cancelTimelineLiveFollowForUserNavigationRef = useRef(
35573548 cancelTimelineLiveFollowForUserNavigation,
......@@ -3607,7 +3598,6 @@ function ChatViewContent(props: ChatViewProps) {
36073598 },
36083599 [composerOverlayHeight],
36093600 );
3610
36113601 // Live-follow stays active after send/thread-open until an actual list scroll
36123602 // gesture opts out.
36133603 const scrollToEnd = useCallback((animated = false) => {
......@@ -3619,7 +3609,12 @@ function ChatViewContent(props: ChatViewProps) {
36193609 activeTimelineAnchorIndexRef.current = null;
36203610 showScrollDebouncer.current.cancel();
36213611 setShowScrollToBottom(false);
3622 void legendListRef.current?.scrollToEnd?.({ animated });
3612 setTimelineAnchor((current) =>
3613 current.messageId === null ? current : { ...current, messageId: null },
3614 );
3615 requestAnimationFrame(() => {
3616 void legendListRef.current?.scrollToEnd?.({ animated });
3617 });
36233618 }, []);
36243619 useEffect(() => {
36253620 let removeListeners: (() => void) | null = null;
......@@ -3758,75 +3753,23 @@ function ChatViewContent(props: ChatViewProps) {
37583753 }
37593754 return;
37603755 }
3761 const scrollNode = list.getScrollableNode();
3762 let finished = false;
3763 const finishAnimatedPositioning = () => {
3764 if (finished) {
3765 return;
3766 }
3767 finished = true;
3768 window.clearTimeout(fallbackTimer);
3769 scrollNode.removeEventListener("scrollend", finishAnimatedPositioning);
3770 if (positionedTimelineAnchorRef.current !== messageId) {
3771 return;
3772 }
3773 const scrollOffset = list.getState().scroll;
3774 void list.scrollToOffset({ offset: scrollOffset, animated: false });
3775 settledTimelineAnchorRef.current = messageId;
3776 };
3777 const fallbackTimer = window.setTimeout(finishAnimatedPositioning, 750);
3778 scrollNode.addEventListener("scrollend", finishAnimatedPositioning, { once: true });
3779 void list.scrollToIndex({
3780 index: anchorIndex,
3781 animated: true,
3782 viewPosition: 0,
3783 viewOffset: CHAT_LIST_ANCHOR_OFFSET,
3784 });
3756 void list
3757 .scrollToIndex({
3758 index: anchorIndex,
3759 animated: true,
3760 viewPosition: 0,
3761 viewOffset: CHAT_LIST_ANCHOR_OFFSET,
3762 })
3763 .then(() => {
3764 if (positionedTimelineAnchorRef.current !== messageId) {
3765 return;
3766 }
3767 settledTimelineAnchorRef.current = messageId;
3768 });
37853769 });
37863770 };
37873771 requestAnimationFrame(() => positionAnchor(12));
37883772 }, []);
3789 const onTimelineAnchorSizeChanged = useCallback((messageId: MessageId) => {
3790 if (settledTimelineAnchorRef.current !== messageId) {
3791 return;
3792 }
3793 if (liveFollowUserScrollGenerationRef.current === anchorUserScrollGenerationRef.current) {
3794 return;
3795 }
3796 const scrollOffset = legendListRef.current?.getState().scroll;
3797 if (scrollOffset === undefined) {
3798 return;
3799 }
3800 if (pendingAnchorScrollRestoreRef.current === null) {
3801 pendingAnchorScrollRestoreRef.current = {
3802 messageId,
3803 offset: scrollOffset,
3804 userScrollGeneration: anchorUserScrollGenerationRef.current,
3805 };
3806 }
3807 if (anchorScrollRestoreFrameRef.current !== null) {
3808 return;
3809 }
3810 anchorScrollRestoreFrameRef.current = requestAnimationFrame(() => {
3811 anchorScrollRestoreFrameRef.current = null;
3812 const pending = pendingAnchorScrollRestoreRef.current;
3813 pendingAnchorScrollRestoreRef.current = null;
3814 if (
3815 pending &&
3816 settledTimelineAnchorRef.current === pending.messageId &&
3817 pending.userScrollGeneration === anchorUserScrollGenerationRef.current
3818 ) {
3819 const list = legendListRef.current;
3820 const currentScrollOffset = list?.getState().scroll;
3821 if (
3822 typeof currentScrollOffset === "number" &&
3823 Math.abs(currentScrollOffset - pending.offset) <= 2
3824 ) {
3825 void list?.scrollToOffset({ offset: pending.offset, animated: false });
3826 }
3827 }
3828 });
3829 }, []);
38303773
38313774 const onIsAtEndChange = useCallback((isAtEnd: boolean) => {
38323775 if (
......@@ -3852,6 +3795,9 @@ function ChatViewContent(props: ChatViewProps) {
38523795 }
38533796 }, []);
38543797
3798 // Anchored end space intentionally disables LegendList's normal end-follow so
3799 // the sent message can stay near the top. T3 only owns streaming adjustments
3800 // during that mode; LegendList owns ordinary end-follow everywhere else.
38553801 useEffect(() => {
38563802 if (!activeThread?.id) {
38573803 return;
......@@ -3859,6 +3805,9 @@ function ChatViewContent(props: ChatViewProps) {
38593805 if (liveFollowUserScrollGenerationRef.current !== anchorUserScrollGenerationRef.current) {
38603806 return;
38613807 }
3808 if (timelineScrollModeRef.current !== "anchoring-new-turn") {
3809 return;
3810 }
38623811
38633812 let secondFrame: number | null = null;
38643813 const frame = requestAnimationFrame(() => {
......@@ -3880,28 +3829,13 @@ function ChatViewContent(props: ChatViewProps) {
38803829 return;
38813830 }
38823831
3883 if (timelineScrollModeRef.current === "anchoring-new-turn") {
3884 const metrics = getActiveTimelineTurnMetrics(list);
3885 if (!metrics) {
3886 return;
3887 }
3888 if (metrics.scrollDeltaToRevealEnd <= 1) {
3889 return;
3890 }
3891
3892 const nextOffset = list.getState().scroll + metrics.scrollDeltaToRevealEnd;
3893 void list.scrollToOffset({ offset: nextOffset, animated: false });
3894 return;
3895 }
3896
3897 if (timelineScrollModeRef.current !== "following-end") {
3898 return;
3899 }
3900 if (!timelineRealContentOverflowsViewport(list)) {
3832 const metrics = getActiveTimelineTurnMetrics(list);
3833 if (!metrics || metrics.scrollDeltaToRevealEnd <= 1) {
39013834 return;
39023835 }
39033836
3904 void list.scrollToEnd?.({ animated: false });
3837 const nextOffset = list.getState().scroll + metrics.scrollDeltaToRevealEnd;
3838 void list.scrollToOffset({ offset: nextOffset, animated: false });
39053839 });
39063840 });
39073841
......@@ -3911,12 +3845,7 @@ function ChatViewContent(props: ChatViewProps) {
39113845 cancelAnimationFrame(secondFrame);
39123846 }
39133847 };
3914 }, [
3915 activeThread?.id,
3916 timelineEntries,
3917 getActiveTimelineTurnMetrics,
3918 timelineRealContentOverflowsViewport,
3919 ]);
3848 }, [activeThread?.id, timelineEntries, getActiveTimelineTurnMetrics]);
39203849
39213850 useEffect(() => {
39223851 setPullRequestDialogState(null);
......@@ -6118,7 +6047,6 @@ function ChatViewContent(props: ChatViewProps) {
61186047 skills={activeProviderStatus?.skills ?? EMPTY_PROVIDER_SKILLS}
61196048 anchorMessageId={timelineAnchorMessageId}
61206049 onAnchorReady={onTimelineAnchorReady}
6121 onAnchorSizeChanged={onTimelineAnchorSizeChanged}
61226050 contentInsetEndAdjustment={composerOverlayHeight}
61236051 liveFollowEnabled={timelineLiveFollowEnabled}
61246052 onIsAtEndChange={onIsAtEndChange}
apps/web/src/components/chat/MessagesTimeline.test.tsx+7-7
......@@ -18,7 +18,6 @@ vi.mock("@legendapp/list/react", async () => {
1818 anchorMaxSize?: number;
1919 anchorOffset?: number;
2020 onReady?: (info: { anchorIndex: number }) => void;
21 onSizeChanged?: (size: number) => void;
2221 };
2322 contentInsetEndAdjustment?: number;
2423 className?: string;
......@@ -42,7 +41,6 @@ vi.mock("@legendapp/list/react", async () => {
4241 ref?: Ref<LegendListRef>;
4342 }) => {
4443 if (props.anchoredEndSpace) {
45 props.anchoredEndSpace.onSizeChanged?.(240);
4644 props.anchoredEndSpace.onReady?.({ anchorIndex: props.anchoredEndSpace.anchorIndex });
4745 }
4846 return (
......@@ -90,6 +88,11 @@ vi.mock("@legendapp/list/react", async () => {
9088 ? props.maintainVisibleContentPosition.size
9189 : undefined
9290 }
91 data-maintain-visible-content-position-restore={
92 typeof props.maintainVisibleContentPosition === "object"
93 ? Boolean(props.maintainVisibleContentPosition.shouldRestorePosition)
94 : undefined
95 }
9396 >
9497 {props.ListHeaderComponent}
9598 {props.data.map((item) => (
......@@ -192,7 +195,6 @@ function buildProps() {
192195 workspaceRoot: undefined,
193196 anchorMessageId: null,
194197 onAnchorReady: () => {},
195 onAnchorSizeChanged: () => {},
196198 contentInsetEndAdjustment: 0,
197199 liveFollowEnabled: true,
198200 onIsAtEndChange: () => {},
......@@ -386,7 +388,6 @@ describe("MessagesTimeline", () => {
386388
387389 it("anchors a sent attachment message using its measured height", () => {
388390 const onAnchorReady = vi.fn();
389 const onAnchorSizeChanged = vi.fn();
390391 const firstEntry = buildUserTimelineEntry("First prompt.");
391392 const secondEntry = {
392393 ...buildUserTimelineEntry("Newest prompt."),
......@@ -411,7 +412,6 @@ describe("MessagesTimeline", () => {
411412 {...buildProps()}
412413 anchorMessageId={secondEntry.message.id}
413414 onAnchorReady={onAnchorReady}
414 onAnchorSizeChanged={onAnchorSizeChanged}
415415 contentInsetEndAdjustment={144}
416416 timelineEntries={[firstEntry, secondEntry]}
417417 />,
......@@ -426,10 +426,10 @@ describe("MessagesTimeline", () => {
426426 expect(markup).not.toContain('data-maintain-scroll-at-end="enabled"');
427427 expect(markup).toContain('data-maintain-visible-content-position="object"');
428428 expect(markup).toContain('data-maintain-visible-content-position-data="true"');
429 expect(markup).toContain('data-maintain-visible-content-position-size="false"');
429 expect(markup).toContain('data-maintain-visible-content-position-size="true"');
430 expect(markup).toContain('data-maintain-visible-content-position-restore="true"');
430431 expect(onAnchorReady).toHaveBeenCalledOnce();
431432 expect(onAnchorReady).toHaveBeenCalledWith(secondEntry.message.id, 1);
432 expect(onAnchorSizeChanged).toHaveBeenCalledWith(secondEntry.message.id, 240);
433433 });
434434
435435 it("renders collapse controls for long user messages", () => {
apps/web/src/components/chat/MessagesTimeline.tsx+84-69
......@@ -29,7 +29,6 @@ import {
2929 type MouseEvent,
3030 type ReactNode,
3131} from "react";
32import { flushSync } from "react-dom";
3332import { LegendList, type LegendListRef } from "@legendapp/list/react";
3433import { FileDiff } from "@pierre/diffs/react";
3534import {
......@@ -142,7 +141,7 @@ interface TimelineRowSharedState {
142141 onImageExpand: (preview: ExpandedImagePreview) => void;
143142 onOpenTurnDiff: (turnId: TurnId, filePath?: string) => void;
144143 onToggleTurnFold: (turnId: TurnId) => void;
145 onToggleWorkGroup: (groupId: string, anchorElement?: HTMLElement) => void;
144 onToggleWorkGroup: (groupId: string, anchorKey: string) => void;
146145 agentPanelModel: AgentPanelModel;
147146 onOpenAgents: () => void;
148147}
......@@ -189,6 +188,14 @@ function TimelineLoadEarlierHeader({
189188}
190189const TIMELINE_LIST_FOOTER = <div className="h-3 sm:h-4" />;
191190const EMPTY_TIMELINE_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
191const TIMELINE_MAINTAIN_SCROLL_AT_END = {
192 animated: false,
193 on: {
194 dataChange: true,
195 itemLayout: true,
196 layout: true,
197 },
198} as const;
192199
193200// ---------------------------------------------------------------------------
194201// Props (public API)
......@@ -220,7 +227,6 @@ interface MessagesTimelineProps {
220227 skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;
221228 anchorMessageId: MessageId | null;
222229 onAnchorReady: (messageId: MessageId, anchorIndex: number) => void;
223 onAnchorSizeChanged: (messageId: MessageId, size: number) => void;
224230 contentInsetEndAdjustment: number;
225231 /**
226232 * Whether the timeline should keep pinning to the live edge as content
......@@ -267,7 +273,6 @@ export const MessagesTimeline = memo(function MessagesTimeline({
267273 skills = EMPTY_TIMELINE_SKILLS,
268274 anchorMessageId,
269275 onAnchorReady,
270 onAnchorSizeChanged,
271276 contentInsetEndAdjustment,
272277 liveFollowEnabled,
273278 onIsAtEndChange,
......@@ -278,51 +283,85 @@ export const MessagesTimeline = memo(function MessagesTimeline({
278283}: MessagesTimelineProps) {
279284 const [expandedTurnIds, setExpandedTurnIds] = useState<ReadonlySet<TurnId>>(new Set());
280285 const [expandedWorkGroupIds, setExpandedWorkGroupIds] = useState<ReadonlySet<string>>(new Set());
286 const [disclosureToggleSettling, setDisclosureToggleSettling] = useState(false);
281287 const [minimapStripMap] = useState(() => new Map<string, HTMLSpanElement>());
288 const disclosureAnchorKeyRef = useRef<string | null>(null);
289 const disclosureSettleFrameRef = useRef<number | null>(null);
290 const disclosureSettleSecondFrameRef = useRef<number | null>(null);
282291
283 const onToggleTurnFold = useCallback((turnId: TurnId) => {
284 setExpandedTurnIds((existing) => {
285 const next = new Set(existing);
286 if (next.has(turnId)) {
287 next.delete(turnId);
288 } else {
289 next.add(turnId);
292 useEffect(() => {
293 return () => {
294 if (disclosureSettleFrameRef.current !== null) {
295 cancelAnimationFrame(disclosureSettleFrameRef.current);
290296 }
291 return next;
292 });
297 if (disclosureSettleSecondFrameRef.current !== null) {
298 cancelAnimationFrame(disclosureSettleSecondFrameRef.current);
299 }
300 };
293301 }, []);
294 const onToggleWorkGroup = useCallback(
295 (groupId: string, anchorElement?: HTMLElement) => {
296 const anchorBottomBeforeToggle = anchorElement?.getBoundingClientRect().bottom ?? null;
297302
298 flushSync(() => {
299 setExpandedWorkGroupIds((existing) => {
300 const next = new Set(existing);
301 if (next.has(groupId)) {
302 next.delete(groupId);
303 } else {
304 next.add(groupId);
305 }
306 return next;
307 });
303 const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string) => {
304 disclosureAnchorKeyRef.current = anchorKey;
305 setDisclosureToggleSettling(true);
306 if (disclosureSettleFrameRef.current !== null) {
307 cancelAnimationFrame(disclosureSettleFrameRef.current);
308 }
309 if (disclosureSettleSecondFrameRef.current !== null) {
310 cancelAnimationFrame(disclosureSettleSecondFrameRef.current);
311 }
312 disclosureSettleFrameRef.current = requestAnimationFrame(() => {
313 disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => {
314 disclosureAnchorKeyRef.current = null;
315 setDisclosureToggleSettling(false);
316 disclosureSettleFrameRef.current = null;
317 disclosureSettleSecondFrameRef.current = null;
308318 });
319 });
320 }, []);
309321
310 if (anchorBottomBeforeToggle === null || !anchorElement) {
311 return;
312 }
322 const shouldRestoreVisibleContentPosition = useCallback((row: MessagesTimelineRow) => {
323 const disclosureAnchorKey = disclosureAnchorKeyRef.current;
324 return disclosureAnchorKey === null || row.id === disclosureAnchorKey;
325 }, []);
313326
314 const delta = anchorElement.getBoundingClientRect().bottom - anchorBottomBeforeToggle;
315 if (Math.abs(delta) < 0.5) {
316 return;
317 }
327 const maintainVisibleContentPosition = useMemo(
328 () => ({
329 data: true,
330 size: true,
331 shouldRestorePosition: shouldRestoreVisibleContentPosition,
332 }),
333 [shouldRestoreVisibleContentPosition],
334 );
318335
319 const list = listRef.current;
320 const currentScroll = list?.getState?.().scroll;
321 if (list && typeof currentScroll === "number") {
322 list.scrollToOffset({ offset: currentScroll + delta, animated: false });
323 }
336 const onToggleTurnFold = useCallback(
337 (turnId: TurnId) => {
338 suspendEndScrollMaintenanceForDisclosure(`turn-fold:${turnId}`);
339 setExpandedTurnIds((existing) => {
340 const next = new Set(existing);
341 if (next.has(turnId)) {
342 next.delete(turnId);
343 } else {
344 next.add(turnId);
345 }
346 return next;
347 });
348 },
349 [suspendEndScrollMaintenanceForDisclosure],
350 );
351 const onToggleWorkGroup = useCallback(
352 (groupId: string, anchorKey: string) => {
353 suspendEndScrollMaintenanceForDisclosure(anchorKey);
354 setExpandedWorkGroupIds((existing) => {
355 const next = new Set(existing);
356 if (next.has(groupId)) {
357 next.delete(groupId);
358 } else {
359 next.add(groupId);
360 }
361 return next;
362 });
324363 },
325 [listRef],
364 [suspendEndScrollMaintenanceForDisclosure],
326365 );
327366
328367 // An in-session interrupt leaves its turn expanded so the user keeps their
......@@ -394,22 +433,12 @@ export const MessagesTimeline = memo(function MessagesTimeline({
394433 },
395434 [anchorMessageId, onAnchorReady],
396435 );
397 const handleAnchorSizeChanged = useCallback(
398 (size: number) => {
399 if (anchorMessageId !== null) {
400 onAnchorSizeChanged(anchorMessageId, size);
401 }
402 },
403 [anchorMessageId, onAnchorSizeChanged],
404 );
405436 const anchoredEndSpace = useMemo(() => {
406437 const config = resolveChatListAnchoredEndSpace(rows, anchorMessageId, (row) =>
407438 row.kind === "message" ? row.message.id : null,
408439 );
409 return config
410 ? { ...config, onReady: handleAnchorReady, onSizeChanged: handleAnchorSizeChanged }
411 : undefined;
412 }, [anchorMessageId, handleAnchorReady, handleAnchorSizeChanged, rows]);
440 return config ? { ...config, onReady: handleAnchorReady } : undefined;
441 }, [anchorMessageId, handleAnchorReady, rows]);
413442
414443 const handleScroll = useCallback(() => {
415444 const state = listRef.current?.getState?.();
......@@ -554,21 +583,11 @@ export const MessagesTimeline = memo(function MessagesTimeline({
554583 {...(anchoredEndSpace ? { anchoredEndSpace } : {})}
555584 contentInsetEndAdjustment={contentInsetEndAdjustment}
556585 maintainScrollAtEnd={
557 anchoredEndSpace || !liveFollowEnabled
586 anchoredEndSpace || !liveFollowEnabled || disclosureToggleSettling
558587 ? false
559 : {
560 animated: false,
561 on: {
562 dataChange: true,
563 itemLayout: true,
564 layout: true,
565 },
566 }
588 : TIMELINE_MAINTAIN_SCROLL_AT_END
567589 }
568 maintainVisibleContentPosition={{
569 data: true,
570 size: false,
571 }}
590 maintainVisibleContentPosition={maintainVisibleContentPosition}
572591 onScroll={handleScroll}
573592 className={cn(
574593 "scrollbar-gutter-both h-full min-h-0 overflow-x-hidden overscroll-y-contain px-3 [overflow-anchor:none] sm:px-5",
......@@ -1381,11 +1400,7 @@ function WorkGroupToggleTimelineRow({
13811400 type="button"
13821401 className="flex w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-[12px] leading-5 transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70"
13831402 aria-expanded={row.expanded}
1384 onClick={(event) => {
1385 const anchorElement =
1386 event.currentTarget.closest<HTMLElement>("[data-timeline-row-id]") ?? event.currentTarget;
1387 ctx.onToggleWorkGroup(row.groupId, anchorElement);
1388 }}
1403 onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)}
13891404 >
13901405 <span className="flex size-5 shrink-0 items-center justify-center text-icon-muted">
13911406 <ChevronDownIcon
pnpm-lock.yaml+9-21
......@@ -536,8 +536,8 @@ importers:
536536 specifier: ^0.9.0
537537 version: 0.9.0
538538 '@legendapp/list':
539 specifier: 3.2.0
540 version: 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
539 specifier: 3.3.3
540 version: 3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
541541 '@lexical/react':
542542 specifier: ^0.41.0
543543 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31)
......@@ -2949,18 +2949,6 @@ packages:
29492949 resolution: {integrity: sha512-hloP58zRVCRSpgDxmqCWJNlizAlUgJFqG2ypq79DCvyv9tHjRYMDOcPFjzfl/A1/YxDvRCZz8wvZvmapQnKwFQ==}
29502950 engines: {node: '>=12'}
29512951
2952 '@legendapp/list@3.2.0':
2953 resolution: {integrity: sha512-bN+g/oQYjFz+UAyuBN4cmYJAwdJS1TdNcZZOVlh3+VwCQUWrsg0PH46Mvm76gdZSCYMfoFanPY4dKnILcYEzeg==}
2954 peerDependencies:
2955 react: '*'
2956 react-dom: '*'
2957 react-native: '*'
2958 peerDependenciesMeta:
2959 react-dom:
2960 optional: true
2961 react-native:
2962 optional: true
2963
29642952 '@legendapp/list@3.3.3':
29652953 resolution: {integrity: sha512-p3g4xG6f//s4XQKhuus2189GCQgOHEIbJXHePqeDxj+6UQQQyij4YBjyArNSCgqoP0c03sxDPSOuCFB128Ql6g==}
29662954 peerDependencies:
......@@ -12998,13 +12986,6 @@ snapshots:
1299812986 dependencies:
1299912987 jsbi: 4.3.2
1300012988
13001 '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
13002 dependencies:
13003 react: 19.2.6
13004 use-sync-external-store: 1.6.0(react@19.2.6)
13005 optionalDependencies:
13006 react-dom: 19.2.6(react@19.2.6)
13007
1300812989 '@legendapp/list@3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
1300912990 dependencies:
1301012991 react: 19.2.3
......@@ -13013,6 +12994,13 @@ snapshots:
1301312994 react-dom: 19.2.3(react@19.2.3)
1301412995 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
1301512996
12997 '@legendapp/list@3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
12998 dependencies:
12999 react: 19.2.6
13000 use-sync-external-store: 1.6.0(react@19.2.6)
13001 optionalDependencies:
13002 react-dom: 19.2.6(react@19.2.6)
13003
1301613004 '@lexical/clipboard@0.41.0':
1301713005 dependencies:
1301813006 '@lexical/html': 0.41.0