| ... | @@ -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 | } |
| 1589 | | 1590 | |
| | 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 | <div | 3265 | <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 | <MessagesTimeline | 3279 | 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> |
| 3302 | | 3321 | |
| 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 | <ComposerPendingUserInputPanel | 3348 | <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} |