fix(tui): stabilize tab status detail updates

This commit is contained in:
Felipe Coury
2026-06-12 12:36:55 -07:00
parent 3a2dff2ebe
commit ce6de831f2
6 changed files with 71 additions and 52 deletions
+1 -48
View File
@@ -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
);
}
+5 -1
View File
@@ -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;
+10
View File
@@ -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]