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:
@@ -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.
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<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 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);
|
||||
}
|
||||
}
|
||||
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user