remove "status text" in bottom line (#2279)

this used to hold the most recent log line, but it was kinda broken and
not that useful.
This commit is contained in:
Jeremy Rose
2025-08-14 14:10:21 -04:00
committed by GitHub
parent 585f7b0679
commit b42e679227
9 changed files with 21 additions and 269 deletions

View File

@@ -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.

View File

@@ -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),

View File

@@ -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

View File

@@ -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, multiline 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<StatusIndicatorWidget>,
/// 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);

View File

@@ -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
}

View File

@@ -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);
}

View File

@@ -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::<String>();
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<String>,
) -> color_eyre::Result<codex_core::protocol::TokenUsage> {
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();

View File

@@ -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 alreadyformatted 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
/// singleline status indicator cannot overflow the viewport.
pub struct TuiLogLayer {
tx: UnboundedSender<String>,
max_len: usize,
}
impl TuiLogLayer {
pub fn new(tx: UnboundedSender<String>, max_len: usize) -> Self {
Self {
tx,
max_len: max_len.max(8),
}
}
}
impl<S> Layer<S> 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 UTF8 codepoint 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, boundarysafe 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);
}
}

View File

@@ -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!({