| ... | ... | @@ -242,6 +242,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 242 | 242 | (store) => store.draftThreadsByThreadId[threadId] ?? null, |
| 243 | 243 | ); |
| 244 | 244 | const promptRef = useRef(prompt); |
| 245 | const [showScrollToBottom, setShowScrollToBottom] = useState(false); |
| 245 | 246 | const [isDragOverComposer, setIsDragOverComposer] = useState(false); |
| 246 | 247 | const [expandedImage, setExpandedImage] = useState<ExpandedImagePreview | null>(null); |
| 247 | 248 | const [optimisticUserMessages, setOptimisticUserMessages] = useState<ChatMessage[]>([]); |
| ... | ... | @@ -1587,6 +1588,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 1587 | 1588 | } |
| 1588 | 1589 | } |
| 1589 | 1590 | |
| 1591 | setShowScrollToBottom(!shouldAutoScrollRef.current); |
| 1590 | 1592 | lastKnownScrollTopRef.current = currentScrollTop; |
| 1591 | 1593 | }, []); |
| 1592 | 1594 | const onMessagesWheel = useCallback((event: React.WheelEvent<HTMLDivElement>) => { |
| ... | ... | @@ -3259,45 +3261,62 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3259 | 3261 | <div className="flex min-h-0 min-w-0 flex-1"> |
| 3260 | 3262 | {/* Chat column */} |
| 3261 | 3263 | <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 | )} |
| 3301 | 3320 | </div> |
| 3302 | 3321 | |
| 3303 | 3322 | {/* Input bar */} |
| ... | ... | @@ -3328,7 +3347,7 @@ export default function ChatView({ threadId }: ChatViewProps) { |
| 3328 | 3347 | <div className="rounded-t-[19px] border-b border-border/65 bg-muted/20"> |
| 3329 | 3348 | <ComposerPendingUserInputPanel |
| 3330 | 3349 | pendingUserInputs={pendingUserInputs} |
| 3331 | | respondingRequestIds={respondingUserInputRequestIds} |
| 3350 | respondingRequestIds={respondingRequestIds} |
| 3332 | 3351 | answers={activePendingDraftAnswers} |
| 3333 | 3352 | questionIndex={activePendingQuestionIndex} |
| 3334 | 3353 | onSelectOption={onSelectActivePendingUserInputOption} |