@@ -1924,6 +1924,8 @@ struct GaryxMessageBubble: View {
19241924 if let notification = taskNotification {
19251925 GaryxTaskNotificationCard ( notification: notification)
19261926 . garyxMessageInteraction ( text: taskNotificationCopyText ( notification) , edge: . trailing)
1927+ } else if messagePresentation == . historySkeleton {
1928+ GaryxUserMessageLoadingBubble ( )
19271929 } else if !displayText. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty {
19281930 GaryxMarkdownText (
19291931 text: displayText,
@@ -1956,8 +1958,8 @@ struct GaryxMessageBubble: View {
19561958 . garyxMessageCopyContext ( text: messageCopyText)
19571959 }
19581960 if message. isStreaming && message. text. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty {
1959- if message . attachments . isEmpty {
1960- GaryxThinkingLabel ( )
1961+ if case . thinkingLabel ( let text ) = messagePresentation {
1962+ GaryxThinkingLabel ( text : text )
19611963 }
19621964 } else if let notification = taskNotification {
19631965 GaryxTaskNotificationCard ( notification: notification)
@@ -2005,16 +2007,12 @@ struct GaryxMessageBubble: View {
20052007 }
20062008 }
20072009
2010+ private var messagePresentation : GaryxMobileMessagePresentation {
2011+ GaryxMobileMessagePresentation . make ( for: message)
2012+ }
2013+
20082014 private var displayText : String {
2009- if message. text. isEmpty, message. isStreaming { return " Thinking " }
2010- if !message. attachments. isEmpty,
2011- let summary = GaryxStructuredContentRenderer . attachmentSummary (
2012- from: message. attachments. map ( \. contentDescriptor)
2013- ) ,
2014- message. text == summary {
2015- return " "
2016- }
2017- return message. text
2015+ messagePresentation. text
20182016 }
20192017
20202018 private var taskNotification : GaryxTaskNotification ? {
@@ -2522,12 +2520,41 @@ struct GaryxMessageFileAttachmentView: View {
25222520}
25232521
25242522struct GaryxThinkingLabel : View {
2523+ var text : String = " Thinking "
2524+
25252525 var body : some View {
2526- GaryxShimmerText ( text: " Thinking " , font: GaryxFont . body ( ) )
2526+ GaryxShimmerText ( text: text , font: GaryxFont . body ( ) )
25272527 . frame ( minHeight: 22 )
25282528 }
25292529}
25302530
2531+ struct GaryxUserMessageLoadingBubble : View {
2532+ private static let shimmerDuration : Double = 2.4
2533+
2534+ var body : some View {
2535+ TimelineView ( . animation( minimumInterval: 1.0 / 30.0 , paused: false ) ) { context in
2536+ let normalized = context. date. timeIntervalSinceReferenceDate
2537+ . truncatingRemainder ( dividingBy: Self . shimmerDuration) / Self. shimmerDuration
2538+ let phase = CGFloat ( normalized) * 2.0 - 0.5
2539+ let fill = LinearGradient (
2540+ colors: [
2541+ Color . primary. opacity ( 0.05 ) ,
2542+ Color . primary. opacity ( 0.11 ) ,
2543+ Color . primary. opacity ( 0.05 ) ,
2544+ ] ,
2545+ startPoint: UnitPoint ( x: phase - 0.6 , y: 0.35 ) ,
2546+ endPoint: UnitPoint ( x: phase + 0.6 , y: 0.65 )
2547+ )
2548+
2549+ RoundedRectangle ( cornerRadius: 19 , style: . continuous)
2550+ . fill ( fill)
2551+ . frame ( width: 156 , height: 38 )
2552+ }
2553+ . accessibilityElement ( children: . ignore)
2554+ . accessibilityLabel ( " Loading message " )
2555+ }
2556+ }
2557+
25312558/// Tail banner shown when the selected thread's last run was cut off by the
25322559/// provider's usage quota. The countdown re-derives every second from the
25332560/// server-provided reset time via `GaryxRateLimitBannerModel`; when the gateway
0 commit comments