fix(chat-ui): avoid animated initial scroll

This commit is contained in:
Peter Steinberger
2025-12-21 12:33:41 +01:00
parent 5adec0eae0
commit 4021da524c

View File

@@ -10,6 +10,7 @@ public struct ClawdisChatView: View {
@State private var viewModel: ClawdisChatViewModel
@State private var scrollerBottomID = UUID()
@State private var showSessions = false
@State private var hasPerformedInitialScroll = false
private let showsSessionSwitcher: Bool
private let style: Style
@@ -67,44 +68,59 @@ public struct ClawdisChatView: View {
private var messageList: some View {
ScrollViewReader { proxy in
ScrollView {
LazyVStack(spacing: Layout.messageSpacing) {
ForEach(self.visibleMessages) { msg in
ChatMessageBubble(message: msg, style: self.style)
.frame(
maxWidth: .infinity,
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
}
ZStack {
ScrollView {
LazyVStack(spacing: Layout.messageSpacing) {
ForEach(self.visibleMessages) { msg in
ChatMessageBubble(message: msg, style: self.style)
.frame(
maxWidth: .infinity,
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
}
if self.viewModel.pendingRunCount > 0 {
ChatTypingIndicatorBubble(style: self.style)
.frame(maxWidth: .infinity, alignment: .leading)
}
if self.viewModel.pendingRunCount > 0 {
ChatTypingIndicatorBubble(style: self.style)
.frame(maxWidth: .infinity, alignment: .leading)
}
if !self.viewModel.pendingToolCalls.isEmpty {
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
.frame(maxWidth: .infinity, alignment: .leading)
}
if !self.viewModel.pendingToolCalls.isEmpty {
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let text = self.viewModel.streamingAssistantText, !text.isEmpty {
ChatStreamingAssistantBubble(text: text)
.frame(maxWidth: .infinity, alignment: .leading)
}
if let text = self.viewModel.streamingAssistantText, !text.isEmpty {
ChatStreamingAssistantBubble(text: text)
.frame(maxWidth: .infinity, alignment: .leading)
}
Color.clear
.frame(height: 1)
.id(self.scrollerBottomID)
Color.clear
.frame(height: 1)
.id(self.scrollerBottomID)
}
.padding(.top, Layout.messageListPaddingTop)
.padding(.bottom, Layout.messageListPaddingBottom)
.padding(.horizontal, Layout.messageListPaddingHorizontal)
}
.padding(.top, Layout.messageListPaddingTop)
.padding(.bottom, Layout.messageListPaddingBottom)
.padding(.horizontal, Layout.messageListPaddingHorizontal)
if self.viewModel.isLoading {
ProgressView()
.controlSize(.large)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.onChange(of: self.viewModel.isLoading) { _, isLoading in
guard !isLoading, !self.hasPerformedInitialScroll else { return }
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
self.hasPerformedInitialScroll = true
}
.onChange(of: self.viewModel.messages.count) { _, _ in
guard self.hasPerformedInitialScroll else { return }
withAnimation(.snappy(duration: 0.22)) {
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
}
}
.onChange(of: self.viewModel.pendingRunCount) { _, _ in
guard self.hasPerformedInitialScroll else { return }
withAnimation(.snappy(duration: 0.22)) {
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
}