Remove thread title generation spinners from the TUI (#55324)

## What changed

Stop showing title-generation spinners in the footer, status line, and terminal title. Remove the associated UI pending state and animation refresh logic while retaining background title generation.

## Testing

Add a snapshot covering the footer and terminal title while automatic naming is pending. Update status-surface snapshots to cover thread names and ID fallbacks without progress indicators.

GitOrigin-RevId: 061456929149c216f0a98d7214179ec69cdaeee4
This commit is contained in:
Channing Conger
2026-10-19 17:09:36 +00:00
committed by copyberry
parent a309f9206a
commit 8dd6099e26
17 changed files with 75 additions and 292 deletions
-1
View File
@@ -1228,7 +1228,6 @@ impl App {
}
fn render_chat_widget_frame(&mut self, tui: &mut tui::Tui, screen_size: Size) -> Result<Rect> {
self.sync_thread_title_progress();
self.chat_widget
.set_sparkle_terminal_focus(tui.is_terminal_focused());
if tui.is_owned_screen() {
-1
View File
@@ -389,7 +389,6 @@ impl App {
for (_, cancellation) in self.pending_thread_titles.drain() {
cancellation.cancel();
}
self.sync_thread_title_progress();
self.agents_overview.dispatched_requests.clear();
self.agents_overview.request_id = None;
self.agents_overview.refresh_show_more = false;
@@ -0,0 +1,10 @@
---
source: tui/src/app/thread_title_tests.rs
assertion_line: 302
expression: "normalize_snapshot_paths(format!(\"terminal title: {}\\nfooter:\\n{}\",\napp.chat_widget.last_terminal_title.as_deref().unwrap_or_default(),\nrender_bottom_popup(&app.chat_widget, 120)))"
---
terminal title: project
footer:
› Ask Codex to do anything
gpt-5.2 default · /tmp/project
-1
View File
@@ -1455,7 +1455,6 @@ See the Codex keymap documentation for supported actions and examples."
}
} => {
app.chat_widget.refresh_goal_status_indicator_for_time_tick();
app.chat_widget.refresh_thread_title_progress_for_time_tick();
app.chat_widget.refresh_terminal_title();
AppRunControl::Continue
}
-13
View File
@@ -38,23 +38,12 @@ struct GeneratedThreadTitle {
}
impl App {
pub(super) fn sync_thread_title_progress(&mut self) {
let pending = self.chat_widget.thread_id().is_some_and(|thread_id| {
self.pending_thread_titles
.keys()
.any(|(id, _)| *id == thread_id)
});
self.chat_widget
.set_thread_title_generation_pending(pending);
}
pub(super) fn finish_thread_title_generation(
&mut self,
thread_id: ThreadId,
destination: ThreadTitleDestination,
) {
self.pending_thread_titles.remove(&(thread_id, destination));
self.sync_thread_title_progress();
}
/// A saved manual name supersedes any pending title generation for that thread.
@@ -67,7 +56,6 @@ impl App {
true
}
});
self.sync_thread_title_progress();
}
/// Start a hidden title-generation thread without blocking the UI loop.
@@ -84,7 +72,6 @@ impl App {
return;
};
let cancellation = entry.insert(CancellationToken::new()).clone();
self.sync_thread_title_progress();
let request_handle = app_server.request_handle();
let model = if self.chat_widget.config_ref().model_provider_id == "openai"
&& self.chat_widget.has_chatgpt_account()
+15 -34
View File
@@ -297,7 +297,20 @@ async fn check_thread_title_generation(scenario: TitleScenario) -> color_eyre::R
.await?;
}
assert_eq!(app.chat_widget.thread_name(), None);
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
if matches!(scenario, TitleScenario::Automatic) {
app.chat_widget.refresh_status_surfaces();
insta::assert_snapshot!(
"automatic_thread_title_pending",
normalize_snapshot_paths(format!(
"terminal title: {}\nfooter:\n{}",
app.chat_widget
.last_terminal_title
.as_deref()
.unwrap_or_default(),
render_bottom_popup(&app.chat_widget, /*width*/ 120)
))
);
}
assert_eq!(
app_server
.thread_read(thread_id, /*include_turns*/ false)
@@ -379,7 +392,6 @@ async fn check_thread_title_generation(scenario: TitleScenario) -> color_eyre::R
assert!(app.pending_thread_titles.is_empty());
let popup = render_bottom_popup(&app.chat_widget, /*width*/ 120);
assert!(popup.contains("Keep this title"));
assert!(!popup.contains('⠋'));
if matches!(scenario, TitleScenario::ManualRename) {
insta::assert_snapshot!(
"manual_rename_cancels_thread_title",
@@ -426,7 +438,6 @@ async fn check_thread_title_generation(scenario: TitleScenario) -> color_eyre::R
}
assert!(app.temporary_structured_requests.is_empty());
assert!(app.pending_thread_titles.is_empty());
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
let request = response.single_request();
assert!(
@@ -454,7 +465,6 @@ async fn canceled_thread_title_ignores_late_start_and_completion() -> color_eyre
let destination = ThreadTitleDestination::Automatic;
app.pending_thread_titles
.insert((thread_id, destination), cancellation.clone());
app.sync_thread_title_progress();
app.try_submit_active_thread_op_via_app_server(
&mut app_server,
@@ -755,11 +765,8 @@ fn title_agent_message(id: &str, text: &str, phase: Option<MessagePhase>) -> Thr
}
#[tokio::test]
async fn thread_title_progress_clears_failed_requests_and_follows_thread_switches()
-> color_eyre::Result<()> {
async fn failed_thread_title_requests_are_removed() -> color_eyre::Result<()> {
let mut app = make_test_app().await;
app.local_settings.tui.animations = false;
app.chat_widget.local_settings.tui.animations = false;
let mut app_server = crate::start_embedded_app_server_for_picker(&app.config).await?;
let started = app_server.start_thread(&app.config).await?;
let thread_id = started.session.thread_id;
@@ -775,8 +782,6 @@ async fn thread_title_progress_clears_failed_requests_and_follows_thread_switche
),
((thread_id, suggestion), CancellationToken::new()),
]);
app.sync_thread_title_progress();
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
app.on_thread_title_started(
&app_server,
@@ -787,7 +792,6 @@ async fn thread_title_progress_clears_failed_requests_and_follows_thread_switche
Err("startup failed".to_string()),
CancellationToken::new(),
);
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
app.on_thread_title_started(
&app_server,
thread_id,
@@ -798,34 +802,12 @@ async fn thread_title_progress_clears_failed_requests_and_follows_thread_switche
CancellationToken::new(),
);
assert!(app.pending_thread_titles.is_empty());
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
let mut tui = crate::tui::test_support::make_test_tui()?;
app.pending_thread_titles.insert(
(thread_id, ThreadTitleDestination::Automatic),
CancellationToken::new(),
);
let second = app_server.start_thread(&app.config).await?;
let second_id = second.session.thread_id;
app.ensure_thread_channel(second_id)
.store
.lock()
.await
.set_session(second.session, second.turns);
app.select_agent_thread(&mut tui, &mut app_server, second_id)
.await?;
app.render_chat_widget_frame(
&mut tui,
ratatui::layout::Size::new(/*width*/ 120, /*height*/ 30),
)?;
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
app.select_agent_thread(&mut tui, &mut app_server, thread_id)
.await?;
app.render_chat_widget_frame(
&mut tui,
ratatui::layout::Size::new(/*width*/ 120, /*height*/ 30),
)?;
assert!(render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
app.handle_event(
&mut tui,
&mut app_server,
@@ -839,7 +821,6 @@ async fn thread_title_progress_clears_failed_requests_and_follows_thread_switche
)
.await?;
assert!(app.pending_thread_titles.is_empty());
assert!(!render_bottom_popup(&app.chat_widget, /*width*/ 120).contains('⠋'));
app_server.shutdown().await?;
Ok(())
}
-1
View File
@@ -395,7 +395,6 @@ mod status_surfaces;
use self::status_surface_commands::StatusSurfaceOverrides;
mod streaming;
use self::status_surfaces::CachedProjectRootName;
mod thread_title_status;
mod thread_usage;
pub(crate) use self::thread_usage::ThreadUsageOutcome;
mod tool_lifecycle;
@@ -1,7 +0,0 @@
---
source: tui/src/chatwidget/tests/terminal_title.rs
expression: "render_bottom_popup(&chat, 100)"
---
› Ask Codex to do anything
GPT-5.6-Sol default · /tmp/project · ⠋
@@ -126,7 +126,6 @@ pub(super) struct StatusState {
pub(super) program_status_after_turn: ProgramStatus,
pub(super) retry_status_header: Option<String>,
pub(super) pending_status_indicator_restore: bool,
pub(super) thread_title_generation_pending: bool,
}
impl Default for StatusState {
@@ -142,7 +141,6 @@ impl Default for StatusState {
program_status_after_turn: ProgramStatus::Idle,
retry_status_header: None,
pending_status_indicator_restore: false,
thread_title_generation_pending: false,
}
}
}
+7 -33
View File
@@ -25,7 +25,7 @@ use codex_utils_sandbox_summary::summarize_permission_profile;
use super::status_state::TerminalTitleStatusKind;
/// Items shown in the terminal title when the user has not configured a
/// custom selection. Unnamed threads omit the name until generation starts.
/// custom selection. Unnamed threads omit the name until one is available.
pub(super) const DEFAULT_TERMINAL_TITLE_ITEMS: [&str; 3] =
["activity", "thread-name", "project-name"];
@@ -435,27 +435,6 @@ impl ChatWidget {
&self,
selections: &StatusSurfaceSelections,
) -> Option<Duration> {
if self.local_settings.tui.animations
&& self.local_settings.tui.effects.progress
&& self.status_state.thread_title_generation_pending
&& (selections.status_line_items.iter().any(|item| {
matches!(
item,
StatusLineItem::ThreadName
| StatusLineItem::ThreadTitle
| StatusLineItem::SessionId
)
}) || selections.terminal_title_items.iter().any(|item| {
matches!(
item,
TerminalTitleItem::ThreadName
| TerminalTitleItem::Thread
| TerminalTitleItem::SessionId
)
}))
{
return Some(TERMINAL_TITLE_SPINNER_INTERVAL);
}
if self.local_settings.tui.animations
&& self.local_settings.tui.effects.title
&& self.terminal_title_shows_action_required_with_selections(selections)
@@ -746,7 +725,7 @@ impl ChatWidget {
/// Returning `None` means "omit this item for now", not "configuration error". Callers rely on
/// this to keep partially available status lines readable while waiting for session, token, or
/// git metadata.
pub(super) fn status_line_value(&mut self, item: StatusLineItem) -> Option<String> {
pub(super) fn status_line_value_for_item(&mut self, item: StatusLineItem) -> Option<String> {
match item {
StatusLineItem::ModelName => Some(self.model_display_name().to_string()),
StatusLineItem::ModelWithReasoning => Some(self.model_with_reasoning_display_name()),
@@ -945,10 +924,8 @@ impl ChatWidget {
} else {
StatusLineItem::ThreadTitle
};
let value = self
.status_line_value(item)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 48));
self.with_thread_title_progress(value, now)
self.status_line_value_for_item(item)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 48))
}
TerminalTitleItem::GitBranch => self.status_line_branch.as_ref().map(|branch| {
Self::truncate_terminal_title_part(branch.clone(), /*max_chars*/ 32)
@@ -983,12 +960,9 @@ impl ChatWidget {
TerminalTitleItem::EstimatedThreadCost => self
.status_line_value_for_item(StatusLineItem::EstimatedThreadCost)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)),
TerminalTitleItem::SessionId => {
let value = self
.status_line_value(StatusLineItem::SessionId)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32));
self.with_thread_title_progress(value, now)
}
TerminalTitleItem::SessionId => self
.status_line_value_for_item(StatusLineItem::SessionId)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)),
TerminalTitleItem::FastMode => self
.status_line_value_for_item(StatusLineItem::FastMode)
.map(|value| Self::truncate_terminal_title_part(value, /*max_chars*/ 32)),
@@ -0,0 +1,7 @@
---
source: tui/src/chatwidget/tests/status_surface_previews.rs
assertion_line: 43
expression: "snapshot.join(\"\\n\")"
---
colors=true: Line::from_iter([Span::from("Named task").fg(Color::Rgb(242, 205, 205)), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task").fg(Color::Rgb(242, 205, 205))])
colors=false: Line::from_iter([Span::from("Named task").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim()])
@@ -1,8 +0,0 @@
---
source: tui/src/chatwidget/tests/status_surface_previews.rs
expression: "snapshot.join(\"\\n\")"
---
pending=false colors=true: Line::from_iter([Span::from("Named task").fg(Color::Rgb(242, 205, 205)), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task").fg(Color::Rgb(242, 205, 205))])
pending=false colors=false: Line::from_iter([Span::from("Named task").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim()])
pending=true colors=true: Line::from_iter([Span::from("Named task ⠋").fg(Color::Rgb(242, 205, 205)), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task ⠋").fg(Color::Rgb(242, 205, 205))])
pending=true colors=false: Line::from_iter([Span::from("Named task ⠋").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from(" · ").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim(), Span::from("Named task ⠋").fg(Color::Rgb(140, 140, 140)).not_bold().not_dim()])
@@ -0,0 +1,14 @@
---
source: tui/src/chatwidget/tests/terminal_title.rs
assertion_line: 320
expression: "rendered.join(\"\\n\\n\")"
---
name=None
status: | 550e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-446655440000
title: | 550e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-44665...
default title: project
name=Some("Fix login timeout")
status: Fix login timeout | Fix login timeout | 550e8400-e29b-41d4-a716-446655440000
title: Fix login timeout | Fix login timeout | 550e8400-e29b-41d4-a716-44665...
default title: Fix login timeout | project
@@ -1,23 +0,0 @@
---
source: tui/src/chatwidget/tests/terminal_title.rs
expression: "rendered.join(\"\\n\\n\")"
---
name=None, pending=false
status: | 550e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-446655440000
title: | 550e8400-e29b-41d4-a716-446655440000 | 550e8400-e29b-41d4-a716-44665...
default title: project
name=None, pending=true
status: ⠋ | 550e8400-e29b-41d4-a716-446655440000 ⠋ | 550e8400-e29b-41d4-a716-446655440000 ⠋
title: ⠋ | 550e8400-e29b-41d4-a716-446655440000 ⠋ | 550e8400-e29b-41d4-a716-44665... ⠋
default title: ⠋ | project
name=Some("Fix login timeout"), pending=true
status: Fix login timeout ⠋ | Fix login timeout ⠋ | 550e8400-e29b-41d4-a716-446655440000 ⠋
title: Fix login timeout ⠋ | Fix login timeout ⠋ | 550e8400-e29b-41d4-a716-44665... ⠋
default title: Fix login timeout ⠋ | project
name=Some("Fix login timeout"), pending=false
status: Fix login timeout | Fix login timeout | 550e8400-e29b-41d4-a716-446655440000
title: Fix login timeout | Fix login timeout | 550e8400-e29b-41d4-a716-44665...
default title: Fix login timeout | project
@@ -13,7 +13,7 @@ fn line_text(line: Line<'static>) -> String {
}
#[tokio::test]
async fn thread_color_preview_matches_footer_while_auto_naming() {
async fn thread_color_preview_matches_footer() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
crate::terminal_palette::with_test_default_colors(
crate::terminal_probe::DefaultColors {
@@ -23,26 +23,22 @@ async fn thread_color_preview_matches_footer_while_auto_naming() {
|| {
let id = ThreadId::from_u128(/*value*/ 42);
chat.thread_id = Some(id);
chat.local_settings.tui.animations = false;
chat.thread_name = Some("Named task".into());
let items = [StatusLineItem::ThreadName, StatusLineItem::ThreadTitle];
let mut snapshot = Vec::new();
for pending in [false, true] {
chat.set_thread_title_generation_pending(pending);
let preview = chat.status_surface_preview_data();
for use_colors in [true, false] {
let line = preview.status_line_for_items(items, use_colors).unwrap();
let footer = crate::bottom_pane::status_line_from_segments(
items
.into_iter()
.map(|item| (item, chat.status_line_value_for_item(item).unwrap())),
use_colors,
Some(id),
)
.unwrap();
assert_eq!(line, footer);
snapshot.push(format!("pending={pending} colors={use_colors}: {line:?}"));
}
let preview = chat.status_surface_preview_data();
for use_colors in [true, false] {
let line = preview.status_line_for_items(items, use_colors).unwrap();
let footer = crate::bottom_pane::status_line_from_segments(
items
.into_iter()
.map(|item| (item, chat.status_line_value_for_item(item).unwrap())),
use_colors,
Some(id),
)
.unwrap();
assert_eq!(line, footer);
snapshot.push(format!("colors={use_colors}: {line:?}"));
}
insta::assert_snapshot!(snapshot.join("\n"));
},
@@ -14,7 +14,8 @@ async fn daybreak_status_surfaces_follow_the_thread_preference() {
chat.refresh_terminal_title();
values.push(format!(
"{} | {}",
chat.status_line_value(StatusLineItem::Daybreak).unwrap(),
chat.status_line_value_for_item(StatusLineItem::Daybreak)
.unwrap(),
chat.last_terminal_title.as_deref().unwrap()
));
}
@@ -22,7 +23,8 @@ async fn daybreak_status_surfaces_follow_the_thread_preference() {
chat.refresh_terminal_title();
values.push(format!(
"{} | {}",
chat.status_line_value(StatusLineItem::Daybreak).unwrap(),
chat.status_line_value_for_item(StatusLineItem::Daybreak)
.unwrap(),
chat.last_terminal_title.as_deref().unwrap()
));
insta::assert_snapshot!(values.join("\n"), @r"
@@ -281,19 +283,13 @@ async fn terminal_title_activity_indicators_do_not_animate_when_animations_are_d
}
#[tokio::test]
async fn thread_title_progress_renders_names_and_fallbacks_on_both_surfaces() {
async fn thread_identity_renders_names_and_fallbacks_on_both_surfaces() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.thread_id = Some(ThreadId::from_string("550e8400-e29b-41d4-a716-446655440000").unwrap());
chat.local_settings.tui.animations = false;
let mut rendered = Vec::new();
for (name, pending) in [
(None, false),
(None, true),
(Some("Fix login timeout"), true),
(Some("Fix login timeout"), false),
] {
for name in [None, Some("Fix login timeout")] {
chat.thread_name = name.map(str::to_owned);
chat.set_thread_title_generation_pending(pending);
chat.refresh_status_surfaces();
let status = [
StatusLineItem::ThreadName,
@@ -317,14 +313,11 @@ async fn thread_title_progress_renders_names_and_fallbacks_on_both_surfaces() {
.collect::<Vec<_>>()
.join(" | ");
rendered.push(format!(
"name={name:?}, pending={pending}\nstatus: {status}\ntitle: {title}\ndefault title: {}",
"name={name:?}\nstatus: {status}\ntitle: {title}\ndefault title: {}",
chat.last_terminal_title.as_deref().unwrap()
));
}
insta::assert_snapshot!(
"thread_title_generation_status_surfaces",
rendered.join("\n\n")
);
insta::assert_snapshot!("thread_identity_status_surfaces", rendered.join("\n\n"));
chat.thread_name = Some(" ".to_string());
assert_eq!(
chat.status_line_value_for_item(StatusLineItem::ThreadName),
@@ -335,69 +328,3 @@ async fn thread_title_progress_renders_names_and_fallbacks_on_both_surfaces() {
None
);
}
#[tokio::test]
async fn thread_title_progress_animates_when_main_turn_is_idle() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
chat.frame_requester = frame_requester;
chat.local_settings.tui.terminal_title = Some(Vec::new());
chat.local_settings.tui.status_line = Some(vec!["thread-name".to_string()]);
chat.bottom_pane.set_task_running(/*running*/ false);
chat.set_thread_title_generation_pending(/*pending*/ true);
assert!(chat.terminal_title_next_refresh.is_some());
while draw_rx.try_recv().is_ok() {}
let now = Instant::now();
chat.terminal_title_animation_origin = now;
assert_eq!(
chat.terminal_title_value_for_item(TerminalTitleItem::ThreadName, now),
Some("⠋".to_string())
);
assert_eq!(
chat.terminal_title_value_for_item(
TerminalTitleItem::ThreadName,
now + Duration::from_millis(/*millis*/ 100)
),
Some("⠙".to_string())
);
chat.refresh_thread_title_progress_for_time_tick();
assert!(draw_rx.try_recv().is_ok());
chat.local_settings.tui.status_line = Some(Vec::new());
chat.local_settings.tui.terminal_title = Some(vec!["thread-name".to_string()]);
chat.refresh_status_surfaces();
assert!(chat.terminal_title_next_refresh.is_some());
while draw_rx.try_recv().is_ok() {}
chat.refresh_thread_title_progress_for_time_tick();
assert!(draw_rx.try_recv().is_err());
chat.local_settings.tui.animations = false;
chat.refresh_status_surfaces();
assert!(chat.terminal_title_next_refresh.is_none());
assert_eq!(chat.last_terminal_title, Some("⠋".to_string()));
chat.local_settings.tui.animations = true;
chat.set_thread_title_generation_pending(/*pending*/ false);
assert_eq!(chat.last_terminal_title, None);
assert!(chat.terminal_title_next_refresh.is_none());
}
#[tokio::test]
async fn thread_title_progress_preserves_suffix_after_truncation_and_in_default_footer() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.local_settings.tui.animations = false;
chat.set_thread_title_generation_pending(/*pending*/ true);
chat.show_welcome_banner = false;
assert_chatwidget_snapshot!(
"default_footer_generating_thread_title",
normalize_snapshot_paths(render_bottom_popup(&chat, /*width*/ 100))
);
chat.thread_name = Some("Long title ".repeat(/*n*/ 12));
for item in [TerminalTitleItem::ThreadName, TerminalTitleItem::Thread] {
let title = chat
.terminal_title_value_for_item(item, Instant::now())
.unwrap();
assert!(title.ends_with("... ⠋"), "{title}");
assert_eq!(title.chars().count(), 50);
}
}
@@ -1,69 +0,0 @@
//! Title-generation progress on thread identity items in both status surfaces.
//!
//! The app owns requests across thread switches. Only the displayed thread's pending state
//! lives here; the existing terminal-title timer also redraws the footer while it is pending.
use super::ChatWidget;
use super::status_surfaces::TERMINAL_TITLE_SPINNER_FRAMES;
use crate::bottom_pane::StatusLineItem;
use std::time::Instant;
impl ChatWidget {
pub(crate) fn set_thread_title_generation_pending(&mut self, pending: bool) {
if self.status_state.thread_title_generation_pending != pending {
self.status_state.thread_title_generation_pending = pending;
self.refresh_status_surfaces();
self.request_redraw();
}
}
pub(super) fn status_line_value_for_item(&mut self, item: StatusLineItem) -> Option<String> {
let value = self.status_line_value(item);
if matches!(
item,
StatusLineItem::ThreadName | StatusLineItem::ThreadTitle | StatusLineItem::SessionId
) {
self.with_thread_title_progress(value, Instant::now())
} else {
value
}
}
pub(super) fn with_thread_title_progress(
&self,
value: Option<String>,
now: Instant,
) -> Option<String> {
if !self.status_state.thread_title_generation_pending {
return value;
}
let spinner =
if self.local_settings.tui.animations && self.local_settings.tui.effects.progress {
self.terminal_title_spinner_frame_at(now)
} else {
TERMINAL_TITLE_SPINNER_FRAMES[0]
};
Some(match value {
Some(value) => format!("{value} {spinner}"),
None => spinner.to_string(),
})
}
pub(crate) fn refresh_thread_title_progress_for_time_tick(&mut self) {
if self.status_state.thread_title_generation_pending
&& self.local_settings.tui.animations
&& self.local_settings.tui.effects.progress
&& self.configured_status_line_items().iter().any(|item| {
matches!(
item.parse::<StatusLineItem>(),
Ok(StatusLineItem::ThreadName
| StatusLineItem::ThreadTitle
| StatusLineItem::SessionId)
)
})
{
self.refresh_status_line();
self.request_redraw();
}
}
}