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) {...@@ -637,6 +637,11 @@ export default function ChatView({ threadId }: ChatViewProps) {
637 const lastTouchClientYRef = useRef<number | null>(null);637 const lastTouchClientYRef = useRef<number | null>(null);
638 const pendingUserScrollUpIntentRef = useRef(false);638 const pendingUserScrollUpIntentRef = useRef(false);
639 const pendingAutoScrollFrameRef = useRef<number | null>(null);639 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);
640 const composerEditorRef = useRef<ComposerPromptEditorHandle>(null);645 const composerEditorRef = useRef<ComposerPromptEditorHandle>(null);
641 const composerFormRef = useRef<HTMLFormElement>(null);646 const composerFormRef = useRef<HTMLFormElement>(null);
642 const composerFormHeightRef = useRef(0);647 const composerFormHeightRef = useRef(0);
...@@ -1055,7 +1060,8 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1055,7 +1060,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
1055 return [...serverMessagesWithPreviewHandoff, ...pendingMessages];1060 return [...serverMessagesWithPreviewHandoff, ...pendingMessages];
1056 }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]);1061 }, [serverMessages, attachmentPreviewHandoffByMessageId, optimisticUserMessages]);
1057 const timelineEntries = useMemo(1062 const timelineEntries = useMemo(
1058 () => deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries),1063 () =>
1064 deriveTimelineEntries(timelineMessages, activeThread?.proposedPlans ?? [], workLogEntries),
1059 [activeThread?.proposedPlans, timelineMessages, workLogEntries],1065 [activeThread?.proposedPlans, timelineMessages, workLogEntries],
1060 );1066 );
1061 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =1067 const { turnDiffSummaries, inferredCheckpointTurnCountByTurnId } =
...@@ -1675,6 +1681,12 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1675,6 +1681,12 @@ export default function ChatView({ threadId }: ChatViewProps) {
1675 pendingAutoScrollFrameRef.current = null;1681 pendingAutoScrollFrameRef.current = null;
1676 window.cancelAnimationFrame(pendingFrame);1682 window.cancelAnimationFrame(pendingFrame);
1677 }, []);1683 }, []);
1684 const cancelPendingInteractionAnchorAdjustment = useCallback(() => {
1685 const pendingFrame = pendingInteractionAnchorFrameRef.current;
1686 if (pendingFrame === null) return;
1687 pendingInteractionAnchorFrameRef.current = null;
1688 window.cancelAnimationFrame(pendingFrame);
1689 }, []);
1678 const scheduleStickToBottom = useCallback(() => {1690 const scheduleStickToBottom = useCallback(() => {
1679 if (pendingAutoScrollFrameRef.current !== null) return;1691 if (pendingAutoScrollFrameRef.current !== null) return;
1680 pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => {1692 pendingAutoScrollFrameRef.current = window.requestAnimationFrame(() => {
...@@ -1682,6 +1694,40 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1682,6 +1694,40 @@ export default function ChatView({ threadId }: ChatViewProps) {
1682 scrollMessagesToBottom();1694 scrollMessagesToBottom();
1683 });1695 });
1684 }, [scrollMessagesToBottom]);1696 }, [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 );
1685 const forceStickToBottom = useCallback(() => {1731 const forceStickToBottom = useCallback(() => {
1686 cancelPendingStickToBottom();1732 cancelPendingStickToBottom();
1687 scrollMessagesToBottom();1733 scrollMessagesToBottom();
...@@ -1751,8 +1797,9 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1751,8 +1797,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
1751 useEffect(() => {1797 useEffect(() => {
1752 return () => {1798 return () => {
1753 cancelPendingStickToBottom();1799 cancelPendingStickToBottom();
1800 cancelPendingInteractionAnchorAdjustment();
1754 };1801 };
1755 }, [cancelPendingStickToBottom]);1802 }, [cancelPendingInteractionAnchorAdjustment, cancelPendingStickToBottom]);
1756 useLayoutEffect(() => {1803 useLayoutEffect(() => {
1757 if (!activeThread?.id) return;1804 if (!activeThread?.id) return;
1758 shouldAutoScrollRef.current = true;1805 shouldAutoScrollRef.current = true;
...@@ -2279,7 +2326,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -2279,7 +2326,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
2279 if (showPlanFollowUpPrompt && activeProposedPlan) {2326 if (showPlanFollowUpPrompt && activeProposedPlan) {
2280 const followUpText =2327 const followUpText =
2281 trimmed.length > 02328 trimmed.length > 0
2282 ? trimmed2329 ? trimmed
2283 : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown);2330 : buildPlanImplementationPrompt(activeProposedPlan.planMarkdown);
2284 const nextInteractionMode = trimmed.length > 0 ? "plan" : "default";2331 const nextInteractionMode = trimmed.length > 0 ? "plan" : "default";
2285 promptRef.current = "";2332 promptRef.current = "";
...@@ -3323,6 +3370,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3323,6 +3370,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
3323 ref={setMessagesScrollContainerRef}3370 ref={setMessagesScrollContainerRef}
3324 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"3371 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"
3325 onScroll={onMessagesScroll}3372 onScroll={onMessagesScroll}
3373 onClickCapture={onMessagesClickCapture}
3326 onWheel={onMessagesWheel}3374 onWheel={onMessagesWheel}
3327 onPointerDown={onMessagesPointerDown}3375 onPointerDown={onMessagesPointerDown}
3328 onPointerUp={onMessagesPointerUp}3376 onPointerUp={onMessagesPointerUp}
...@@ -4270,15 +4318,14 @@ const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({...@@ -4270,15 +4318,14 @@ const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBanner({
4270 return (4318 return (
4271 <div className="px-4 py-3.5 sm:px-5 sm:py-4">4319 <div className="px-4 py-3.5 sm:px-5 sm:py-4">
4272 <div className="flex flex-wrap items-center gap-2">4320 <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>
4274 {planTitle ? (4322 {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>
4276 ) : null}4324 ) : null}
4277 </div>4325 </div>
4278 <div className="mt-2 text-xs text-muted-foreground">4326 {/* <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, or4327 Review the plan
4280 type feedback to refine it.4328 </div> */}
4281 </div>
4282 </div>4329 </div>
4283 );4330 );
4284});4331});