mirror of
https://github.com/openai/codex.git
synced 2026-05-02 04:11:39 +03:00
fix(tui): keep unified exec summary on working line (#10962)
## Problem When unified-exec background sessions appear while the status indicator is visible, the bottom pane can grow by one row to show a dedicated footer line. That row insertion/removal makes the composer jump vertically and produces visible jitter/flicker during streaming turns. ## Mental model The bottom pane should expose one canonical background-exec summary string, but it should surface that string in only one place at a time: - if the status indicator row is visible, show the summary inline on that row; - if the status indicator row is hidden, show the summary as the standalone unified-exec footer row. This keeps status information visible while preserving a stable pane height. ## Non-goals This change does not alter unified-exec lifecycle, process tracking, or `/ps` behavior. It does not redesign status text copy, spinner timing, or interrupt handling semantics. ## Tradeoffs Inlining the summary preserves layout stability and keeps interrupt affordances in a fixed location, but it reduces horizontal space for long status/detail text in narrow terminals. We accept that truncation risk in exchange for removing vertical jitter and keeping the composer anchored. ## Architecture `UnifiedExecFooter` remains the source of truth for background-process summary copy via `summary_text()`. `BottomPane` mirrors that text into `StatusIndicatorWidget::update_inline_message()` whenever process state changes or a status widget is created. Rendering enforces single-surface output: the standalone footer row is skipped while status is present, and the status row appends the summary after the elapsed/interrupt segment. ## Documentation pass Added non-functional docs/comments that make the new invariant explicit: - status row owns inline summary when present; - unified-exec footer row renders only when status row is absent; - summary ordering keeps elapsed/interrupt affordance in a stable position. ## Observability No new telemetry or logs are introduced. The behavior is traceable through: - `BottomPane::set_unified_exec_processes()` for state updates, - `BottomPane::sync_status_inline_message()` for status-row synchronization, - `StatusIndicatorWidget::render()` for final inline ordering. ## Tests - Added `bottom_pane::tests::unified_exec_summary_does_not_increase_height_when_status_visible` to lock the no-height-growth invariant. - Updated the unified-exec status restoration snapshot to match inline rendering order. - Validated with: - `just fmt` - `cargo test -p codex-tui --lib` --------- Co-authored-by: Sayan Sisodiya <sayan@openai.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
//! A live status indicator that shows the *latest* log line emitted by the
|
||||
//! application while the agent is processing a long‑running task.
|
||||
//! A live task status row rendered above the composer while the agent is busy.
|
||||
//!
|
||||
//! The row owns spinner timing, the optional interrupt hint, and short inline
|
||||
//! context (for example, the unified-exec background-process summary). Keeping
|
||||
//! these pieces on one line avoids vertical layout churn in the bottom pane.
|
||||
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
@@ -30,10 +33,13 @@ use crate::wrapping::word_wrap_lines;
|
||||
const DETAILS_MAX_LINES: usize = 3;
|
||||
const DETAILS_PREFIX: &str = " └ ";
|
||||
|
||||
/// Displays a single-line in-progress status with optional wrapped details.
|
||||
pub(crate) struct StatusIndicatorWidget {
|
||||
/// Animated header text (defaults to "Working").
|
||||
header: String,
|
||||
details: Option<String>,
|
||||
/// Optional suffix rendered after the elapsed/interrupt segment.
|
||||
inline_message: Option<String>,
|
||||
show_interrupt_hint: bool,
|
||||
|
||||
elapsed_running: Duration,
|
||||
@@ -70,6 +76,7 @@ impl StatusIndicatorWidget {
|
||||
Self {
|
||||
header: String::from("Working"),
|
||||
details: None,
|
||||
inline_message: None,
|
||||
show_interrupt_hint: true,
|
||||
elapsed_running: Duration::ZERO,
|
||||
last_resume_at: Instant::now(),
|
||||
@@ -97,6 +104,17 @@ impl StatusIndicatorWidget {
|
||||
.map(|details| capitalize_first(details.trim_start()));
|
||||
}
|
||||
|
||||
/// Update the inline suffix text shown after `({elapsed} • esc to interrupt)`.
|
||||
///
|
||||
/// Callers should provide plain, already-contextualized text. Passing
|
||||
/// verbose status prose here can cause frequent width truncation and hide
|
||||
/// the more important elapsed/interrupt hint.
|
||||
pub(crate) fn update_inline_message(&mut self, message: Option<String>) {
|
||||
self.inline_message = message
|
||||
.map(|message| message.trim().to_string())
|
||||
.filter(|message| !message.is_empty());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn header(&self) -> &str {
|
||||
&self.header
|
||||
@@ -225,6 +243,12 @@ impl Renderable for StatusIndicatorWidget {
|
||||
} else {
|
||||
spans.push(format!("({pretty_elapsed})").dim());
|
||||
}
|
||||
if let Some(message) = &self.inline_message {
|
||||
// Keep optional context after elapsed/interrupt text so that core
|
||||
// interrupt affordances stay in a fixed visual location.
|
||||
spans.push(" · ".dim());
|
||||
spans.push(message.clone().dim());
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push(Line::from(spans));
|
||||
|
||||
Reference in New Issue
Block a user