From e158bee95fee822a70c47da788db8b7714d76612 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 24 Dec 2025 13:51:00 +0100 Subject: [PATCH] perf: reduce chat animation churn --- .../ClawdisChatUI/ChatMessageViews.swift | 34 +++++++++++++++++-- .../Sources/ClawdisChatUI/ChatView.swift | 2 ++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift index a266e221b..094785996 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatMessageViews.swift @@ -323,6 +323,13 @@ struct ChatTypingIndicatorBubble: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)) .frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading) + .focusable(false) + } +} + +extension ChatTypingIndicatorBubble: @MainActor Equatable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.style == rhs.style } } @@ -342,6 +349,7 @@ struct ChatStreamingAssistantBubble: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)) .frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading) + .focusable(false) } } @@ -376,12 +384,20 @@ struct ChatPendingToolsBubble: View { RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1)) .frame(maxWidth: ChatUIConstants.bubbleMaxWidth, alignment: .leading) + .focusable(false) + } +} + +extension ChatPendingToolsBubble: @MainActor Equatable { + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.toolCalls == rhs.toolCalls } } @MainActor private struct TypingDots: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.scenePhase) private var scenePhase @State private var animate = false var body: some View { @@ -399,10 +415,22 @@ private struct TypingDots: View { value: self.animate) } } - .onAppear { - guard !self.reduceMotion else { return } - self.animate = true + .onAppear { self.updateAnimationState() } + .onDisappear { self.animate = false } + .onChange(of: self.scenePhase) { _, _ in + self.updateAnimationState() } + .onChange(of: self.reduceMotion) { _, _ in + self.updateAnimationState() + } + } + + private func updateAnimationState() { + guard !self.reduceMotion, self.scenePhase == .active else { + self.animate = false + return + } + self.animate = true } } diff --git a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift index 2a1e5894d..9a138e87a 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisChatUI/ChatView.swift @@ -80,11 +80,13 @@ public struct ClawdisChatView: View { if self.viewModel.pendingRunCount > 0 { ChatTypingIndicatorBubble(style: self.style) + .equatable() .frame(maxWidth: .infinity, alignment: .leading) } if !self.viewModel.pendingToolCalls.isEmpty { ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls) + .equatable() .frame(maxWidth: .infinity, alignment: .leading) }