@@ -19,6 +19,9 @@ struct MessageBubble: View {
1919 /// Threshold (character count) for collapsing long text
2020 private static let longTextThreshold = 500
2121
22+ /// Height the collapsed (long) user bubble is clipped to before "Show more".
23+ private static let collapsedMaxHeight : CGFloat = 120
24+
2225 private enum AssistantRenderBlock : Identifiable {
2326 case text( MessageBlock )
2427 case tool( ToolCall )
@@ -234,24 +237,25 @@ struct MessageBubble: View {
234237 } else {
235238 VStack ( alignment: . leading, spacing: 6 ) {
236239 let isLong = displayText. count > Self . longTextThreshold
237- ChatTextContentView (
238- attributed: chipifiedAttributedString ( displayText) ,
239- size: ClaudeTheme . messageSize ( 14 ) ,
240- color: ClaudeTheme . userBubbleText,
241- maximumNumberOfLines: isLong && !isLongTextExpanded ? 5 : nil
240+ let collapsed = isLong && !isLongTextExpanded
241+ MarkdownContentView (
242+ text: markdownUserText ( displayText) ,
243+ style: . rxCodeChatUser
242244 ) { url in
243245 // Intercept the synthetic `rxcode-image://<index>` link emitted
244- // by chipifiedAttributedString and open the matching image in the
245- // preview sheet rather than the system browser.
246+ // by markdownUserText and open the matching image in the preview
247+ // sheet rather than the system browser.
246248 guard url. scheme == " rxcode-image " ,
247249 let index = Int ( url. host ?? " " ) ,
248250 let path = imagePath ( forChipIndex: index) else {
249- return false
251+ return . systemAction
250252 }
251253 previewImagePath = path
252- return true
254+ return . handled
253255 }
254- . fixedSize ( horizontal: false , vertical: true )
256+ . frame ( maxHeight: collapsed ? Self . collapsedMaxHeight : nil , alignment: . topLeading)
257+ . clipped ( )
258+ . fixedSize ( horizontal: false , vertical: !collapsed)
255259 if isLong {
256260 Button {
257261 withAnimation ( . easeInOut( duration: 0.2 ) ) {
@@ -660,32 +664,21 @@ struct MessageBubble: View {
660664 return result
661665 }
662666
663- /// Renders `[Image\d+]` tokens with accent-tinted chip styling. The same tokens
664- /// are inserted into the input bar by `WindowState.insertImageToken` and drawn
665- /// with a rounded background there via `ChipLayoutManager`; this mirrors that
666- /// treatment in the sent user bubble.
667- ///
668- /// Each chip also gets a `rxcode-image://<index>` link attribute; an
669- /// `environment(\.openURL, ...)` handler on the Text intercepts the tap and
670- /// opens the corresponding image in `MessageImagePreviewSheet`. The index
671- /// matches `WindowState.imageIndex(for:)` — 1-based, image-only.
672- private func chipifiedAttributedString( _ text: String ) -> AttributedString {
673- var attr = AttributedString ( text)
667+ /// Prepares user-message text for the markdown renderer by rewriting `[ImageN]`
668+ /// chip tokens into `[ImageN](rxcode-image://N)` links. The renderer then draws
669+ /// them as tappable accents; the `rxcode-image` scheme is intercepted by the
670+ /// bubble's link handler to open the image preview rather than the browser. The
671+ /// same `[ImageN]` tokens are inserted into the input bar by
672+ /// `WindowState.insertImageToken`; the index matches `WindowState.imageIndex(for:)`
673+ /// — 1-based, image-only.
674+ private func markdownUserText( _ text: String ) -> String {
674675 let ns = text as NSString
675676 let fullRange = NSRange ( location: 0 , length: ns. length)
676- Self . imageChipRegex. enumerateMatches ( in: text, range: fullRange) { match, _, _ in
677- guard let m = match,
678- let range = Range ( m. range, in: attr) ,
679- m. numberOfRanges >= 2 ,
680- let indexRange = Range ( m. range ( at: 1 ) , in: text) ,
681- let index = Int ( text [ indexRange] ) else { return }
682- attr [ range] . backgroundColor = ClaudeTheme . accent. opacity ( 0.22 )
683- attr [ range] . foregroundColor = ClaudeTheme . accent
684- attr [ range] . font = . system( size: ClaudeTheme . messageSize ( 13 ) , weight: . medium)
685- attr [ range] . link = URL ( string: " rxcode-image:// \( index) " )
686- attr [ range] . underlineStyle = nil
687- }
688- return attr
677+ return Self . imageChipRegex. stringByReplacingMatches (
678+ in: text,
679+ range: fullRange,
680+ withTemplate: " [Image$1](rxcode-image://$1) "
681+ )
689682 }
690683
691684 /// Resolve `[ImageN]` chip index (1-based) to a concrete image file path.
0 commit comments