|
|
@@ -1564,9 +1564,16 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
}
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
guard let self else { return }
|
|
|
+ let scroll: () -> Void = {
|
|
|
+ if isUser {
|
|
|
+ self.scrollChatToBottom()
|
|
|
+ } else {
|
|
|
+ self.scrollChatToShowTopOfView(host)
|
|
|
+ }
|
|
|
+ }
|
|
|
if self.prefersReducedMotion {
|
|
|
self.updateChatBubbleWidths()
|
|
|
- self.scrollChatToBottom()
|
|
|
+ scroll()
|
|
|
return
|
|
|
}
|
|
|
NSAnimationContext.runAnimationGroup { ctx in
|
|
|
@@ -1575,7 +1582,7 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
host.animator().alphaValue = 1
|
|
|
}
|
|
|
self.updateChatBubbleWidths()
|
|
|
- self.scrollChatToBottom()
|
|
|
+ scroll()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1746,6 +1753,24 @@ final class DashboardView: NSView, NSTextFieldDelegate {
|
|
|
chatScrollView.reflectScrolledClipView(chatScrollView.contentView)
|
|
|
}
|
|
|
|
|
|
+ /// Scrolls so the top of `view` sits at the top of the chat clip (flipped document coords). Long assistant replies stay readable from the first line instead of jumping to the end.
|
|
|
+ private func scrollChatToShowTopOfView(_ view: NSView) {
|
|
|
+ chatDocumentView.layoutSubtreeIfNeeded()
|
|
|
+ view.layoutSubtreeIfNeeded()
|
|
|
+ let doc = chatDocumentView
|
|
|
+ let visibleHeight = chatScrollView.contentView.bounds.height
|
|
|
+ let docHeight = doc.bounds.height
|
|
|
+ guard docHeight > 0, visibleHeight > 0 else {
|
|
|
+ scrollChatToBottom()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ let rectInDoc = view.convert(view.bounds, to: doc)
|
|
|
+ let maxY = max(0, docHeight - visibleHeight)
|
|
|
+ let targetY = min(max(0, rectInDoc.minY), maxY)
|
|
|
+ chatScrollView.contentView.scroll(to: NSPoint(x: 0, y: targetY))
|
|
|
+ chatScrollView.reflectScrolledClipView(chatScrollView.contentView)
|
|
|
+ }
|
|
|
+
|
|
|
private func addInlineChatThinkingRow() {
|
|
|
removeInlineChatThinkingRow()
|
|
|
let host = NSView()
|