authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-05 15:39:37-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-05 23:00:41-08:00
logae7283e021a58c2717005a5745def6e4f0afa2aa
tree62ceef61f280206f86484644568e6a4ee3802de5
parent640a2cca9f9effc434d1296d7b625ee09bcba64e

style


1 files changed, 56 insertions(+), 9 deletions(-)

apps/web/src/components/ChatView.tsx+56-9
......@@ -637,6 +637,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
637637 const lastTouchClientYRef = useRef<number | null>(null);
638638 const pendingUserScrollUpIntentRef = useRef(false);
639639 const pendingAutoScrollFrameRef = useRef<number | null>(null);
640 const pendingInteractionAnchorRef = useRef<{
641 element: HTMLElement;
642 top: number;
643 } | null>(null);
644 const pendingInteractionAnchorFrameRef = useRef<number | null>(null);
640645 const composerEditorRef = useRef<ComposerPromptEditorHandle>(null);
641646 const composerFormRef = useRef<HTMLFormElement>(null);
642647 const composerFormHeightRef = useRef(0);
......@@ -1055,7 +1060,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
10551060 return [...serverMessagesWithPreviewHandoff, ...pendingMessages];
10561061 }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]);
10571062 const timelineEntries = useMemo(
1058 () => deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries),
1063 () =>
1064 deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries),
10591065 [activeThread?.proposedPlans, timelineMessages, workLogEntries],
10601066 );
10611067 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
......@@ -1675,6 +1681,12 @@ export default function ChatView({ threadId }: ChatViewProps) {
16751681 pendingAutoScrollFrameRef.current = null;
16761682 window.cancelAnimationFrame(pendingFrame);
16771683 }, []);
1684 const cancelPendingInteractionAnchorAdjustment = useCallback(() => {
1685 const pendingFrame = pendingInteractionAnchorFrameRef.current;
1686 if (pendingFrame === null) return;
1687 pendingInteractionAnchorFrameRef.current = null;
1688 window.cancelAnimationFrame(pendingFrame);
1689 }, []);
16781690 const scheduleStickToBottom = useCallback(() => {
16791691 if (pendingAutoScrollFrameRef.current !== null) return;
16801692 pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => {
......@@ -1682,6 +1694,40 @@ export default function ChatView({ threadId }: ChatViewProps) {
16821694 scrollMessagesToBottom();
16831695 });
16841696 }, [scrollMessagesToBottom]);
1697 const onMessagesClickCapture = useCallback(
1698 (event: React.MouseEvent<HTMLDivElement>) => {
1699 const scrollContainer = messagesScrollRef.current;
1700 if (!scrollContainer || !(event.target instanceof Element)) return;
1701
1702 const trigger = event.target.closest<HTMLElement>(
1703 "button, summary, [role='button'], [data-scroll-anchor-target]",
1704 );
1705 if (!trigger || !scrollContainer.contains(trigger)) return;
1706
1707 pendingInteractionAnchorRef.current = {
1708 element: trigger,
1709 top: trigger.getBoundingClientRect().top,
1710 };
1711
1712 cancelPendingInteractionAnchorAdjustment();
1713 pendingInteractionAnchorFrameRef.current = window.requestAnimationFrame(() => {
1714 pendingInteractionAnchorFrameRef.current = null;
1715 const anchor = pendingInteractionAnchorRef.current;
1716 pendingInteractionAnchorRef.current = null;
1717 const activeScrollContainer = messagesScrollRef.current;
1718 if (!anchor || !activeScrollContainer) return;
1719 if (!anchor.element.isConnected || !activeScrollContainer.contains(anchor.element)) return;
1720
1721 const nextTop = anchor.element.getBoundingClientRect().top;
1722 const delta = nextTop - anchor.top;
1723 if (Math.abs(delta) < 0.5) return;
1724
1725 activeScrollContainer.scrollTop += delta;
1726 lastKnownScrollTopRef.current = activeScrollContainer.scrollTop;
1727 });
1728 },
1729 [cancelPendingInteractionAnchorAdjustment],
1730 );
16851731 const forceStickToBottom = useCallback(() => {
16861732 cancelPendingStickToBottom();
16871733 scrollMessagesToBottom();
......@@ -1751,8 +1797,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
17511797 useEffect(() => {
17521798 return () => {
17531799 cancelPendingStickToBottom();
1800 cancelPendingInteractionAnchorAdjustment();
17541801 };
1755 }, [cancelPendingStickToBottom]);
1802 }, [cancelPendingInteractionAnchorAdjustment, cancelPendingStickToBottom]);
17561803 useLayoutEffect(() => {
17571804 if (!activeThread?.id) return;
17581805 shouldAutoScrollRef.current = true;
......@@ -2279,7 +2326,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
22792326 if (showPlanFollowUpPrompt && activeProposedPlan) {
22802327 const followUpText =
22812328 trimmed.length > 0
2282 ? trimmed
2329 ? trimmed
22832330 : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown);
22842331 const nextInteractionMode = trimmed.length > 0 ? "plan" : "default";
22852332 promptRef.current = "";
......@@ -3323,6 +3370,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
33233370 ref={setMessagesScrollContainerRef}
33243371 className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
33253372 onScroll={onMessagesScroll}
3373 onClickCapture={onMessagesClickCapture}
33263374 onWheel={onMessagesWheel}
33273375 onPointerDown={onMessagesPointerDown}
33283376 onPointerUp={onMessagesPointerUp}
......@@ -4270,15 +4318,14 @@ const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({
42704318 return (
42714319 <div className="px-4 py-3.5 sm:px-5 sm:py-4">
42724320 <div className="flex flex-wrap items-center gap-2">
4273 <Badge variant="secondary">Plan ready</Badge>
4321 <span className="uppercase text-sm tracking-[0.2em]">Plan ready</span>
42744322 {planTitle ? (
4275 <span className="min-w-0 flex-1 truncate text-xs text-muted-foreground">{planTitle}</span>
4323 <span className="min-w-0 flex-1 truncate text-sm font-medium">{planTitle}</span>
42764324 ) : null}
42774325 </div>
4278 <div className="mt-2 text-xs text-muted-foreground">
4279 Leave the composer empty to implement here, use the menu to implement in a new thread, or
4280 type feedback to refine it.
4281 </div>
4326 {/* <div className="mt-2 text-xs text-muted-foreground">
4327 Review the plan
4328 </div> */}
42824329 </div>
42834330 );
42844331});