From e89b7c442c0433ad5167e72a5a25bb6d0712766a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Thu, 15 Oct 2026 06:58:13 +0000 Subject: [PATCH] Apply syntax theme colors to Markdown prose in the TUI (#54217) ## What changed Use the active syntax theme's markup foregrounds for headings, emphasis, strong text, list markers, links, and blockquotes. Preserve existing bold, italic, and underline modifiers, and retain previous styles when a scope provides no foreground. ## Testing Update rendering snapshots and streaming blockquote assertions for themed colors. Exercise existing fallback expectations with an empty syntax theme. GitOrigin-RevId: b00aa4af30f73f538ff5f888a0f9b4edbdc15893 --- .../tui/src/app/owned_transcript_tests.rs | 4 +- .../tui/src/inline_visualization_tests.rs | 4 +- codex-rs/tui/src/markdown_render.rs | 58 ++++++++++++++----- ...ed_without_coloring_surrounding_prose.snap | 2 +- ...eep_link_style_when_wrapped_in_tables.snap | 4 +- ...y_and_fallback_presentations_snapshot.snap | 18 +++--- .../src/markdown_render/web_links_tests.rs | 23 ++++---- codex-rs/tui/src/markdown_render_tests.rs | 28 +++++++++ codex-rs/tui/src/markdown_stream.rs | 35 +++++++---- ...markers_use_terminal_palette_snapshot.snap | 2 +- ...els_have_a_visible_underline_snapshot.snap | 2 +- ...ooltips__tests__desktop_app_tip_linux.snap | 4 +- ...ooltips__tests__desktop_app_tip_macos.snap | 2 +- ...ltips__tests__desktop_app_tip_windows.snap | 4 +- ...er__tests__incremental_file_citations.snap | 12 ++-- ...remental_render_representative_stream.snap | 10 ++-- ...out_context_use_canonical_full_render.snap | 2 +- ..._shortcut_tips_render_at_narrow_width.snap | 28 ++++----- 18 files changed, 155 insertions(+), 87 deletions(-) diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index 1ca9ddc35a..a68938e517 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -188,11 +188,11 @@ async fn list_spacing_completion_preserves_the_scrolled_reader() -> Result<()> { let mut after = Buffer::empty(area); app.transcript_view .render(area, &mut after, &app.transcript_cells); - assert_eq!(after, before); + assert_eq!(buffer_text(&after), buffer_text(&before)); app.transcript_view.jump_to_latest(); app.transcript_view .render(area, &mut after, &app.transcript_cells); - assert_eq!(after, before); + assert_eq!(buffer_text(&after), buffer_text(&before)); Ok(()) } diff --git a/codex-rs/tui/src/inline_visualization_tests.rs b/codex-rs/tui/src/inline_visualization_tests.rs index 7105d7c40e..de7f18320f 100644 --- a/codex-rs/tui/src/inline_visualization_tests.rs +++ b/codex-rs/tui/src/inline_visualization_tests.rs @@ -346,7 +346,9 @@ fn finalized_agent_cell_replays_visualization_link() { .expect("visualization URL span"); assert_eq!( url_span.style, - Style::new().fg(crate::style::accent_color()).underlined() + crate::render::highlight::foreground_style_for_scopes(&["markup.underline.link"]) + .unwrap() + .underlined() ); let destinations = lines .iter() diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 8d65d0a1c8..7f09494c78 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -38,6 +38,8 @@ //! body rows, or even 3-char-wide columns cannot fit, body rows render as //! key/value records. //! +//! Markdown prose uses foregrounds from the active syntax theme's markup scopes, +//! retaining its usual modifiers and falling back when a scope has no foreground. //! Inline code and local file paths share the active syntax theme's raw-markup foreground. use crate::markdown_text_merge::DecodedTextMerge; @@ -133,13 +135,20 @@ impl Default for MarkdownStyles { impl MarkdownStyles { fn for_theme(theme: &syntect::highlighting::Theme) -> Self { + let foreground = |scopes: &[&str]| { + foreground_style_for_scopes_with_theme(theme, scopes).unwrap_or_default() + }; Self { - h1: Style::new().bold().underlined(), - h2: Style::new().bold(), - h3: Style::new().bold().italic(), - h4: Style::new().italic(), - h5: Style::new().italic(), - h6: Style::new().italic(), + h1: foreground(&["markup.heading.1.markdown", "markup.heading.markdown"]) + .bold() + .underlined(), + h2: foreground(&["markup.heading.2.markdown", "markup.heading.markdown"]).bold(), + h3: foreground(&["markup.heading.3.markdown", "markup.heading.markdown"]) + .bold() + .italic(), + h4: foreground(&["markup.heading.4.markdown", "markup.heading.markdown"]).italic(), + h5: foreground(&["markup.heading.5.markdown", "markup.heading.markdown"]).italic(), + h6: foreground(&["markup.heading.6.markdown", "markup.heading.markdown"]).italic(), code: foreground_style_for_scopes_with_theme( theme, &[ @@ -148,13 +157,23 @@ impl MarkdownStyles { ], ) .unwrap_or_else(|| Style::new().fg(accent_color())), - emphasis: Style::new().italic(), - strong: Style::new().bold(), + emphasis: foreground(&["markup.italic.markdown"]).italic(), + strong: foreground(&["markup.bold.markdown"]).bold(), strikethrough: Style::new().crossed_out(), - ordered_list_marker: Style::new().light_blue(), - unordered_list_marker: Style::new(), - link: Style::new().fg(accent_color()).underlined(), - blockquote: Style::new().green(), + ordered_list_marker: Style::new() + .light_blue() + .patch(foreground(&["markup.list.numbered.markdown"])), + unordered_list_marker: foreground(&[ + "markup.list.unnumbered.markdown", + "markup.list.bullet.markdown", + ]), + link: Style::new() + .fg(accent_color()) + .patch(foreground(&["markup.underline.link.markdown"])) + .underlined(), + blockquote: Style::new() + .green() + .patch(foreground(&["markup.quote.markdown"])), } } } @@ -2575,7 +2594,6 @@ mod markdown_render_tests { mod tests { use super::*; use pretty_assertions::assert_eq; - use ratatui::style::Stylize; use ratatui::text::Text; fn lines_to_strings(text: &Text<'_>) -> Vec { @@ -2660,9 +2678,17 @@ mod tests { assert_eq!( source.styled_range(0..source.text.len()), Line::from(vec![ - "block quote with ".green(), - "content".green().bold(), - " that should wrap nicely".green(), + Span::styled("block quote with ", MarkdownStyles::default().blockquote), + Span::styled( + "content", + MarkdownStyles::default() + .blockquote + .patch(MarkdownStyles::default().strong) + ), + Span::styled( + " that should wrap nicely", + MarkdownStyles::default().blockquote + ), ]) ); let lines: Vec<_> = rendered.iter().map(|line| line.line.to_string()).collect(); diff --git a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_are_styled_without_coloring_surrounding_prose.snap b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_are_styled_without_coloring_surrounding_prose.snap index da67cbc905..ee1efc0847 100644 --- a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_are_styled_without_coloring_surrounding_prose.snap +++ b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_are_styled_without_coloring_surrounding_prose.snap @@ -3,4 +3,4 @@ source: tui/src/markdown_render/web_links_tests.rs assertion_line: 37 expression: text --- -Text::from(Line::from_iter([Span::from("界 ("), Span::from("https://example.com/a").fg(Color::Reset).underlined(), Span::from("); "), Span::from("https://example.com/b").fg(Color::Reset).underlined(), Span::from("! "), Span::from("https://example.com/c").fg(Color::Reset).bold().underlined()])) +Text::from(Line::from_iter([Span::from("界 ("), Span::from("https://example.com/a").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from("); "), Span::from("https://example.com/b").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from("! "), Span::from("https://example.com/c").fg(Color::Rgb(137, 180, 250)).bold().underlined()])) diff --git a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_keep_link_style_when_wrapped_in_tables.snap b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_keep_link_style_when_wrapped_in_tables.snap index 7e88fd7d36..911cf60682 100644 --- a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_keep_link_style_when_wrapped_in_tables.snap +++ b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__bare_urls_keep_link_style_when_wrapped_in_tables.snap @@ -11,10 +11,10 @@ Text::from_iter([ Line::from(Span::from("━━━━━━━━━━━━━━━━━━━━").dim()), Line::from_iter([ Span::from(" "), - Span::from("https://").fg(Color::Reset).underlined(), + Span::from("https://").fg(Color::Rgb(137, 180, 250)).underlined(), ]), Line::from_iter([ Span::from(" "), - Span::from("example.com/docs").fg(Color::Reset).underlined(), + Span::from("example.com/docs").fg(Color::Rgb(137, 180, 250)).underlined(), ]), ]) diff --git a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__label_only_and_fallback_presentations_snapshot.snap b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__label_only_and_fallback_presentations_snapshot.snap index 2c6a1bce21..6dd37933fa 100644 --- a/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__label_only_and_fallback_presentations_snapshot.snap +++ b/codex-rs/tui/src/markdown_render/snapshots/codex_tui__markdown_render__web_links__tests__label_only_and_fallback_presentations_snapshot.snap @@ -6,11 +6,11 @@ expression: presentations Text::from_iter([ Line::from_iter([ Span::from("plain "), - Span::from("plain").fg(Color::Reset).underlined(), + Span::from("plain").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), Span::from(" "), - Span::from("code").fg(Color::Reset).underlined(), + Span::from("code").fg(Color::Rgb(137, 180, 250)).underlined(), ]), Line::default(), Line::from_iter([ @@ -20,23 +20,23 @@ expression: presentations Line::from(Span::from("━━━━━━━━━━").dim()), Line::from_iter([ Span::from(" "), - Span::from("docs").fg(Color::Reset).underlined(), + Span::from("docs").fg(Color::Rgb(137, 180, 250)).underlined(), ]), ]), Text::from_iter([ Line::from_iter([ Span::from("plain "), - Span::from("plain").fg(Color::Reset).underlined(), + Span::from("plain").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), - Span::from("https://example.com").fg(Color::Reset).underlined(), + Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), ]), Line::from_iter([ - Span::from("code").fg(Color::Reset).underlined(), + Span::from("code").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), - Span::from("https://example.com").fg(Color::Reset).underlined(), + Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), ]), Line::default(), @@ -47,9 +47,9 @@ expression: presentations Line::from(Span::from("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━").dim()), Line::from_iter([ Span::from(" "), - Span::from("docs").fg(Color::Reset).underlined(), + Span::from("docs").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), - Span::from("https://example.com/docs").fg(Color::Reset).underlined(), + Span::from("https://example.com/docs").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), ]), ]), diff --git a/codex-rs/tui/src/markdown_render/web_links_tests.rs b/codex-rs/tui/src/markdown_render/web_links_tests.rs index 38eba10d2e..cf8a08f929 100644 --- a/codex-rs/tui/src/markdown_render/web_links_tests.rs +++ b/codex-rs/tui/src/markdown_render/web_links_tests.rs @@ -1,4 +1,5 @@ use super::WebLinkDisplay; +use crate::markdown_render::MarkdownStyles; use crate::markdown_render::render_markdown_lines_with_width_cwd_and_hidden_link_destinations; use crate::markdown_render::render_streaming_markdown_lines_with_width_and_cwd; use crate::style::accent_color; @@ -13,6 +14,7 @@ use pretty_assertions::assert_eq; use ratatui::style::Style; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::text::Span; use ratatui::text::Text; use std::collections::BTreeSet; @@ -27,14 +29,11 @@ fn bare_urls_are_styled_without_coloring_surrounding_prose() { text, Text::from(Line::from(vec![ "界 (".into(), - "https://example.com/a".fg(accent_color()).underlined(), + Span::styled("https://example.com/a", MarkdownStyles::default().link), "); ".into(), - "https://example.com/b".fg(accent_color()).underlined(), + Span::styled("https://example.com/b", MarkdownStyles::default().link), "! ".into(), - "https://example.com/c" - .bold() - .fg(accent_color()) - .underlined(), + Span::styled("https://example.com/c", MarkdownStyles::default().link).bold(), ])), ); assert_debug_snapshot!(text); @@ -103,27 +102,27 @@ fn supporting_terminals_render_only_the_styled_label_and_keep_its_target() { for (markdown, label) in [ ( "[label](https://example.com)", - "label".fg(accent_color()).underlined(), + Span::styled("label", MarkdownStyles::default().link), ), ( "[`label`](https://example.com)", - "label".fg(accent_color()).underlined(), + Span::styled("label", MarkdownStyles::default().link), ), ( "[**label**](https://example.com)", - "label".fg(accent_color()).bold().underlined(), + Span::styled("label", MarkdownStyles::default().link).bold(), ), ( "[*label*](https://example.com)", - "label".fg(accent_color()).italic().underlined(), + Span::styled("label", MarkdownStyles::default().link).italic(), ), ( "[](https://example.com)", - "".fg(accent_color()).underlined(), + Span::styled("", MarkdownStyles::default().link), ), ( "[https://example.com](https://example.com)", - "https://example.com".fg(accent_color()).underlined(), + Span::styled("https://example.com", MarkdownStyles::default().link), ), ] { let label_width = label.width(); diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 94312ca7ec..3230dd8e36 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1,4 +1,5 @@ use super::MarkdownStyles; +use crate::render::highlight::set_syntax_theme; use crate::style::accent_color; use pretty_assertions::assert_eq; use ratatui::style::Modifier; @@ -146,6 +147,7 @@ fn paragraph_multiple() { #[test] fn headings() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "# Heading 1\n## Heading 2\n### Heading 3\n#### Heading 4\n##### Heading 5\n###### Heading 6\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -166,6 +168,7 @@ fn headings() { #[test] fn blockquote_single() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("> Blockquote"); let expected = Text::from(Line::from_iter(["> ", "Blockquote"]).green()); assert_eq!(text, expected); @@ -196,6 +199,7 @@ fn blockquote_soft_break() { #[test] fn blockquote_multiple_with_break() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("> Blockquote 1\n\n> Blockquote 2\n"); let expected = Text::from_iter([ Line::from_iter(["> ", "Blockquote 1"]).green(), @@ -207,6 +211,7 @@ fn blockquote_multiple_with_break() { #[test] fn blockquote_three_paragraphs_short_lines() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "> one\n>\n> two\n>\n> three\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -221,6 +226,7 @@ fn blockquote_three_paragraphs_short_lines() { #[test] fn blockquote_nested_two_levels() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "> Level 1\n>> Level 2\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -233,6 +239,7 @@ fn blockquote_nested_two_levels() { #[test] fn blockquote_with_list_items() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "> - item 1\n> - item 2\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -244,6 +251,7 @@ fn blockquote_with_list_items() { #[test] fn blockquote_with_ordered_list() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "> 1. first\n> 2. second\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -265,6 +273,7 @@ fn blockquote_with_ordered_list() { #[test] fn blockquote_list_then_nested_blockquote() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "> - parent\n> > child\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -470,6 +479,7 @@ fn blockquote_with_heading_and_paragraph() { #[test] fn blockquote_heading_inherits_heading_style() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("> # test header\n> in blockquote\n"); assert_eq!( text.lines, @@ -565,6 +575,7 @@ fn nested_blockquote_with_inline_and_fenced_code() { #[test] fn list_unordered_single() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("- List item 1\n"); let expected = Text::from_iter([Line::from_iter(["• ", "List item 1"])]); assert_eq!(text, expected); @@ -572,6 +583,7 @@ fn list_unordered_single() { #[test] fn list_unordered_multiple() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("- List item 1\n- List item 2\n"); let expected = Text::from_iter([ Line::from_iter(["• ", "List item 1"]), @@ -599,6 +611,7 @@ fn empty_list_items_keep_their_markers() { #[test] fn list_nested() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("- List item 1\n - Nested list item 1\n"); let expected = Text::from_iter([ Line::from_iter(["• ", "List item 1"]), @@ -627,6 +640,7 @@ fn list_ordered_custom_start() { #[test] fn nested_unordered_in_ordered() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "1. Outer\n - Inner A\n - Inner B\n2. Next\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -641,6 +655,7 @@ fn nested_unordered_in_ordered() { #[test] fn nested_ordered_in_unordered() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "- Outer\n 1. One\n 2. Two\n- Last\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -669,6 +684,7 @@ fn loose_list_item_multiple_paragraphs() { #[test] fn tight_item_with_soft_break() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "- item line1\n item line2\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -680,6 +696,7 @@ fn tight_item_with_soft_break() { #[test] fn deeply_nested_mixed_three_levels() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "1. A\n - B\n 1. C\n2. D\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -802,6 +819,7 @@ fn inline_code_and_file_paths_follow_syntax_theme() { #[test] fn strong() { + set_syntax_theme(syntect::highlighting::Theme::default()); assert_eq!( render_markdown_text("**Strong**"), Text::from(Line::from("Strong".bold())) @@ -810,6 +828,7 @@ fn strong() { #[test] fn emphasis() { + set_syntax_theme(syntect::highlighting::Theme::default()); assert_eq!( render_markdown_text("*Emphasis*"), Text::from(Line::from("Emphasis".italic())) @@ -826,6 +845,7 @@ fn strikethrough() { #[test] fn strong_emphasis() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("**Strong *emphasis***"); let expected = Text::from(Line::from_iter([ "Strong ".bold(), @@ -836,6 +856,7 @@ fn strong_emphasis() { #[test] fn link() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("[Link](https://example.com)"); let expected = Text::from(Line::from_iter([ "Link".fg(accent_color()).underlined(), @@ -848,6 +869,7 @@ fn link() { #[test] fn web_link_labels_use_link_style_and_preserve_inline_formatting() { + set_syntax_theme(syntect::highlighting::Theme::default()); for (label, expected_label) in [ ("plain", "plain".fg(accent_color()).underlined()), ("`code`", "code".fg(accent_color()).underlined()), @@ -875,6 +897,7 @@ fn web_link_labels_use_link_style_and_preserve_inline_formatting() { #[test] fn web_link_labels_keep_link_style_in_wrapped_prose_and_tables() { + set_syntax_theme(syntect::highlighting::Theme::default()); let links = "[plain](https://example.com) [`code`](https://example.com) [](https://example.com)"; for markdown in [ @@ -1210,6 +1233,7 @@ fn file_link_appends_hash_range_when_label_lacks_it() { #[test] fn multiline_file_link_label_after_styled_prefix_does_not_panic() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text_for_cwd( "**bold** plain [foo\nbar](file:///Users/example/code/codex/codex-rs/tui/src/markdown_render.rs#L74C3)", Path::new("/Users/example/code/codex"), @@ -1250,6 +1274,7 @@ fn file_link_uses_target_path_for_hash_range() { #[test] fn url_link_shows_destination() { + set_syntax_theme(syntect::highlighting::Theme::default()); let text = render_markdown_text("[docs](https://example.com/docs)"); let expected = Text::from(Line::from_iter([ "docs".fg(accent_color()).underlined(), @@ -1739,6 +1764,7 @@ fn ordered_item_with_code_block_and_nested_bullet() { #[test] fn nested_five_levels_mixed_lists() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "1. First\n - Second level\n 1. Third level (ordered)\n - Fourth level (bullet)\n - Fifth level to test indent consistency\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -1787,6 +1813,7 @@ fn html_in_tight_ordered_item_soft_breaks_with_space() { #[test] fn html_continuation_paragraph_in_unordered_item_indented() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "- Item\n\n continued\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ @@ -1836,6 +1863,7 @@ fn ordered_item_continuation_paragraph_is_indented() { #[test] fn nested_item_continuation_paragraph_is_indented() { + set_syntax_theme(syntect::highlighting::Theme::default()); let md = "1. A\n - B\n\n Continuation for B\n2. C\n"; let text = render_markdown_text(md); let expected = Text::from_iter([ diff --git a/codex-rs/tui/src/markdown_stream.rs b/codex-rs/tui/src/markdown_stream.rs index 1a52c958bb..d2bcf53ce5 100644 --- a/codex-rs/tui/src/markdown_stream.rs +++ b/codex-rs/tui/src/markdown_stream.rs @@ -225,6 +225,7 @@ pub(crate) fn simulate_stream_markdown_for_tests( #[cfg(test)] mod tests { use super::*; + use crate::render::highlight::foreground_style_for_scopes; use ratatui::style::Color; #[tokio::test] @@ -247,20 +248,20 @@ mod tests { } #[tokio::test] - async fn e2e_stream_blockquote_simple_is_green() { + async fn e2e_stream_blockquote_simple_follows_theme() { let out = super::simulate_stream_markdown_for_tests(&["> Hello\n"], /*finalize*/ true); assert_eq!(out.len(), 1); let l = &out[0]; assert_eq!( l.style.fg, - Some(Color::Green), - "expected blockquote line fg green, got {:?}", + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg, + "expected themed blockquote foreground, got {:?}", l.style.fg ); } #[tokio::test] - async fn e2e_stream_blockquote_nested_is_green() { + async fn e2e_stream_blockquote_nested_follows_theme() { let out = super::simulate_stream_markdown_for_tests( &["> Level 1\n>> Level 2\n"], /*finalize*/ true, @@ -281,19 +282,31 @@ mod tests { }) .collect(); assert_eq!(non_blank.len(), 2); - assert_eq!(non_blank[0].style.fg, Some(Color::Green)); - assert_eq!(non_blank[1].style.fg, Some(Color::Green)); + assert_eq!( + non_blank[0].style.fg, + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg + ); + assert_eq!( + non_blank[1].style.fg, + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg + ); } #[tokio::test] - async fn e2e_stream_blockquote_with_list_items_is_green() { + async fn e2e_stream_blockquote_with_list_items_follows_theme() { let out = super::simulate_stream_markdown_for_tests( &["> - item 1\n> - item 2\n"], /*finalize*/ true, ); assert_eq!(out.len(), 2); - assert_eq!(out[0].style.fg, Some(Color::Green)); - assert_eq!(out[1].style.fg, Some(Color::Green)); + assert_eq!( + out[0].style.fg, + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg + ); + assert_eq!( + out[1].style.fg, + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg + ); } #[tokio::test] @@ -328,7 +341,7 @@ mod tests { } #[tokio::test] - async fn e2e_stream_blockquote_wrap_preserves_green_style() { + async fn e2e_stream_blockquote_wrap_preserves_theme_style() { let long = "> This is a very long quoted line that should wrap across multiple columns to verify style preservation."; let out = super::simulate_stream_markdown_for_tests(&[long, "\n"], /*finalize*/ true); // Wrap to a narrow width to force multiple output lines. @@ -356,7 +369,7 @@ mod tests { for (i, l) in non_blank.iter().enumerate() { assert_eq!( l.spans[0].style.fg, - Some(Color::Green), + foreground_style_for_scopes(&["markup.quote"]).unwrap().fg, "wrapped line {} should preserve green style, got {:?}", i, l.spans[0].style.fg diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__ordered_list_markers_use_terminal_palette_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__ordered_list_markers_use_terminal_palette_snapshot.snap index 1661b027e8..9635df8b75 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__ordered_list_markers_use_terminal_palette_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__ordered_list_markers_use_terminal_palette_snapshot.snap @@ -3,4 +3,4 @@ source: tui/src/markdown_render_tests.rs assertion_line: 608 expression: text --- -Text::from(Line::from_iter([Span::from("1. ").light_blue(), Span::from("plain "), Span::from("plain").fg(Color::Reset).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Reset).underlined(), Span::from(")"), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), Span::from(" "), Span::from("code").fg(Color::Reset).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Reset).underlined(), Span::from(")")])) +Text::from(Line::from_iter([Span::from("1. ").light_blue(), Span::from("plain "), Span::from("plain").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), Span::from(" "), Span::from("code").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")")])) diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__web_link_labels_have_a_visible_underline_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__web_link_labels_have_a_visible_underline_snapshot.snap index a0e0732aee..a385a8c188 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__web_link_labels_have_a_visible_underline_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__web_link_labels_have_a_visible_underline_snapshot.snap @@ -2,4 +2,4 @@ source: tui/src/markdown_render_tests.rs expression: text --- -Text::from(Line::from_iter([Span::from("plain "), Span::from("plain").fg(Color::Reset).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Reset).underlined(), Span::from(")"), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), Span::from(" "), Span::from("code").fg(Color::Reset).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Reset).underlined(), Span::from(")")])) +Text::from(Line::from_iter([Span::from("plain "), Span::from("plain").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), Span::from(" "), Span::from("code").fg(Color::Rgb(166, 227, 161)), Span::from(" "), Span::from("code").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(" ("), Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")")])) diff --git a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_linux.snap b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_linux.snap index 2a904cd13d..77283c5ec8 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_linux.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_linux.snap @@ -2,7 +2,7 @@ source: tui/src/tooltips.rs expression: rendered --- -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Use the "), Span::from("desktop app").bold(), Span::from(". Install it")]) [] +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Use the "), Span::from("desktop app").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(". Install it")]) [] Line::from("from") [] -Line::from(Span::from("https://learn.chatgpt.com/docs/linux/linux-app").fg(Color::Reset).underlined()) [TerminalHyperlink { columns: 0..46, destination: "https://learn.chatgpt.com/docs/linux/linux-app", destination_kind: Web }] +Line::from(Span::from("https://learn.chatgpt.com/docs/linux/linux-app").fg(Color::Rgb(137, 180, 250)).underlined()) [TerminalHyperlink { columns: 0..46, destination: "https://learn.chatgpt.com/docs/linux/linux-app", destination_kind: Web }] Line::from_iter([Span::from("and run "), Span::from("chatgpt").fg(Color::Rgb(166, 227, 161)), Span::from(".")]) [] diff --git a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_macos.snap b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_macos.snap index 2a5ac3cff4..f5e13b5c2f 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_macos.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_macos.snap @@ -2,6 +2,6 @@ source: tui/src/tooltips.rs expression: rendered --- -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Use the "), Span::from("desktop app").bold(), Span::from(". Run "), Span::from("codex app").fg(Color::Rgb(166, 227, 161))]) [] +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Use the "), Span::from("desktop app").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(". Run "), Span::from("codex app").fg(Color::Rgb(166, 227, 161))]) [] Line::from("to open it. It installs automatically if") [] Line::from("needed.") [] diff --git a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_windows.snap b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_windows.snap index ed749405d8..629d1bc236 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_windows.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__desktop_app_tip_windows.snap @@ -2,7 +2,7 @@ source: tui/src/tooltips.rs expression: rendered --- -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Use the "), Span::from("desktop app").bold(), Span::from(". Install it")]) [] +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Use the "), Span::from("desktop app").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(". Install it")]) [] Line::from("from") [] -Line::from(Span::from("https://chatgpt.com/codex?app-landing-page=true").fg(Color::Reset).underlined()) [TerminalHyperlink { columns: 0..47, destination: "https://chatgpt.com/codex?app-landing-page=true", destination_kind: Web }] +Line::from(Span::from("https://chatgpt.com/codex?app-landing-page=true").fg(Color::Rgb(137, 180, 250)).underlined()) [TerminalHyperlink { columns: 0..47, destination: "https://chatgpt.com/codex?app-landing-page=true", destination_kind: Web }] Line::from_iter([Span::from("and run "), Span::from("codex app").fg(Color::Rgb(166, 227, 161)), Span::from(".")]) [] diff --git a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_file_citations.snap b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_file_citations.snap index 784f1b1f77..8b48d83117 100644 --- a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_file_citations.snap +++ b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_file_citations.snap @@ -6,8 +6,8 @@ expression: rendered_cases [ HyperlinkLine { line: Line::from_iter([ - Span::from("# ").bold().underlined(), - Span::from("Output").bold().underlined(), + Span::from("# ").fg(Color::Rgb(243, 139, 168)).bold().underlined(), + Span::from("Output").fg(Color::Rgb(243, 139, 168)).bold().underlined(), ]), hyperlinks: [], }, @@ -17,7 +17,7 @@ expression: rendered_cases }, HyperlinkLine { line: Line::from_iter([ - Span::from("• "), + Span::from("• ").fg(Color::Rgb(148, 226, 213)), Span::from("Quarterly Report.xlsx").fg(Color::Rgb(166, 227, 161)), ]), hyperlinks: [], @@ -34,8 +34,8 @@ expression: rendered_cases [ HyperlinkLine { line: Line::from_iter([ - Span::from("# ").bold().underlined(), - Span::from("Output").bold().underlined(), + Span::from("# ").fg(Color::Rgb(243, 139, 168)).bold().underlined(), + Span::from("Output").fg(Color::Rgb(243, 139, 168)).bold().underlined(), ]), hyperlinks: [], }, @@ -45,7 +45,7 @@ expression: rendered_cases }, HyperlinkLine { line: Line::from_iter([ - Span::from("• "), + Span::from("• ").fg(Color::Rgb(148, 226, 213)), Span::from("Résumé *final* ✨.xlsx").fg(Color::Rgb(166, 227, 161)), ]), hyperlinks: [], diff --git a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_render_representative_stream.snap b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_render_representative_stream.snap index b95cd9374e..47a962de5d 100644 --- a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_render_representative_stream.snap +++ b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__incremental_render_representative_stream.snap @@ -6,8 +6,8 @@ expression: render.lines [ HyperlinkLine { line: Line::from_iter([ - Span::from("# ").bold().underlined(), - Span::from("Heading").bold().underlined(), + Span::from("# ").fg(Color::Rgb(243, 139, 168)).bold().underlined(), + Span::from("Heading").fg(Color::Rgb(243, 139, 168)).bold().underlined(), ]), hyperlinks: [], }, @@ -18,7 +18,7 @@ expression: render.lines HyperlinkLine { line: Line::from_iter([ Span::from("First paragraph with a "), - Span::from("link").fg(Color::Rgb(99, 168, 248)).underlined(), + Span::from("link").fg(Color::Rgb(137, 180, 250)).underlined(), ]), hyperlinks: [ TerminalHyperlink { @@ -31,7 +31,7 @@ expression: render.lines HyperlinkLine { line: Line::from_iter([ Span::from("("), - Span::from("https://example.com").fg(Color::Rgb(99, 168, 248)).underlined(), + Span::from("https://example.com").fg(Color::Rgb(137, 180, 250)).underlined(), Span::from(")"), Span::from("."), ]), @@ -73,7 +73,7 @@ expression: render.lines line: Line::from_iter([ Span::from("> "), Span::from("Quoted paragraph"), - ]).green(), + ]).fg(Color::Rgb(245, 194, 231)), hyperlinks: [], }, HyperlinkLine { diff --git a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__inline_visualizations_without_context_use_canonical_full_render.snap b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__inline_visualizations_without_context_use_canonical_full_render.snap index 687e703b40..b719e64ee7 100644 --- a/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__inline_visualizations_without_context_use_canonical_full_render.snap +++ b/codex-rs/tui/src/streaming/snapshots/codex_tui__streaming__render__tests__inline_visualizations_without_context_use_canonical_full_render.snap @@ -12,7 +12,7 @@ expression: render.lines hyperlinks: [], }, HyperlinkLine { - line: Line::from(Span::from("Visualization unavailable on this device.").italic()), + line: Line::from(Span::from("Visualization unavailable on this device.").fg(Color::Rgb(243, 139, 168)).italic()), hyperlinks: [], }, ] diff --git a/codex-rs/tui/src/tooltips/snapshots/codex_tui__tooltips__keybinding_tests__configured_shortcut_tips_render_at_narrow_width.snap b/codex-rs/tui/src/tooltips/snapshots/codex_tui__tooltips__keybinding_tests__configured_shortcut_tips_render_at_narrow_width.snap index d91e302745..3862c8feef 100644 --- a/codex-rs/tui/src/tooltips/snapshots/codex_tui__tooltips__keybinding_tests__configured_shortcut_tips_render_at_narrow_width.snap +++ b/codex-rs/tui/src/tooltips/snapshots/codex_tui__tooltips__keybinding_tests__configured_shortcut_tips_render_at_narrow_width.snap @@ -3,50 +3,50 @@ source: tui/src/tooltips/keybinding_tests.rs expression: "lines.iter().map(|line| format!(\"{line:?}\")).collect::>().join(\"\\n\")" --- Line::from("Default") -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("tab").fg(Color::Rgb(166, 227, 161)), Span::from(" to queue a message when a")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("tab").fg(Color::Rgb(166, 227, 161)), Span::from(" to queue a message when a")]) Line::from("task is running; otherwise it sends") Line::from_iter([Span::from("immediately (except "), Span::from("!").fg(Color::Rgb(166, 227, 161)), Span::from(").")]) Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Use "), Span::from("/copy").bold(), Span::from(" or press "), Span::from("⌃o").fg(Color::Rgb(166, 227, 161)), Span::from(" to copy the")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Use "), Span::from("/copy").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" or press "), Span::from("⌃o").fg(Color::Rgb(166, 227, 161)), Span::from(" to copy the")]) Line::from("latest agent response as Markdown.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃t").fg(Color::Rgb(166, 227, 161)), Span::from(" to open the full")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃t").fg(Color::Rgb(166, 227, 161)), Span::from(" to open the full")]) Line::from("transcript.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("f3").fg(Color::Rgb(166, 227, 161)), Span::from(" to search this")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("f3").fg(Color::Rgb(166, 227, 161)), Span::from(" to search this")]) Line::from("conversation.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃g").fg(Color::Rgb(166, 227, 161)), Span::from(" to edit your current draft")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃g").fg(Color::Rgb(166, 227, 161)), Span::from(" to edit your current draft")]) Line::from("in an external editor.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃r").fg(Color::Rgb(166, 227, 161)), Span::from(" to search previously")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃r").fg(Color::Rgb(166, 227, 161)), Span::from(" to search previously")]) Line::from("entered prompts.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" For models with adjustable")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" For models with adjustable")]) Line::from_iter([Span::from("reasoning, press "), Span::from("⌥.").fg(Color::Rgb(166, 227, 161)), Span::from(" to increase")]) Line::from_iter([Span::from("reasoning effort or "), Span::from("⌥,").fg(Color::Rgb(166, 227, 161)), Span::from(" to decrease it.")]) Line::default() Line::from("Remapped") -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃x q").fg(Color::Rgb(166, 227, 161)), Span::from(" to queue a message when")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃x q").fg(Color::Rgb(166, 227, 161)), Span::from(" to queue a message when")]) Line::from("a task is running; otherwise it sends") Line::from_iter([Span::from("immediately (except "), Span::from("!").fg(Color::Rgb(166, 227, 161)), Span::from(").")]) Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Use "), Span::from("/copy").bold(), Span::from(" or press "), Span::from("⌃x `").fg(Color::Rgb(166, 227, 161)), Span::from(" to copy the")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Use "), Span::from("/copy").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" or press "), Span::from("⌃x `").fg(Color::Rgb(166, 227, 161)), Span::from(" to copy the")]) Line::from("latest agent response as Markdown.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃x t").fg(Color::Rgb(166, 227, 161)), Span::from(" to open the full")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃x t").fg(Color::Rgb(166, 227, 161)), Span::from(" to open the full")]) Line::from("transcript.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃x f").fg(Color::Rgb(166, 227, 161)), Span::from(" to search this")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃x f").fg(Color::Rgb(166, 227, 161)), Span::from(" to search this")]) Line::from("conversation.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃x e").fg(Color::Rgb(166, 227, 161)), Span::from(" to edit your current")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃x e").fg(Color::Rgb(166, 227, 161)), Span::from(" to edit your current")]) Line::from("draft in an external editor.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" Press "), Span::from("⌃x r").fg(Color::Rgb(166, 227, 161)), Span::from(" to search previously")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" Press "), Span::from("⌃x r").fg(Color::Rgb(166, 227, 161)), Span::from(" to search previously")]) Line::from("entered prompts.") Line::default() -Line::from_iter([Span::from("Tip:").bold(), Span::from(" For models with adjustable")]) +Line::from_iter([Span::from("Tip:").fg(Color::Rgb(243, 139, 168)).bold(), Span::from(" For models with adjustable")]) Line::from_iter([Span::from("reasoning, press "), Span::from("⌃x +").fg(Color::Rgb(166, 227, 161)), Span::from(" to increase")]) Line::from_iter([Span::from("reasoning effort or "), Span::from("⌃x -").fg(Color::Rgb(166, 227, 161)), Span::from(" to decrease it.")]) Line::default()