Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion src/tui/app/state_ui_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,33 @@ fn display_message_from_stored_message(
}

fn stored_message_visible_text(message: &crate::session::StoredMessage) -> String {
// Issue #98: when `display.show_thinking = false`, MiniMax (and any other
// provider that emits `reasoning_content`) was still surfacing the
// thinking text here because we lumped Reasoning blocks in with normal
// assistant text. Respect the setting at the rendering boundary.
let show_thinking = crate::config::config().display.show_thinking;
stored_message_visible_text_with_show_thinking(message, show_thinking)
}

/// Pure helper for `stored_message_visible_text` so tests can pin the
/// `show_thinking` decision without mutating the global config.
pub(super) fn stored_message_visible_text_with_show_thinking(
message: &crate::session::StoredMessage,
show_thinking: bool,
) -> String {
let mut parts = Vec::new();
for block in &message.content {
match block {
ContentBlock::Text { text, .. } | ContentBlock::Reasoning { text } => {
ContentBlock::Text { text, .. } => {
if !text.trim().is_empty() {
parts.push(text.trim().to_string());
}
}
ContentBlock::Reasoning { text } => {
if show_thinking && !text.trim().is_empty() {
parts.push(text.trim().to_string());
}
}
ContentBlock::ToolUse { name, input, .. } => {
parts.push(format!("[tool:{} {}]", name, input));
}
Expand Down Expand Up @@ -515,3 +534,83 @@ fn parse_leading_usize(text: &str) -> Option<(usize, &str)> {
let value = text[..end].parse().ok()?;
Some((value, &text[end..]))
}

#[cfg(test)]
mod show_thinking_tests {
use super::*;
use crate::message::ContentBlock;
use crate::session::StoredMessage;
use jcode_message_types::Role;

fn assistant_with_blocks(blocks: Vec<ContentBlock>) -> StoredMessage {
StoredMessage {
id: "test".to_string(),
role: Role::Assistant,
content: blocks,
display_role: None,
timestamp: None,
tool_duration_ms: None,
token_usage: None,
}
}

#[test]
fn reasoning_block_hidden_when_show_thinking_is_false() {
// Regression for issue #98: MiniMax-style reasoning_content was
// leaking into the rendered transcript when display.show_thinking was
// false because Reasoning blocks were being lumped with Text blocks.
let msg = assistant_with_blocks(vec![
ContentBlock::Reasoning {
text: "internal thoughts the user explicitly chose not to see".to_string(),
},
ContentBlock::Text {
text: "Final answer for the user.".to_string(),
cache_control: None,
},
]);

let visible = stored_message_visible_text_with_show_thinking(&msg, false);
assert_eq!(visible, "Final answer for the user.");
assert!(
!visible.contains("internal thoughts"),
"show_thinking=false must suppress Reasoning content; got {visible:?}"
);
}

#[test]
fn reasoning_block_shown_when_show_thinking_is_true() {
let msg = assistant_with_blocks(vec![
ContentBlock::Reasoning {
text: "thinking step 1".to_string(),
},
ContentBlock::Text {
text: "answer".to_string(),
cache_control: None,
},
]);

let visible = stored_message_visible_text_with_show_thinking(&msg, true);
assert!(
visible.contains("thinking step 1"),
"show_thinking=true must include reasoning; got {visible:?}"
);
assert!(visible.contains("answer"));
}

#[test]
fn empty_reasoning_block_is_skipped_either_way() {
let msg = assistant_with_blocks(vec![
ContentBlock::Reasoning {
text: " \n ".to_string(),
},
ContentBlock::Text {
text: "real".to_string(),
cache_control: None,
},
]);
for show in [true, false] {
let visible = stored_message_visible_text_with_show_thinking(&msg, show);
assert_eq!(visible, "real");
}
}
}