authorgravatar for saishankar2803@gmail.comSai Shankar <saishankar2803@gmail.com> 2026-03-14 23:14:55+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-14 10:44:55-07:00
log2bb71f41c34ef1fc2444901a64242f84601075bc
treed8cbe5029b52c73bc6100ae4923e9264a5f9c05e
parentcc2ab000745f71a07220d1ad620b1f23b4f6f430
signature Signed by PGP key B5690EEEBB952194

feat(web): add scroll to bottom pill in chat view (#619)


1 files changed, 59 insertions(+), 40 deletions(-)

apps/web/src/components/ChatView.tsx+59-40
...@@ -242,6 +242,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -242,6 +242,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
242 (store) => store.draftThreadsByThreadId[threadId] ?? null,242 (store) => store.draftThreadsByThreadId[threadId] ?? null,
243 );243 );
244 const promptRef = useRef(prompt);244 const promptRef = useRef(prompt);
245 const [showScrollToBottom, setShowScrollToBottom] = useState(false);
245 const [isDragOverComposer, setIsDragOverComposer] = useState(false);246 const [isDragOverComposer, setIsDragOverComposer] = useState(false);
246 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);247 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
247 const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);248 const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
...@@ -1587,6 +1588,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -1587,6 +1588,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
1587 }1588 }
1588 }1589 }
15891590
1591 setShowScrollToBottom(!shouldAutoScrollRef.current);
1590 lastKnownScrollTopRef.current = currentScrollTop;1592 lastKnownScrollTopRef.current = currentScrollTop;
1591 }, []);1593 }, []);
1592 const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {1594 const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
...@@ -3259,45 +3261,62 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3259,45 +3261,62 @@ export default function ChatView({ threadId }: ChatViewProps) {
3259 <div className="flex min-h-0 min-w-0 flex-1">3261 <div className="flex min-h-0 min-w-0 flex-1">
3260 {/* Chat column */}3262 {/* Chat column */}
3261 <div className="flex min-h-0 min-w-0 flex-1 flex-col">3263 <div className="flex min-h-0 min-w-0 flex-1 flex-col">
3262 {/* Messages */}3264 {/* Messages Wrapper */}
3263 <div3265 <div className="relative flex min-h-0 flex-1 flex-col">
3264 ref={setMessagesScrollContainerRef}3266 {/* Messages */}
3265 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"3267 <div
3266 onScroll={onMessagesScroll}3268 ref={setMessagesScrollContainerRef}
3267 onClickCapture={onMessagesClickCapture}3269 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"
3268 onWheel={onMessagesWheel}3270 onScroll={onMessagesScroll}
3269 onPointerDown={onMessagesPointerDown}3271 onClickCapture={onMessagesClickCapture}
3270 onPointerUp={onMessagesPointerUp}3272 onWheel={onMessagesWheel}
3271 onPointerCancel={onMessagesPointerCancel}3273 onPointerDown={onMessagesPointerDown}
3272 onTouchStart={onMessagesTouchStart}3274 onPointerUp={onMessagesPointerUp}
3273 onTouchMove={onMessagesTouchMove}3275 onPointerCancel={onMessagesPointerCancel}
3274 onTouchEnd={onMessagesTouchEnd}3276 onTouchStart={onMessagesTouchStart}
3275 onTouchCancel={onMessagesTouchEnd}3277 onTouchMove={onMessagesTouchMove}
3276 >3278 onTouchEnd={onMessagesTouchEnd}
3277 <MessagesTimeline3279 onTouchCancel={onMessagesTouchEnd}
3278 key={activeThread.id}3280 >
3279 hasMessages={timelineEntries.length > 0}3281 <MessagesTimeline
3280 isWorking={isWorking}3282 key={activeThread.id}
3281 activeTurnInProgress={isWorking || !latestTurnSettled}3283 hasMessages={timelineEntries.length > 0}
3282 activeTurnStartedAt={activeWorkStartedAt}3284 isWorking={isWorking}
3283 scrollContainer={messagesScrollElement}3285 activeTurnInProgress={isWorking || !latestTurnSettled}
3284 timelineEntries={timelineEntries}3286 activeTurnStartedAt={activeWorkStartedAt}
3285 completionDividerBeforeEntryId={completionDividerBeforeEntryId}3287 scrollContainer={messagesScrollElement}
3286 completionSummary={completionSummary}3288 timelineEntries={timelineEntries}
3287 turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}3289 completionDividerBeforeEntryId={completionDividerBeforeEntryId}
3288 nowIso={nowIso}3290 completionSummary={completionSummary}
3289 expandedWorkGroups={expandedWorkGroups}3291 turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
3290 onToggleWorkGroup={onToggleWorkGroup}3292 nowIso={nowIso}
3291 onOpenTurnDiff={onOpenTurnDiff}3293 expandedWorkGroups={expandedWorkGroups}
3292 revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}3294 onToggleWorkGroup={onToggleWorkGroup}
3293 onRevertUserMessage={onRevertUserMessage}3295 onOpenTurnDiff={onOpenTurnDiff}
3294 isRevertingCheckpoint={isRevertingCheckpoint}3296 revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
3295 onImageExpand={onExpandTimelineImage}3297 onRevertUserMessage={onRevertUserMessage}
3296 markdownCwd={gitCwd ?? undefined}3298 isRevertingCheckpoint={isRevertingCheckpoint}
3297 resolvedTheme={resolvedTheme}3299 onImageExpand={onExpandTimelineImage}
3298 timestampFormat={timestampFormat}3300 markdownCwd={gitCwd ?? undefined}
3299 workspaceRoot={activeProject?.cwd ?? undefined}3301 resolvedTheme={resolvedTheme}
3300 />3302 timestampFormat={timestampFormat}
3303 workspaceRoot={activeProject?.cwd ?? undefined}
3304 />
3305 </div>
3306
3307 {/* scroll to bottom pill — shown when user has scrolled away from the bottom */}
3308 {showScrollToBottom && (
3309 <div className="pointer-events-none absolute bottom-1 left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5">
3310 <button
3311 type="button"
3312 onClick={() => scrollMessagesToBottom("smooth")}
3313 className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground"
3314 >
3315 <ChevronDownIcon className="size-3.5" />
3316 Scroll to bottom
3317 </button>
3318 </div>
3319 )}
3301 </div>3320 </div>
33023321
3303 {/* Input bar */}3322 {/* Input bar */}
...@@ -3328,7 +3347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {...@@ -3328,7 +3347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
3328 <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">3347 <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
3329 <ComposerPendingUserInputPanel3348 <ComposerPendingUserInputPanel
3330 pendingUserInputs={pendingUserInputs}3349 pendingUserInputs={pendingUserInputs}
3331 respondingRequestIds={respondingUserInputRequestIds}3350 respondingRequestIds={respondingRequestIds}
3332 answers={activePendingDraftAnswers}3351 answers={activePendingDraftAnswers}
3333 questionIndex={activePendingQuestionIndex}3352 questionIndex={activePendingQuestionIndex}
3334 onSelectOption={onSelectActivePendingUserInputOption}3353 onSelectOption={onSelectActivePendingUserInputOption}