diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index fb45ecfd..a6ced654 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -183,12 +183,6 @@ impl App<'_> { } } - /// Clone of the internal event sender so external tasks (e.g. log bridge) - /// can inject `AppEvent`s. - pub fn event_sender(&self) -> AppEventSender { - self.app_event_tx.clone() - } - /// Schedule a redraw if one is not already pending. #[allow(clippy::unwrap_used)] fn schedule_redraw(&self) { @@ -325,10 +319,6 @@ impl App<'_> { AppState::Chat { widget } => widget.submit_op(op), AppState::Onboarding { .. } => {} }, - AppEvent::LatestLog(line) => match &mut self.app_state { - AppState::Chat { widget } => widget.update_latest_log(line), - AppState::Onboarding { .. } => {} - }, AppEvent::DispatchCommand(command) => match command { SlashCommand::New => { // User accepted – switch to chat view. diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index a52f8baf..9fe952ea 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -29,9 +29,6 @@ pub(crate) enum AppEvent { /// bubbling channels through layers of widgets. CodexOp(codex_core::protocol::Op), - /// Latest formatted log line emitted by `tracing`. - LatestLog(String), - /// Dispatch a recognized slash command from the UI (composer) to the app /// layer so it can be handled centrally. DispatchCommand(SlashCommand), diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index a5616371..c86dad31 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -6,12 +6,6 @@ use ratatui::layout::Rect; use super::BottomPane; use super::CancellationEvent; -/// Type to use for a method that may require a redraw of the UI. -pub(crate) enum ConditionalUpdate { - NeedsRedraw, - NoRedraw, -} - /// Trait implemented by every view that can be shown in the bottom pane. pub(crate) trait BottomPaneView<'a> { /// Handle a key event while the view is active. A redraw is always @@ -34,11 +28,6 @@ pub(crate) trait BottomPaneView<'a> { /// Render the view: this will be displayed in place of the composer. fn render(&self, area: Rect, buf: &mut Buffer); - /// Update the status indicator text. - fn update_status_text(&mut self, _text: String) -> ConditionalUpdate { - ConditionalUpdate::NoRedraw - } - /// Called when task completes to check if the view should be hidden. fn should_hide_when_task_is_done(&mut self) -> bool { false diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 69f174f1..b2da8d28 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -32,7 +32,6 @@ pub(crate) enum CancellationEvent { pub(crate) use chat_composer::ChatComposer; pub(crate) use chat_composer::InputResult; -use crate::status_indicator_widget::StatusIndicatorWidget; use approval_modal_view::ApprovalModalView; use status_indicator_view::StatusIndicatorView; @@ -50,11 +49,6 @@ pub(crate) struct BottomPane<'a> { is_task_running: bool, ctrl_c_quit_hint: bool, - /// Optional live, multi‑line status/"live cell" rendered directly above - /// the composer while a task is running. Unlike `active_view`, this does - /// not replace the composer; it augments it. - live_status: Option, - /// True if the active view is the StatusIndicatorView that replaces the /// composer during a running task. status_view_active: bool, @@ -81,27 +75,18 @@ impl BottomPane<'_> { has_input_focus: params.has_input_focus, is_task_running: false, ctrl_c_quit_hint: false, - live_status: None, status_view_active: false, } } pub fn desired_height(&self, width: u16) -> u16 { - let overlay_status_h = self - .live_status - .as_ref() - .map(|s| s.desired_height(width)) - .unwrap_or(0); - let view_height = if let Some(view) = self.active_view.as_ref() { view.desired_height(width) } else { self.composer.desired_height(width) }; - overlay_status_h - .saturating_add(view_height) - .saturating_add(Self::BOTTOM_PAD_LINES) + view_height.saturating_add(Self::BOTTOM_PAD_LINES) } pub fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> { @@ -182,44 +167,6 @@ impl BottomPane<'_> { self.request_redraw(); } - /// Update the status indicator text. Prefer replacing the composer with - /// the StatusIndicatorView so the input pane shows a single-line status - /// like: `▌ Working waiting for model`. - pub(crate) fn update_status_text(&mut self, text: String) { - let mut handled_by_view = false; - if let Some(view) = self.active_view.as_mut() { - if matches!( - view.update_status_text(text.clone()), - bottom_pane_view::ConditionalUpdate::NeedsRedraw - ) { - handled_by_view = true; - } - } else { - let mut v = StatusIndicatorView::new(self.app_event_tx.clone()); - v.update_text(text.clone()); - self.active_view = Some(Box::new(v)); - self.status_view_active = true; - handled_by_view = true; - } - - // Fallback: if the current active view did not consume status updates - // and no modal view is active, present an overlay above the composer. - // If a modal is active, do NOT render the overlay to avoid drawing - // over the dialog. - if !handled_by_view && self.active_view.is_none() { - if self.live_status.is_none() { - self.live_status = Some(StatusIndicatorWidget::new(self.app_event_tx.clone())); - } - if let Some(status) = &mut self.live_status { - status.update_text(text); - } - } else if !handled_by_view { - // Ensure any previous overlay is cleared when a modal becomes active. - self.live_status = None; - } - self.request_redraw(); - } - pub(crate) fn show_ctrl_c_quit_hint(&mut self) { self.ctrl_c_quit_hint = true; self.composer @@ -252,7 +199,6 @@ impl BottomPane<'_> { } self.request_redraw(); } else { - self.live_status = None; // Drop the status view when a task completes, but keep other // modal views (e.g. approval dialogs). if let Some(mut view) = self.active_view.take() { @@ -302,8 +248,6 @@ impl BottomPane<'_> { // Otherwise create a new approval modal overlay. let modal = ApprovalModalView::new(request, self.app_event_tx.clone()); self.active_view = Some(Box::new(modal)); - // Hide any overlay status while a modal is visible. - self.live_status = None; self.status_view_active = false; self.request_redraw() } @@ -342,46 +286,31 @@ impl BottomPane<'_> { impl WidgetRef for &BottomPane<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let mut y_offset = 0u16; - if let Some(status) = &self.live_status { - let live_h = status - .desired_height(area.width) - .min(area.height.saturating_sub(y_offset)); - if live_h > 0 { - let live_rect = Rect { - x: area.x, - y: area.y + y_offset, - width: area.width, - height: live_h, - }; - status.render_ref(live_rect, buf); - y_offset = y_offset.saturating_add(live_h); - } - } - if let Some(view) = &self.active_view { - if y_offset < area.height { - // Reserve bottom padding lines; keep at least 1 line for the view. - let avail = area.height - y_offset; + // Reserve bottom padding lines; keep at least 1 line for the view. + let avail = area.height; + if avail > 0 { let pad = BottomPane::BOTTOM_PAD_LINES.min(avail.saturating_sub(1)); let view_rect = Rect { x: area.x, - y: area.y + y_offset, + y: area.y, width: area.width, height: avail - pad, }; view.render(view_rect, buf); } - } else if y_offset < area.height { - let composer_rect = Rect { - x: area.x, - y: area.y + y_offset, - width: area.width, - // Reserve bottom padding - height: (area.height - y_offset) - - BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1)), - }; - (&self.composer).render_ref(composer_rect, buf); + } else { + let avail = area.height; + if avail > 0 { + let composer_rect = Rect { + x: area.x, + y: area.y, + width: area.width, + // Reserve bottom padding + height: avail - BottomPane::BOTTOM_PAD_LINES.min(avail.saturating_sub(1)), + }; + (&self.composer).render_ref(composer_rect, buf); + } } } } @@ -431,10 +360,8 @@ mod tests { // Create an approval modal (active view). pane.push_approval_request(exec_request()); - // Attempt to update status; this should NOT create an overlay while modal is visible. - pane.update_status_text("running command".to_string()); - // Render and verify the top row does not include the Working header overlay. + // Render and verify the top row does not include an overlay. let area = Rect::new(0, 0, 60, 6); let mut buf = Buffer::empty(area); (&pane).render_ref(area, &mut buf); @@ -445,7 +372,7 @@ mod tests { } assert!( !r0.contains("Working"), - "overlay Working header should not render above modal" + "overlay should not render above modal" ); } @@ -461,7 +388,6 @@ mod tests { // Start a running task so the status indicator replaces the composer. pane.set_task_running(true); - pane.update_status_text("waiting for model".to_string()); // Push an approval modal (e.g., command approval) which should hide the status view. pane.push_approval_request(exec_request()); @@ -511,11 +437,6 @@ mod tests { // Begin a task: show initial status. pane.set_task_running(true); - pane.update_status_text("waiting for model".to_string()); - - // As a long-running command begins (post-approval), ensure the status - // indicator is visible while we wait for the command to run. - pane.update_status_text("running command".to_string()); // Allow some frames so the animation thread ticks. std::thread::sleep(std::time::Duration::from_millis(120)); @@ -547,7 +468,6 @@ mod tests { // Activate spinner (status view replaces composer) with no live ring. pane.set_task_running(true); - pane.update_status_text("waiting for model".to_string()); // Use height == desired_height; expect 1 status row at top and 2 bottom padding rows. let height = pane.desired_height(30); @@ -598,7 +518,6 @@ mod tests { }); pane.set_task_running(true); - pane.update_status_text("waiting for model".to_string()); // Height=2 → pad shrinks to 1; bottom row is blank, top row has spinner. let area2 = Rect::new(0, 0, 20, 2); diff --git a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs index cad4f0f2..b0f64a97 100644 --- a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs +++ b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs @@ -8,7 +8,6 @@ use crate::bottom_pane::BottomPane; use crate::status_indicator_widget::StatusIndicatorWidget; use super::BottomPaneView; -use super::bottom_pane_view::ConditionalUpdate; pub(crate) struct StatusIndicatorView { view: StatusIndicatorWidget, @@ -27,11 +26,6 @@ impl StatusIndicatorView { } impl BottomPaneView<'_> for StatusIndicatorView { - fn update_status_text(&mut self, text: String) -> ConditionalUpdate { - self.update_text(text); - ConditionalUpdate::NeedsRedraw - } - fn should_hide_when_task_is_done(&mut self) -> bool { true } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fba3caf1..0319163f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -166,7 +166,6 @@ impl ChatWidget<'_> { fn on_task_started(&mut self) { self.bottom_pane.clear_ctrl_c_quit_hint(); self.bottom_pane.set_task_running(true); - self.set_waiting_for_model_status(); self.stream.reset_headers_for_new_turn(); self.last_stream_kind = None; self.mark_needs_redraw(); @@ -338,16 +337,9 @@ impl ChatWidget<'_> { } } - #[inline] - fn set_waiting_for_model_status(&mut self) { - self.bottom_pane - .update_status_text("waiting for model".to_string()); - } - #[inline] fn handle_streaming_delta(&mut self, kind: StreamKind, delta: String) { let sink = AppEventHistorySink(self.app_event_tx.clone()); - self.set_waiting_for_model_status(); self.stream.begin(kind, &sink); self.last_stream_kind = Some(kind); self.stream.push_and_maybe_commit(&delta, &sink); @@ -437,8 +429,6 @@ impl ChatWidget<'_> { pub(crate) fn handle_exec_begin_now(&mut self, ev: ExecCommandBeginEvent) { // Ensure the status indicator is visible while the command runs. - self.bottom_pane - .update_status_text("running command".to_string()); self.running_commands.insert( ev.call_id.clone(), RunningCommand { @@ -670,13 +660,6 @@ impl ChatWidget<'_> { } } - /// Update the live log preview while a task is running. - pub(crate) fn update_latest_log(&mut self, line: String) { - if self.bottom_pane.is_task_running() { - self.bottom_pane.update_status_text(line); - } - } - fn request_redraw(&mut self) { self.app_event_tx.send(AppEvent::RequestRedraw); } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 3ddebe75..a661242d 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -14,7 +14,6 @@ use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; use codex_login::CodexAuth; use codex_ollama::DEFAULT_OSS_MODEL; -use log_layer::TuiLogLayer; use std::fs::OpenOptions; use std::path::PathBuf; use tracing::error; @@ -39,7 +38,6 @@ mod get_git_diff; mod history_cell; pub mod insert_history; pub mod live_wrap; -mod log_layer; mod markdown; mod markdown_stream; pub mod onboarding; @@ -212,14 +210,7 @@ pub async fn run_main( .map_err(|e| std::io::Error::other(format!("OSS setup failed: {e}")))?; } - // Channel that carries formatted log lines to the UI. - let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel::(); - let tui_layer = TuiLogLayer::new(log_tx.clone(), 120).with_filter(env_filter()); - - let _ = tracing_subscriber::registry() - .with(file_layer) - .with(tui_layer) - .try_init(); + let _ = tracing_subscriber::registry().with(file_layer).try_init(); #[allow(clippy::print_stderr)] #[cfg(not(debug_assertions))] @@ -253,7 +244,7 @@ pub async fn run_main( eprintln!(""); } - run_ratatui_app(cli, config, should_show_trust_screen, log_rx) + run_ratatui_app(cli, config, should_show_trust_screen) .map_err(|err| std::io::Error::other(err.to_string())) } @@ -261,7 +252,6 @@ fn run_ratatui_app( cli: Cli, config: Config, should_show_trust_screen: bool, - mut log_rx: tokio::sync::mpsc::UnboundedReceiver, ) -> color_eyre::Result { color_eyre::install()?; @@ -283,16 +273,6 @@ fn run_ratatui_app( let Cli { prompt, images, .. } = cli; let mut app = App::new(config.clone(), prompt, images, should_show_trust_screen); - // Bridge log receiver into the AppEvent channel so latest log lines update the UI. - { - let app_event_tx = app.event_sender(); - tokio::spawn(async move { - while let Some(line) = log_rx.recv().await { - app_event_tx.send(crate::app_event::AppEvent::LatestLog(line)); - } - }); - } - let app_result = app.run(&mut terminal); let usage = app.token_usage(); diff --git a/codex-rs/tui/src/log_layer.rs b/codex-rs/tui/src/log_layer.rs deleted file mode 100644 index a55c66bf..00000000 --- a/codex-rs/tui/src/log_layer.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Custom `tracing_subscriber` layer that forwards every formatted log event to the -//! TUI so the status indicator can display the *latest* log line while a task is -//! running. -//! -//! The layer is intentionally extremely small: we implement `on_event()` only and -//! ignore spans/metadata because we only care about the already‑formatted output -//! that the default `fmt` layer would print. We therefore borrow the same -//! formatter (`tracing_subscriber::fmt::format::FmtSpan`) used by the default -//! fmt layer so the text matches what is written to the log file. - -use std::fmt::Write as _; - -use tokio::sync::mpsc::UnboundedSender; -use tracing::Event; -use tracing::Subscriber; -use tracing::field::Field; -use tracing::field::Visit; -use tracing_subscriber::Layer; -use tracing_subscriber::layer::Context; -use tracing_subscriber::registry::LookupSpan; - -/// Maximum characters forwarded to the TUI. Longer messages are truncated so the -/// single‑line status indicator cannot overflow the viewport. -pub struct TuiLogLayer { - tx: UnboundedSender, - max_len: usize, -} - -impl TuiLogLayer { - pub fn new(tx: UnboundedSender, max_len: usize) -> Self { - Self { - tx, - max_len: max_len.max(8), - } - } -} - -impl Layer for TuiLogLayer -where - S: Subscriber + for<'a> LookupSpan<'a>, -{ - fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { - // Build a terse line like `[TRACE core::session] message …` by visiting - // fields into a buffer. This avoids pulling in the heavyweight - // formatter machinery. - - struct Visitor<'a> { - buf: &'a mut String, - } - - impl Visit for Visitor<'_> { - fn record_debug(&mut self, _field: &Field, value: &dyn std::fmt::Debug) { - let _ = write!(self.buf, " {value:?}"); - } - } - - let mut buf = String::new(); - let _ = write!( - buf, - "[{} {}]", - event.metadata().level(), - event.metadata().target() - ); - - event.record(&mut Visitor { buf: &mut buf }); - - // `String::truncate` operates on UTF‑8 code‑point boundaries and will - // panic if the provided index is not one. Because we limit the log - // line by its **byte** length we can not guarantee that the index we - // want to cut at happens to be on a boundary. Therefore we fall back - // to a simple, boundary‑safe loop that pops complete characters until - // the string is within the designated size. - - if buf.len() > self.max_len { - // Attempt direct truncate at the byte index. If that is not a - // valid boundary we advance to the next one ( ≤3 bytes away ). - if buf.is_char_boundary(self.max_len) { - buf.truncate(self.max_len); - } else { - let mut idx = self.max_len; - while idx < buf.len() && !buf.is_char_boundary(idx) { - idx += 1; - } - buf.truncate(idx); - } - } - - let sanitized = buf.replace(['\n', '\r'], " "); - let _ = self.tx.send(sanitized); - } -} diff --git a/codex-rs/tui/src/session_log.rs b/codex-rs/tui/src/session_log.rs index 6cdfc22f..e94f5843 100644 --- a/codex-rs/tui/src/session_log.rs +++ b/codex-rs/tui/src/session_log.rs @@ -188,15 +188,6 @@ pub(crate) fn log_inbound_app_event(event: &AppEvent) { }); LOGGER.write_json_line(value); } - AppEvent::LatestLog(line) => { - let value = json!({ - "ts": now_ts(), - "dir": "to_tui", - "kind": "log_line", - "line": line, - }); - LOGGER.write_json_line(value); - } // Noise or control flow – record variant only other => { let value = json!({