mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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:
committed by
copyberry
parent
a309f9206a
commit
8dd6099e26
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+10
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
-7
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+7
@@ -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()])
|
||||
-8
@@ -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()])
|
||||
+14
@@ -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
|
||||
-23
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user