Skip to content
Merged
Show file tree
Hide file tree
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
Add independent TUI rendering toggles for Mermaid, math, and tables (#…
…46938)

## What changed

Add `tui.rendering.mermaid`, `tui.rendering.math`, and `tui.rendering.tables`, all enabled by default and independent of animation settings. Disable individual renderers to display their source instead of diagrams, Unicode math, or formatted tables.

Apply preferences to streaming and completed responses, startup previews, and session changes. When table rendering is disabled, preserve pipe syntax, cell markup, and enclosing Markdown fences. Keep HTML clipboard formatting independent of terminal rendering preferences.

## Testing

Add coverage for individual defaults, source preservation, nested table fences, session settings, and clipboard behavior. Verify incremental rendering and emitted output match completed rendering across all eight toggle combinations.

GitOrigin-RevId: 48d765eac20a82881eed94f186bf5b025ff4f0db
  • Loading branch information
etraut-openai authored and copyberry committed Sep 21, 2026
commit a86631502d49274cb47208925c7d3dcece032029
1 change: 1 addition & 0 deletions codex-rs/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub mod test_support;
mod thread_config;
mod tui_effects;
mod tui_keymap;
mod tui_rendering;
pub mod types;

pub const CONFIG_TOML_FILE: &str = "config.toml";
Expand Down
28 changes: 28 additions & 0 deletions codex-rs/config/src/tui_rendering.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! TUI content rendering preferences, independent of animation effects.

use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;

/// Optional rich renderers. Disabled renderers preserve the original source.
#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, JsonSchema)]
#[serde(default)]
#[schemars(deny_unknown_fields)]
pub struct TuiRendering {
/// Render Mermaid code blocks as diagrams.
pub mermaid: bool,
/// Render math expressions using Unicode notation.
pub math: bool,
/// Render pipe tables, including tables inside Markdown fences.
pub tables: bool,
}

impl Default for TuiRendering {
fn default() -> Self {
Self {
mermaid: true,
math: true,
tables: true,
}
}
}
5 changes: 5 additions & 0 deletions codex-rs/config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub use crate::tui_keymap::TuiPagerKeymap;
pub use crate::tui_keymap::TuiVimNormalKeymap;
pub use crate::tui_keymap::TuiVimOperatorKeymap;
pub use crate::tui_keymap::TuiVimSearchKeymap;
pub use crate::tui_rendering::TuiRendering;

pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
pub const DEFAULT_MEMORIES_MAX_ROLLOUTS_PER_STARTUP: usize = 2;
Expand Down Expand Up @@ -758,6 +759,10 @@ pub struct Tui {
#[serde(default)]
pub effects: TuiEffects,

/// Rich content rendering. Independent of animations and visual effects.
#[serde(default)]
pub rendering: TuiRendering,

/// Show startup tooltips in the TUI welcome screen.
/// Defaults to `true`.
#[serde(default = "default_true")]
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/config/src/types_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,19 @@ fn memories_version_selects_pipeline_without_changing_other_defaults() {
}
assert!(toml::from_str::<MemoriesToml>("version = \"v3\"").is_err());
}

#[test]
fn rendering_preferences_default_individually_and_ignore_animation_switch() {
for key in ["mermaid", "math", "tables"] {
let tui: Tui =
toml::from_str(&format!("animations = false\n[rendering]\n{key} = false\n")).unwrap();
assert_eq!(
tui.rendering,
TuiRendering {
mermaid: key != "mermaid",
math: key != "math",
tables: key != "tables",
}
);
}
}
35 changes: 35 additions & 0 deletions codex-rs/core/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4479,6 +4479,19 @@
"description": "Start the TUI in raw scrollback mode for copy-friendly transcript output. Defaults to `false`.",
"type": "boolean"
},
"rendering": {
"allOf": [
{
"$ref": "#/definitions/TuiRendering"
}
],
"default": {
"math": true,
"mermaid": true,
"tables": true
},
"description": "Rich content rendering. Independent of animations and visual effects."
},
"resume_cwd": {
"allOf": [
{
Expand Down Expand Up @@ -5612,6 +5625,28 @@
}
]
},
"TuiRendering": {
"additionalProperties": false,
"description": "Optional rich renderers. Disabled renderers preserve the original source.",
"properties": {
"math": {
"default": true,
"description": "Render math expressions using Unicode notation.",
"type": "boolean"
},
"mermaid": {
"default": true,
"description": "Render Mermaid code blocks as diagrams.",
"type": "boolean"
},
"tables": {
"default": true,
"description": "Render pipe tables, including tables inside Markdown fences.",
"type": "boolean"
}
},
"type": "object"
},
"TuiVimNormalKeymap": {
"additionalProperties": false,
"description": "Vim normal-mode keybindings for modal editing inside text areas.\n\nActions that use uppercase letters (like `A` for append-line-end) should be specified as `shift-a` in config; the runtime matcher handles cross-terminal shift-reporting differences automatically.",
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/core/src/config/config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1254,6 +1254,7 @@ fn config_toml_deserializes_model_availability_nux() {
animations: true,
screen_reader_detection_done: None,
effects: Default::default(),
rendering: Default::default(),
show_tooltips: true,
show_server_version_notice: true,
auto_recap: true,
Expand Down Expand Up @@ -4382,6 +4383,7 @@ fn tui_config_missing_notifications_field_defaults_to_enabled() {
animations: true,
screen_reader_detection_done: None,
effects: Default::default(),
rendering: Default::default(),
show_tooltips: true,
show_server_version_notice: true,
auto_recap: true,
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,9 @@ pub struct Config {
/// Individual TUI effects, subordinate to the animation master switch.
pub tui_effects: codex_config::types::TuiEffects,

/// Rich content rendering preferences, independent of animations.
pub tui_rendering: codex_config::types::TuiRendering,

/// Show startup tooltips in the TUI welcome screen.
pub show_tooltips: bool,

Expand Down Expand Up @@ -4386,6 +4389,7 @@ impl Config {
.unwrap_or_default(),
animations: cfg.tui.as_ref().map(|t| t.animations).unwrap_or(true),
tui_effects: cfg.tui.as_ref().map(|t| t.effects).unwrap_or_default(),
tui_rendering: cfg.tui.as_ref().map(|t| t.rendering).unwrap_or_default(),
show_tooltips: cfg.tui.as_ref().map(|t| t.show_tooltips).unwrap_or(true),
tui_show_server_version_notice: cfg
.tui
Expand Down
1 change: 1 addition & 0 deletions codex-rs/thread-manager-sample/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ fn new_config(model: Option<String>, arg0_paths: Arg0DispatchPaths) -> anyhow::R
tui_notifications: TuiNotificationSettings::default(),
animations: true,
tui_effects: Default::default(),
tui_rendering: Default::default(),
show_tooltips: true,
tui_show_server_version_notice: true,
tui_auto_recap: true,
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app/agents_overview_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use std::collections::HashMap;
const PREVIEW_CHARS: usize = 512;

pub(super) fn preview_agent_message(text: &str) -> String {
preview_markdown(&crate::markdown::unwrap_markdown_fences(text))
preview_markdown(&crate::markdown::normalize_markdown_for_rendering(text))
}

pub(super) fn preview_markdown(text: &str) -> String {
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app/agents_overview_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ impl AgentsOverviewView {
if let Some((message, cwd)) = &row.details.last_message {
details.extend([Line::default(), "Last message".dim().into()]);
crate::markdown::append_markdown(
&crate::markdown::unwrap_markdown_fences(message),
&crate::markdown::normalize_markdown_for_rendering(message),
Some(width),
Some(cwd.as_path()),
&mut details,
Expand Down
8 changes: 8 additions & 0 deletions codex-rs/tui/src/app/config_persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,8 @@ theme = "dracula"
let mut tui = crate::tui::test_support::make_test_tui()?;
app.sync_tui_theme_selection("dracula".to_string());
app.chat_widget.requires_openai_auth = false;
crate::markdown_render::preferences::init(Default::default());
app.local_settings.tui.rendering.math = false;
let mut legacy_config = app.config.clone();
legacy_config.tui_theme = Some("nord".to_string());
legacy_config.model_provider.requires_openai_auth = true;
Expand All @@ -1896,6 +1898,12 @@ theme = "dracula"
let replacement = ChatWidget::new_with_app_event(init);
assert_eq!(replacement.local_settings, app.local_settings);
assert!(!replacement.requires_openai_auth);
app.replace_chat_widget(replacement);
let source = r"Math: \(x^2\)";
assert_eq!(
crate::markdown_render::render_markdown_text(source).to_string(),
source
);
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/app/session_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ impl App {
);
}
chat_widget.restore_kill_buffer_snapshot(self.chat_widget.take_kill_buffer_snapshot());
crate::markdown_render::preferences::init(chat_widget.local_settings.tui.rendering);
self.chat_widget = chat_widget;
self.sync_active_agent_label();
}
Expand Down
16 changes: 11 additions & 5 deletions codex-rs/tui/src/clipboard_html_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,16 @@ fn markdown_clipboard_escapes_raw_html_and_rejects_unsafe_destinations() {
}

#[test]
fn markdown_clipboard_unwraps_table_fences_like_the_tui() {
fn markdown_clipboard_unwraps_table_fences_independent_of_tui_preferences() {
let table = "| Item | Result |\n| --- | --- |\n| Copy | Formatted |\n";
assert_eq!(
render_markdown(&format!("```markdown\n{table}```")),
render_markdown(table)
);
for tables in [true, false] {
crate::markdown_render::preferences::init(codex_config::types::TuiRendering {
tables,
..Default::default()
});
assert_eq!(
render_markdown(&format!("```markdown\n{table}```")),
render_markdown(table)
);
}
}
2 changes: 2 additions & 0 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,7 @@ async fn run_ratatui_app(
})
};

crate::markdown_render::preferences::init(config.tui_rendering);
// Startup pickers need the current theme before selection can reload config.
// Leave the one-time override initialization below to use the final config.
if (cli.resume_picker || cli.fork_picker)
Expand Down Expand Up @@ -1869,6 +1870,7 @@ async fn run_ratatui_app(
}

let local_settings = crate::local_settings::LocalSettings::for_tui(&config, &tui);
crate::markdown_render::preferences::init(local_settings.tui.rendering);
// Configure syntax highlighting theme from the final config — onboarding
// and resume/fork can both reload config with a different tui_theme, so
// this must happen after the last possible reload.
Expand Down
1 change: 1 addition & 0 deletions codex-rs/tui/src/local_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ impl LocalSettings {
animations: animations && system_motion == crate::motion::MotionMode::Animated,
screen_reader_detection_done: None,
effects: config.tui_effects,
rendering: config.tui_rendering,
show_tooltips: config.show_tooltips,
show_server_version_notice: config.tui_show_server_version_notice,
auto_recap: config.tui_auto_recap,
Expand Down
9 changes: 9 additions & 0 deletions codex-rs/tui/src/local_settings_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ terminal_resize_reflow_max_rows = 0
session_picker_view = "comfortable"
[tui.effects]
shimmer = false
[tui.rendering]
mermaid = false
math = false
tables = false
[history]
persistence = "none"
max_bytes = 4096
Expand Down Expand Up @@ -138,6 +142,11 @@ fast_default_opt_out = true
if !config_text.is_empty() {
expected.animations = false;
expected.effects.shimmer = false;
expected.rendering = codex_config::types::TuiRendering {
mermaid: false,
math: false,
tables: false,
};
expected.show_tooltips = false;
expected.show_server_version_notice = false;
expected.auto_recap = false;
Expand Down
21 changes: 15 additions & 6 deletions codex-rs/tui/src/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
//! - [`append_markdown`] -- general-purpose, used for plan blocks and history
//! cells that already hold pre-processed markdown (no fence unwrapping).
//! - [`append_markdown_agent`] -- for agent responses. Runs
//! [`unwrap_markdown_fences`] first so that `` ```md ``/`` ```markdown ``
//! [`normalize_markdown_for_rendering`] first so that `` ```md ``/`` ```markdown ``
//! fences containing tables are stripped and `pulldown-cmark` sees raw
//! table syntax instead of fenced code.
//! table syntax instead of fenced code, when `tui.rendering.tables` is enabled.
//!
//! ## Why fence unwrapping exists
//!
Expand Down Expand Up @@ -65,7 +65,7 @@ pub(crate) fn append_markdown_agent(
width: Option<usize>,
lines: &mut Vec<Line<'static>>,
) {
let normalized = unwrap_markdown_fences(markdown_source);
let normalized = normalize_markdown_for_rendering(markdown_source);
let rendered = crate::markdown_render::render_markdown_text_with_width_and_cwd(
&normalized,
width,
Expand Down Expand Up @@ -97,7 +97,7 @@ pub(crate) fn render_markdown_agent_with_list_spacing(
list_spacing: ListSpacing,
) -> Vec<HyperlinkLine> {
let rewritten = rewrite_inline_visualizations(markdown_source, inline_visualization_context);
let normalized = unwrap_markdown_fences(&rewritten.markdown);
let normalized = normalize_markdown_for_rendering(&rewritten.markdown);
let is_hidden_link_destination = |destination: &str| {
rewritten.trusted_file_links.contains_key(destination)
|| crate::markdown_render::hide_web_link_destination(destination)
Expand Down Expand Up @@ -129,7 +129,7 @@ pub(crate) fn render_streaming_markdown_agent_with_links_and_cwd(
cwd: Option<&Path>,
list_spacing: ListSpacing,
) -> crate::markdown_render::StreamingMarkdownRender {
let normalized = unwrap_markdown_fences(markdown_source);
let normalized = normalize_markdown_for_rendering(markdown_source);
let mut rendered = crate::markdown_render::render_streaming_markdown_lines_with_width_and_cwd(
&normalized,
width,
Expand All @@ -150,7 +150,7 @@ pub(crate) fn render_streaming_markdown_agent_with_links_and_cwd(
.last_top_level_block_start
.and_then(|boundary| markdown_source.strip_suffix(&normalized[boundary..]))
.map(str::len);
rendered.mermaid_start = rendered.mermaid_start.map(|boundary| {
rendered.mutable_fence_start = rendered.mutable_fence_start.map(|boundary| {
markdown_source
.strip_suffix(&normalized[boundary..])
.map_or(0, str::len)
Expand Down Expand Up @@ -246,6 +246,15 @@ pub(crate) fn extract_copy_targets(markdown_source: &str) -> Vec<CopyTarget> {
targets
}

/// Apply table-fence normalization only when terminal table layouts are enabled.
pub(crate) fn normalize_markdown_for_rendering(markdown_source: &str) -> Cow<'_, str> {
if crate::markdown_render::preferences::current().tables {
unwrap_markdown_fences(markdown_source)
} else {
Cow::Borrowed(markdown_source)
}
}

/// Strip `` ```md ``/`` ```markdown `` fences that contain tables, emitting their content as bare
/// markdown so `pulldown-cmark` parses the tables natively.
///
Expand Down
12 changes: 8 additions & 4 deletions codex-rs/tui/src/markdown_render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! This module consumes `pulldown-cmark` events and emits styled `ratatui`
//! lines, including table layout, Mermaid previews, width-aware wrapping, and local file-link
//! display. It is the final rendering stage used by higher-level helpers in
//! `markdown.rs`.
//! `markdown.rs`. Launch-time `tui.rendering` preferences preserve disabled features as source.
//!
//! Local file-link parsing and display policy live in [`local_links`].
//! List spacing stays a renderer policy: compact while streaming in an owned viewport, uniform
Expand Down Expand Up @@ -78,6 +78,8 @@ mod list_spacing;
mod local_links;
mod math;
mod mermaid;
pub(crate) mod preferences;
mod source_tables;
mod streaming;
mod table_key_value;
mod web_links;
Expand Down Expand Up @@ -366,9 +368,10 @@ pub(crate) fn render_markdown_lines_with_width_cwd_and_hidden_link_destinations(
options.insert(Options::ENABLE_STRIKETHROUGH);
options.insert(Options::ENABLE_TABLES);
let math = math::MathMarkdown::new(input, options, width);
let parser = DecodedTextMerge::new(
let parser = DecodedTextMerge::new(source_tables::preserve(
input,
math.events(Parser::new_ext(&math.markdown, options).into_offset_iter()),
);
));
let mut w = Writer::new(input, parser, width, cwd, is_hidden_link_destination);
w.run();
w.text
Expand Down Expand Up @@ -956,7 +959,8 @@ where
if let Some(lang) = self.code_block_lang.take() {
let code = std::mem::take(&mut self.code_block_buffer);
if !code.is_empty() {
let diagram = if lang == "mermaid"
let diagram = if preferences::current().mermaid
&& lang == "mermaid"
&& mermaid::has_closing_fence(self.input, range, self.code_block_content_end)
{
let indent =
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/markdown_render/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ impl<'a> MathMarkdown<'a> {
continue;
}
}
let rendered = if formula.len() < MAX_MATH_BYTES {
let rendered = if super::preferences::current().math && formula.len() < MAX_MATH_BYTES {
render::render(formula, display)
} else {
None
Expand Down
Loading
Loading