From 83ac8a3418ffc660f41ab4cf760ec8ee05cc53a7 Mon Sep 17 00:00:00 2001 From: m57 <41645758+Hy4ri@users.noreply.github.com> Date: Fri, 25 Sep 2026 22:47:00 +0300 Subject: [PATCH] fix(#1263): render completed reasoning as Markdown (#1269) --- .../ui/chat/components/ReasoningCardTest.kt | 24 +++++++++ .../ui/chat/components/MessageCards.kt | 53 +++++++++++-------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ReasoningCardTest.kt b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ReasoningCardTest.kt index 494509c1..94882e6e 100644 --- a/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ReasoningCardTest.kt +++ b/app/src/androidTest/java/com/m57/hermescontrol/ui/chat/components/ReasoningCardTest.kt @@ -71,4 +71,28 @@ class ReasoningCardTest { composeTestRule.onNodeWithTag("reasoning_card").performClick() composeTestRule.onNodeWithText("Show full").assertDoesNotExist() } + + @Test + fun completedReasoning_rendersMarkdown() { + renderInLazyColumn("**Important** completed thought") + + composeTestRule.onNodeWithTag("reasoning_card").performClick() + composeTestRule.onNodeWithText("Important completed thought").assertIsDisplayed() + composeTestRule.onNodeWithText("**Important** completed thought").assertDoesNotExist() + } + + @Test + fun streamingReasoning_remainsRawText() { + composeTestRule.setContent { + LazyColumn { + item(key = "reasoning") { + ReasoningCard(reasoningText = "**Thinking**", isStreaming = true) + } + } + } + + composeTestRule.onNodeWithTag("reasoning_card").performClick() + composeTestRule.onNodeWithText("**Thinking**").assertIsDisplayed() + composeTestRule.onNodeWithText("Thinking").assertDoesNotExist() + } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt index 6bd00aaa..df4038d9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/MessageCards.kt @@ -89,6 +89,7 @@ import com.m57.hermescontrol.theme.CodeTerminalBorder import com.m57.hermescontrol.theme.CodeTerminalMuted import com.m57.hermescontrol.theme.CodeTerminalText import com.m57.hermescontrol.ui.chat.ClarifyUi +import com.m57.hermescontrol.ui.chat.MarkdownText import com.m57.hermescontrol.ui.chat.SubagentIndicator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -101,7 +102,8 @@ import kotlinx.coroutines.withContext * (reasoning-model thinking steps) before the final answer. * * Collapsed: "🧠 Reasoning · {N} steps" with chevron. - * Expanded: reasoning text in monospace bodySmall, capped at ~40% of the + * Expanded: completed reasoning is Markdown; streaming text stays raw, + * capped at ~40% of the * screen height with internal scroll; "Show full" lifts the cap. * Long-press anywhere: copies the reasoning trace to the clipboard. * Streaming: pulsing indicator at the bottom while [isStreaming]. @@ -180,26 +182,35 @@ fun ReasoningCard( } AnimatedVisibility(visible = expanded) { Column { - Text( - text = reasoningText, - style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = - Modifier - .padding(top = 4.dp) - .then( - if (!fullHeight) { - // Capped at ~40% of the screen with internal scroll. - Modifier.heightIn(max = capHeight).verticalScroll(scrollState) - } else { - // "Show full": render the whole trace at natural height. - // A scrollable must NEVER be measured with an infinite - // max height — inside a LazyColumn item (unbounded main - // axis) that combination throws IllegalStateException. - Modifier - }, - ), - ) + val contentModifier = + Modifier + .padding(top = 4.dp) + .then( + if (!fullHeight) { + // Capped at ~40% of the screen with internal scroll. + Modifier.heightIn(max = capHeight).verticalScroll(scrollState) + } else { + // "Show full": render the whole trace at natural height. + // A scrollable must NEVER be measured with an infinite + // max height — inside a LazyColumn item (unbounded main + // axis) that combination throws IllegalStateException. + Modifier + }, + ) + if (isStreaming) { + Text( + text = reasoningText, + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = contentModifier, + ) + } else { + MarkdownText( + text = reasoningText, + textColor = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = contentModifier, + ) + } if (isStreaming) { ReasoningPulsingDot(modifier = Modifier.padding(top = 6.dp)) }