@@ -47,6 +47,7 @@ import androidx.compose.ui.text.LinkAnnotation
4747import androidx.compose.ui.text.SpanStyle
4848import androidx.compose.ui.text.TextLinkStyles
4949import androidx.compose.ui.text.buildAnnotatedString
50+import androidx.compose.ui.text.font.FontFamily
5051import androidx.compose.ui.text.font.FontStyle
5152import androidx.compose.ui.text.withLink
5253import androidx.compose.ui.text.withStyle
@@ -131,8 +132,9 @@ fun MessageBubble(message: MessageView, modifier: Modifier = Modifier, onRetry:
131132 SelectionContainer {
132133 if (message.text.isNotEmpty()) {
133134 val linkColor = MaterialTheme.colorScheme.primary
134− val annotatedText = remember(message.text, message.streaming, linkColor) {
135− buildLinkedText(message.text, message.streaming, linkColor, context)
135+ val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
136+ val annotatedText = remember(message.text, message.streaming, linkColor, codeBackground, context) {
137+ buildLinkedText(message.text, message.streaming, linkColor, codeBackground, context)
136138 }
137139 Text(
138140 text = annotatedText,
@@ -174,16 +176,39 @@ private fun copyToClipboard(context: Context, text: String) {
174176private val urlRegex = Regex("""https?://[^\s]+""")
175177
176178/**
177− * Render [text] as an AnnotatedString where each http(s) URL becomes a clickable link
178− * that opens in the browser. Falls back to plain text when no URLs are present. The
179− * streaming cursor (" ▌") is appended outside any link annotation.
179+ * Render [text] as an AnnotatedString. Triple-backtick fences split the text into
180+ * prose and code segments: code segments are rendered monospace on a tinted
181+ * [codeBackground] (fences dropped), while prose has each http(s) URL turned into a
182+ * clickable link. The streaming cursor (" ▌") is appended outside any annotation.
180183 */
181184private fun buildLinkedText(
182185 text: String,
183186 streaming: Boolean,
184187 linkColor: Color,
188+ codeBackground: Color,
185189 context: Context,
186190): AnnotatedString = buildAnnotatedString {
191+ // Odd-indexed segments sit between a pair of ``` fences, so they're code. An
192+ // unclosed fence leaves the trailing segment styled as code, mirroring markdown.
193+ val segments = text.split("```")
194+ segments.forEachIndexed { index, segment ->
195+ if (index % 2 == 1) {
196+ withStyle(SpanStyle(fontFamily = FontFamily.Monospace, background = codeBackground)) {
197+ append(segment)
198+ }
199+ } else {
200+ appendLinkedText(segment, linkColor, context)
201+ }
202+ }
203+ if (streaming) append(" ▌")
204+}
205+
206+/** Append [text] to the builder, turning each http(s) URL into a clickable link. */
207+private fun AnnotatedString.Builder.appendLinkedText(
208+ text: String,
209+ linkColor: Color,
210+ context: Context,
211+) {
187212 val matches = urlRegex.findAll(text).toList()
188213 var cursor = 0
189214 for (match in matches) {
@@ -207,7 +232,6 @@ private fun buildLinkedText(
207232 cursor = match.range.last + 1
208233 }
209234 append(text.substring(cursor))
210− if (streaming) append(" ▌")
211235}
212236
213237private fun shareText(context: Context, text: String) {