diff --git a/codex-rs/tui/src/bottom_pane/slash_commands.rs b/codex-rs/tui/src/bottom_pane/slash_commands.rs index e907b6c5e554..a185970f4ef0 100644 --- a/codex-rs/tui/src/bottom_pane/slash_commands.rs +++ b/codex-rs/tui/src/bottom_pane/slash_commands.rs @@ -294,6 +294,7 @@ mod tests { SlashCommand::Ide, SlashCommand::Copy, SlashCommand::Raw, + SlashCommand::Lcs, SlashCommand::Diff, SlashCommand::Mention, SlashCommand::Status, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index f8a33e987576..152553964a15 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -535,6 +535,9 @@ pub(crate) struct ChatWidget { transcript: TranscriptState, config: Config, raw_output_mode: bool, + /// Whether the LCS substrate (MCP tool calls) is rendered as it runs. + /// True by default; `/lcs hidden` sets it false. + show_lcs_substrate: bool, /// Runtime value resolved by core. `config.service_tier` remains the explicit user choice. effective_service_tier: Option, /// The unmasked collaboration mode settings (always Default mode). @@ -1664,6 +1667,38 @@ impl ChatWidget { enabled } + /// Whether MCP tool calls are rendered as they run. + pub(crate) fn show_lcs_substrate(&self) -> bool { + self.show_lcs_substrate + } + + pub(crate) fn set_lcs_substrate(&mut self, enabled: bool) { + self.show_lcs_substrate = enabled; + self.refresh_status_surfaces(); + } + + pub(crate) fn lcs_substrate_notice(enabled: bool) -> &'static str { + if enabled { + "LCS substrate visible: MCP tool calls are shown as they run." + } else { + "LCS substrate hidden: MCP tool calls run silently." + } + } + + pub(crate) fn set_lcs_substrate_and_notify(&mut self, enabled: bool) { + self.set_lcs_substrate(enabled); + self.add_info_message( + Self::lcs_substrate_notice(enabled).to_string(), + /*hint*/ None, + ); + } + + pub(crate) fn toggle_lcs_substrate_and_notify(&mut self) -> bool { + let enabled = !self.show_lcs_substrate; + self.set_lcs_substrate_and_notify(enabled); + enabled + } + /// Update resize-sensitive chat widget state after the terminal width changes. /// /// The app calls this even when terminal resize reflow is disabled so live stream wrapping diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 7e86814b92be..15d08a329d3f 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -106,6 +106,9 @@ impl ChatWidget { }), transcript: TranscriptState::new(active_cell), raw_output_mode: config.tui_raw_output_mode, + // The LCS substrate (MCP tool calls) is visible by default. + // `/lcs hidden` opts out. + show_lcs_substrate: true, config, effective_service_tier, skills_all: Vec::new(), diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index a6fd68326cce..2459f30ebdbc 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -35,6 +35,7 @@ const SIDE_SLASH_COMMAND_UNAVAILABLE_HINT: &str = "Press Ctrl+C to return to the main thread first."; const GOAL_USAGE_HINT: &str = "Example: /goal improve benchmark coverage"; const RAW_USAGE: &str = "Usage: /raw [on|off]"; +const LCS_USAGE: &str = "Usage: /lcs [hidden|visible]"; impl ChatWidget { /// Dispatch a bare slash command and record its staged local-history entry. @@ -374,6 +375,9 @@ impl ChatWidget { let enabled = self.toggle_raw_output_mode_and_notify(); self.emit_raw_output_mode_changed(enabled); } + SlashCommand::Lcs => { + self.toggle_lcs_substrate_and_notify(); + } SlashCommand::Diff => { self.add_diff_in_progress(); let tx = self.app_event_tx.clone(); @@ -669,6 +673,11 @@ impl ChatWidget { } _ => self.add_error_message(RAW_USAGE.to_string()), }, + SlashCommand::Lcs => match trimmed.to_ascii_lowercase().as_str() { + "hidden" => self.set_lcs_substrate_and_notify(/*enabled*/ false), + "visible" => self.set_lcs_substrate_and_notify(/*enabled*/ true), + _ => self.add_error_message(LCS_USAGE.to_string()), + }, SlashCommand::Rename if !trimmed.is_empty() => { if !self.ensure_thread_rename_allowed() { return; @@ -996,6 +1005,7 @@ impl ChatWidget { | SlashCommand::Rollout | SlashCommand::Copy | SlashCommand::Raw + | SlashCommand::Lcs | SlashCommand::Vim | SlashCommand::Diff | SlashCommand::App diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 9c2802fb6041..5c310d3d704b 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -2419,6 +2419,44 @@ async fn raw_slash_command_reports_usage_for_invalid_arg() { ); } +#[tokio::test] +async fn lcs_slash_command_toggles_and_accepts_hidden_visible_args() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + // The LCS substrate (MCP tool calls) is visible by default. + assert!(chat.show_lcs_substrate()); + + chat.dispatch_command_with_args(SlashCommand::Lcs, "hidden".to_string(), Vec::new()); + assert!(!chat.show_lcs_substrate()); + + chat.dispatch_command_with_args(SlashCommand::Lcs, "visible".to_string(), Vec::new()); + assert!(chat.show_lcs_substrate()); + + // Bare `/lcs` toggles. + chat.dispatch_command(SlashCommand::Lcs); + assert!(!chat.show_lcs_substrate()); +} + +#[tokio::test] +async fn lcs_slash_command_reports_usage_for_invalid_arg() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + chat.dispatch_command_with_args(SlashCommand::Lcs, "status".to_string(), Vec::new()); + + // Unchanged. + assert!(chat.show_lcs_substrate()); + let cells = drain_insert_history(&mut rx); + let rendered = cells + .iter() + .map(|lines| lines_to_single_string(lines)) + .collect::>() + .join("\n"); + assert!( + rendered.contains("Usage: /lcs [hidden|visible]"), + "expected lcs usage error, got {rendered:?}" + ); +} + #[tokio::test] async fn compact_queues_user_messages_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs index 5df5e7bc124a..2922766df42a 100644 --- a/codex-rs/tui/src/chatwidget/tool_lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/tool_lifecycle.rs @@ -50,19 +50,16 @@ impl ChatWidget { } pub(super) fn on_mcp_tool_call_started(&mut self, item: ThreadItem) { - let item2 = item.clone(); - self.defer_or_handle( - |q| q.push_item_started(item), - |s| s.handle_mcp_tool_call_started_now(item2), - ); + // Render MCP tool calls as they start, rather than batching them into + // the interrupt queue until the stream cycle ends. Flush pending + // interrupts first to preserve ordering, then handle immediately. + self.flush_interrupt_queue(); + self.handle_mcp_tool_call_started_now(item); } pub(super) fn on_mcp_tool_call_completed(&mut self, item: ThreadItem) { - let item2 = item.clone(); - self.defer_or_handle( - |q| q.push_item_completed(item), - |s| s.handle_mcp_tool_call_completed_now(item2), - ); + self.flush_interrupt_queue(); + self.handle_mcp_tool_call_completed_now(item); } pub(super) fn on_web_search_begin(&mut self, call_id: String) { @@ -159,6 +156,9 @@ impl ChatWidget { pub(crate) fn handle_mcp_tool_call_started_now(&mut self, item: ThreadItem) { self.record_visible_turn_activity(); + if !self.show_lcs_substrate { + return; + } let ThreadItem::McpToolCall { id, server, @@ -186,6 +186,10 @@ impl ChatWidget { pub(crate) fn handle_mcp_tool_call_completed_now(&mut self, item: ThreadItem) { self.flush_answer_stream_with_separator(); + self.transcript.had_work_activity = true; + if !self.show_lcs_substrate { + return; + } let ThreadItem::McpToolCall { id, diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 2481af21ede6..cb59e4056f99 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -43,6 +43,8 @@ pub enum SlashCommand { Btw, Copy, Raw, + /// Toggle whether the LCS substrate (MCP tool calls) is shown as it runs. + Lcs, Diff, Mention, Status, @@ -97,6 +99,9 @@ impl SlashCommand { SlashCommand::Quit | SlashCommand::Exit => "exit Codex", SlashCommand::Copy => "copy last response as markdown", SlashCommand::Raw => "toggle raw scrollback mode for copy-friendly terminal selection", + SlashCommand::Lcs => { + "toggle LCS substrate visibility (MCP tool calls): /lcs hidden|visible" + } SlashCommand::Diff => "show git diff (including untracked files)", SlashCommand::Mention => "mention a file", SlashCommand::Skills => "use skills to improve how Codex performs specific tasks", @@ -163,6 +168,7 @@ impl SlashCommand { | SlashCommand::Keymap | SlashCommand::Mcp | SlashCommand::Raw + | SlashCommand::Lcs | SlashCommand::Pets | SlashCommand::Side | SlashCommand::Btw @@ -177,6 +183,7 @@ impl SlashCommand { self, SlashCommand::Copy | SlashCommand::Raw + | SlashCommand::Lcs | SlashCommand::Diff | SlashCommand::Mention | SlashCommand::Status @@ -211,6 +218,7 @@ impl SlashCommand { SlashCommand::Diff | SlashCommand::Copy | SlashCommand::Raw + | SlashCommand::Lcs | SlashCommand::Rename | SlashCommand::Mention | SlashCommand::Skills