mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
fix(tui): stabilize tab status detail updates
This commit is contained in:
@@ -59,6 +59,7 @@ mod request_user_input;
|
||||
mod status_line_setup;
|
||||
mod status_line_style;
|
||||
mod status_surface_preview;
|
||||
mod tab_status_detail;
|
||||
mod tab_status_state;
|
||||
mod title_setup;
|
||||
pub(crate) use action_required_title::ACTION_REQUIRED_PREVIEW_PREFIX;
|
||||
@@ -328,54 +329,6 @@ impl BottomPane {
|
||||
}
|
||||
}
|
||||
|
||||
fn desired_tab_status(&self) -> (TabStatus, Option<String>) {
|
||||
let class = self.desired_tab_class();
|
||||
let detail = match class {
|
||||
TabStatus::Waiting => self
|
||||
.active_view()
|
||||
.and_then(BottomPaneView::tab_status_detail),
|
||||
TabStatus::Working => self.working_tab_status_detail(),
|
||||
TabStatus::Idle => self
|
||||
.tab_status
|
||||
.last_activity()
|
||||
.map(|activity| format!("last: {activity}")),
|
||||
};
|
||||
(class, detail)
|
||||
}
|
||||
|
||||
fn working_tab_status_detail(&self) -> Option<String> {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(activity) = self.tab_status.current_activity() {
|
||||
parts.push(activity.to_string());
|
||||
}
|
||||
let push_if_new = |parts: &mut Vec<String>, candidate: &str| {
|
||||
let candidate = candidate.trim();
|
||||
if candidate.is_empty()
|
||||
|| parts
|
||||
.iter()
|
||||
.any(|part| part.contains(candidate) || candidate.contains(part.as_str()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
parts.push(candidate.to_string());
|
||||
};
|
||||
if let Some(status) = self.status.as_ref() {
|
||||
let header = status.header();
|
||||
if header != "Working" {
|
||||
push_if_new(&mut parts, header);
|
||||
}
|
||||
if let Some(first_line) = status.details().and_then(|details| details.lines().next()) {
|
||||
push_if_new(&mut parts, first_line);
|
||||
}
|
||||
}
|
||||
if !parts.is_empty() {
|
||||
return Some(parts.join(" • "));
|
||||
}
|
||||
self.tab_status
|
||||
.last_activity()
|
||||
.map(|activity| format!("after: {activity}"))
|
||||
}
|
||||
|
||||
fn refresh_tab_status(&mut self) {
|
||||
self.refresh_tab_status_at(Instant::now());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
use super::BottomPane;
|
||||
use super::BottomPaneView;
|
||||
use crate::tab_status::TabStatus;
|
||||
|
||||
impl BottomPane {
|
||||
pub(super) fn desired_tab_status(&self) -> (TabStatus, Option<String>) {
|
||||
let class = self.desired_tab_class();
|
||||
let detail = match class {
|
||||
TabStatus::Waiting => self
|
||||
.active_view()
|
||||
.and_then(BottomPaneView::tab_status_detail),
|
||||
TabStatus::Working => self.working_tab_status_detail(),
|
||||
TabStatus::Idle => self
|
||||
.tab_status
|
||||
.last_activity()
|
||||
.map(|activity| format!("last: {activity}")),
|
||||
};
|
||||
(class, detail)
|
||||
}
|
||||
|
||||
fn working_tab_status_detail(&self) -> Option<String> {
|
||||
let mut parts = Vec::new();
|
||||
if let Some(activity) = self.tab_status.current_activity() {
|
||||
parts.push(activity.to_string());
|
||||
}
|
||||
let push_if_new = |parts: &mut Vec<String>, candidate: &str| {
|
||||
let candidate = candidate.trim();
|
||||
if candidate.is_empty()
|
||||
|| parts
|
||||
.iter()
|
||||
.any(|part| part.contains(candidate) || candidate.contains(part.as_str()))
|
||||
{
|
||||
return;
|
||||
}
|
||||
parts.push(candidate.to_string());
|
||||
};
|
||||
if let Some(status) = self.status.as_ref() {
|
||||
let header = status.header();
|
||||
if header != "Working" {
|
||||
push_if_new(&mut parts, header);
|
||||
}
|
||||
if let Some(first_line) = status.details().and_then(|details| details.lines().next()) {
|
||||
push_if_new(&mut parts, first_line);
|
||||
}
|
||||
}
|
||||
if !parts.is_empty() {
|
||||
return Some(parts.join(" • "));
|
||||
}
|
||||
self.tab_status
|
||||
.last_activity()
|
||||
.map(|activity| format!("after: {activity}"))
|
||||
}
|
||||
}
|
||||
@@ -80,7 +80,6 @@ impl TabStatusState {
|
||||
return;
|
||||
}
|
||||
if self.last_status.as_ref() == Some(&desired) {
|
||||
self.last_emit_at = Some(now);
|
||||
return;
|
||||
}
|
||||
if let Err(err) = set_tab_status(desired.0, desired.1.as_deref()) {
|
||||
|
||||
@@ -43,7 +43,7 @@ fn throttle_defers_detail_changes_but_not_status_changes() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_refresh_rearms_the_detail_throttle() {
|
||||
fn equal_refresh_does_not_rearm_the_detail_throttle() {
|
||||
let mut state = TabStatusState::new();
|
||||
let started_at = Instant::now();
|
||||
let unchanged_at = started_at + Duration::from_millis(/*millis*/ 300);
|
||||
@@ -56,6 +56,6 @@ fn equal_refresh_rearms_the_detail_throttle() {
|
||||
TabStatus::Working,
|
||||
unchanged_at + Duration::from_millis(/*millis*/ 100)
|
||||
),
|
||||
Some(Duration::from_millis(/*millis*/ 150))
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
@@ -131,12 +131,16 @@ pub(crate) fn clear_tab_status() -> io::Result<()> {
|
||||
}
|
||||
|
||||
fn sanitize_detail(detail: &str) -> String {
|
||||
let mut out = String::with_capacity(detail.len());
|
||||
let mut out = String::with_capacity(detail.len().min(MAX_TAB_STATUS_DETAIL_CHARS * 2 + 1));
|
||||
let mut chars_written = 0;
|
||||
let mut pending_space = false;
|
||||
let mut truncated = false;
|
||||
|
||||
for ch in detail.chars() {
|
||||
if chars_written >= MAX_TAB_STATUS_DETAIL_CHARS {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
if ch.is_whitespace() {
|
||||
pending_space = !out.is_empty();
|
||||
continue;
|
||||
|
||||
@@ -50,6 +50,16 @@ fn detail_is_safe_bounded_osc_text() {
|
||||
MAX_TAB_STATUS_DETAIL_CHARS * 2 + 1
|
||||
);
|
||||
assert!(sanitized.ends_with('…'));
|
||||
|
||||
let sanitized = sanitize_detail(&format!(
|
||||
"{}{}tail",
|
||||
"x".repeat(MAX_TAB_STATUS_DETAIL_CHARS),
|
||||
" \u{202E}".repeat(/*n*/ 10_000)
|
||||
));
|
||||
assert_eq!(
|
||||
sanitized,
|
||||
format!("{}…", "x".repeat(MAX_TAB_STATUS_DETAIL_CHARS))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user