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) {
242242 (store) => store.draftThreadsByThreadId[threadId] ?? null,
243243 );
244244 const promptRef = useRef(prompt);
245 const [showScrollToBottom, setShowScrollToBottom] = useState(false);
245246 const [isDragOverComposer, setIsDragOverComposer] = useState(false);
246247 const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null);
247248 const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]);
......@@ -1587,6 +1588,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
15871588 }
15881589 }
15891590
1591 setShowScrollToBottom(!shouldAutoScrollRef.current);
15901592 lastKnownScrollTopRef.current = currentScrollTop;
15911593 }, []);
15921594 const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => {
......@@ -3259,45 +3261,62 @@ export default function ChatView({ threadId }: ChatViewProps) {
32593261 <div className="flex min-h-0 min-w-0 flex-1">
32603262 {/* Chat column */}
32613263 <div className="flex min-h-0 min-w-0 flex-1 flex-col">
3262 {/* Messages */}
3263 <div
3264 ref={setMessagesScrollContainerRef}
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"
3266 onScroll={onMessagesScroll}
3267 onClickCapture={onMessagesClickCapture}
3268 onWheel={onMessagesWheel}
3269 onPointerDown={onMessagesPointerDown}
3270 onPointerUp={onMessagesPointerUp}
3271 onPointerCancel={onMessagesPointerCancel}
3272 onTouchStart={onMessagesTouchStart}
3273 onTouchMove={onMessagesTouchMove}
3274 onTouchEnd={onMessagesTouchEnd}
3275 onTouchCancel={onMessagesTouchEnd}
3276 >
3277 <MessagesTimeline
3278 key={activeThread.id}
3279 hasMessages={timelineEntries.length > 0}
3280 isWorking={isWorking}
3281 activeTurnInProgress={isWorking || !latestTurnSettled}
3282 activeTurnStartedAt={activeWorkStartedAt}
3283 scrollContainer={messagesScrollElement}
3284 timelineEntries={timelineEntries}
3285 completionDividerBeforeEntryId={completionDividerBeforeEntryId}
3286 completionSummary={completionSummary}
3287 turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
3288 nowIso={nowIso}
3289 expandedWorkGroups={expandedWorkGroups}
3290 onToggleWorkGroup={onToggleWorkGroup}
3291 onOpenTurnDiff={onOpenTurnDiff}
3292 revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
3293 onRevertUserMessage={onRevertUserMessage}
3294 isRevertingCheckpoint={isRevertingCheckpoint}
3295 onImageExpand={onExpandTimelineImage}
3296 markdownCwd={gitCwd ?? undefined}
3297 resolvedTheme={resolvedTheme}
3298 timestampFormat={timestampFormat}
3299 workspaceRoot={activeProject?.cwd ?? undefined}
3300 />
3264 {/* Messages Wrapper */}
3265 <div className="relative flex min-h-0 flex-1 flex-col">
3266 {/* Messages */}
3267 <div
3268 ref={setMessagesScrollContainerRef}
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"
3270 onScroll={onMessagesScroll}
3271 onClickCapture={onMessagesClickCapture}
3272 onWheel={onMessagesWheel}
3273 onPointerDown={onMessagesPointerDown}
3274 onPointerUp={onMessagesPointerUp}
3275 onPointerCancel={onMessagesPointerCancel}
3276 onTouchStart={onMessagesTouchStart}
3277 onTouchMove={onMessagesTouchMove}
3278 onTouchEnd={onMessagesTouchEnd}
3279 onTouchCancel={onMessagesTouchEnd}
3280 >
3281 <MessagesTimeline
3282 key={activeThread.id}
3283 hasMessages={timelineEntries.length > 0}
3284 isWorking={isWorking}
3285 activeTurnInProgress={isWorking || !latestTurnSettled}
3286 activeTurnStartedAt={activeWorkStartedAt}
3287 scrollContainer={messagesScrollElement}
3288 timelineEntries={timelineEntries}
3289 completionDividerBeforeEntryId={completionDividerBeforeEntryId}
3290 completionSummary={completionSummary}
3291 turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
3292 nowIso={nowIso}
3293 expandedWorkGroups={expandedWorkGroups}
3294 onToggleWorkGroup={onToggleWorkGroup}
3295 onOpenTurnDiff={onOpenTurnDiff}
3296 revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
3297 onRevertUserMessage={onRevertUserMessage}
3298 isRevertingCheckpoint={isRevertingCheckpoint}
3299 onImageExpand={onExpandTimelineImage}
3300 markdownCwd={gitCwd ?? undefined}
3301 resolvedTheme={resolvedTheme}
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 )}
33013320 </div>
33023321
33033322 {/* Input bar */}
......@@ -3328,7 +3347,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
33283347 <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
33293348 <ComposerPendingUserInputPanel
33303349 pendingUserInputs={pendingUserInputs}
3331 respondingRequestIds={respondingUserInputRequestIds}
3350 respondingRequestIds={respondingRequestIds}
33323351 answers={activePendingDraftAnswers}
33333352 questionIndex={activePendingQuestionIndex}
33343353 onSelectOption={onSelectActivePendingUserInputOption}