mirror of
https://github.com/openai/codex.git
synced 2026-11-05 16:29:31 +00:00
Avoid redundant TUI redraws while streaming (#34049)
## What changed - Redraw assistant and plan stream tails only when a completed line changes the visible tail. - Cache the first reasoning header and update the status indicator only when its visible state changes, while restoring the cached header if the indicator is recreated. - Keep regular commit ticks able to clear stale stream tails. ## Testing - Add coverage for unchanged partial deltas, changed newline-delimited tails, reasoning-header redraws, recreated status indicators, stale plan tails, and history-line draw scheduling. GitOrigin-RevId: 9016175f97ef2db2edc2fd5ba13a4d731dc2d98c
This commit is contained in:
1 parent
028edf8c1e
commit
35eaf3ffb0
18 files changed
+359
-44
No files matched your search
@@ -179,6 +179,7 @@ impl ChatWidget {
|
||||
newly_installed_marketplace_tab_id: None,
|
||||
interrupts: InterruptManager::new(),
|
||||
reasoning_buffer: String::new(),
|
||||
reasoning_header: None,
|
||||
reasoning_summary_parts: Vec::new(),
|
||||
status_state: StatusState::default(),
|
||||
review: ReviewState::default(),
|
||||
|
||||
@@ -42,6 +42,7 @@ impl ChatWidget {
|
||||
// Submitted is emitted when user submits.
|
||||
// Reset any reasoning header only when we are actually submitting a turn.
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header = None;
|
||||
self.reasoning_summary_parts.clear();
|
||||
self.set_status_header(String::from("Working"));
|
||||
self.submit_user_message(user_message);
|
||||
|
||||
@@ -739,6 +739,7 @@ impl ChatWidget {
|
||||
);
|
||||
if self.is_session_configured() {
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header = None;
|
||||
self.reasoning_summary_parts.clear();
|
||||
self.set_status_header(String::from("Working"));
|
||||
self.submit_user_message(user_message);
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/status_and_layout.rs
|
||||
expression: normalized_backend_snapshot(terminal.backend())
|
||||
---
|
||||
" "
|
||||
"• Checking files (0s • esc to interrupt) · 1 background terminal running · /ps …"
|
||||
" "
|
||||
" "
|
||||
"› Ask Codex to do anything "
|
||||
" "
|
||||
" gpt-5.6-sol default · /tmp/project "
|
||||
@@ -9,14 +9,15 @@ use super::*;
|
||||
impl ChatWidget {
|
||||
/// Update the status indicator header and details.
|
||||
///
|
||||
/// Passing `None` clears any existing details.
|
||||
/// Passing `None` clears any existing details. Returns whether the visible status indicator
|
||||
/// requested a redraw.
|
||||
pub(super) fn set_status(
|
||||
&mut self,
|
||||
header: String,
|
||||
details: Option<String>,
|
||||
details_capitalization: StatusDetailsCapitalization,
|
||||
details_max_lines: usize,
|
||||
) {
|
||||
) -> bool {
|
||||
let details = details
|
||||
.filter(|details| !details.is_empty())
|
||||
.map(|details| {
|
||||
@@ -33,7 +34,7 @@ impl ChatWidget {
|
||||
details: details.clone(),
|
||||
details_max_lines,
|
||||
});
|
||||
self.bottom_pane.update_status(
|
||||
let status_indicator_updated = self.bottom_pane.update_status(
|
||||
header,
|
||||
details,
|
||||
StatusDetailsCapitalization::Preserve,
|
||||
@@ -51,17 +52,19 @@ impl ChatWidget {
|
||||
if title_uses_status {
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
status_indicator_updated
|
||||
}
|
||||
|
||||
/// Convenience wrapper around [`Self::set_status`];
|
||||
/// updates the status indicator header and clears any existing details.
|
||||
pub(super) fn set_status_header(&mut self, header: String) {
|
||||
/// updates the status indicator header and clears any existing details, returning whether the
|
||||
/// visible status indicator requested a redraw.
|
||||
pub(super) fn set_status_header(&mut self, header: String) -> bool {
|
||||
self.set_status(
|
||||
header,
|
||||
/*details*/ None,
|
||||
StatusDetailsCapitalization::CapitalizeFirst,
|
||||
STATUS_DETAILS_DEFAULT_MAX_LINES,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
/// Sets the currently rendered footer status-line value.
|
||||
|
||||
@@ -7,7 +7,10 @@ use super::*;
|
||||
|
||||
impl ChatWidget {
|
||||
pub(super) fn restore_reasoning_status_header(&mut self) {
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
if self.reasoning_header.is_none() {
|
||||
self.reasoning_header = extract_first_bold(&self.reasoning_buffer);
|
||||
}
|
||||
if let Some(header) = self.reasoning_header.clone() {
|
||||
self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;
|
||||
self.set_status_header(header);
|
||||
} else if self.bottom_pane.is_task_running() {
|
||||
@@ -149,8 +152,10 @@ impl ChatWidget {
|
||||
self.app_event_tx.send(AppEvent::StartCommitAnimation);
|
||||
self.run_catch_up_commit_tick();
|
||||
}
|
||||
self.sync_active_stream_tail();
|
||||
self.request_redraw();
|
||||
// Unterminated source is buffered by the controller and cannot change the visible tail.
|
||||
if delta.contains('\n') && self.sync_active_stream_tail() {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_plan_item_completed(&mut self, text: String) {
|
||||
@@ -213,24 +218,41 @@ impl ChatWidget {
|
||||
self.reasoning_buffer.push_str(&delta);
|
||||
|
||||
if self.safety_buffering_is_waiting() {
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if self.unified_exec_wait_streak.is_some() {
|
||||
// Unified exec waiting should take precedence over reasoning-derived status headers.
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(header) = extract_first_bold(&self.reasoning_buffer) {
|
||||
// Update the shimmer header to the extracted reasoning chunk header.
|
||||
self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;
|
||||
self.set_status_header(header);
|
||||
} else {
|
||||
if self.reasoning_header.is_none() {
|
||||
self.reasoning_header = extract_first_bold(&self.reasoning_buffer);
|
||||
}
|
||||
let Some(header) = self.reasoning_header.as_deref() else {
|
||||
// Fallback while we don't yet have a bold header: leave existing header as-is.
|
||||
return;
|
||||
};
|
||||
|
||||
let status = &self.status_state.current_status;
|
||||
if self.status_state.terminal_title_status_kind == TerminalTitleStatusKind::Thinking
|
||||
&& status.header == header
|
||||
&& status.details.is_none()
|
||||
&& status.details_max_lines == STATUS_DETAILS_DEFAULT_MAX_LINES
|
||||
&& self
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.is_none_or(|status| status.header() == header)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the shimmer header to the extracted reasoning chunk header.
|
||||
let header = header.to_string();
|
||||
self.status_state.terminal_title_status_kind = TerminalTitleStatusKind::Thinking;
|
||||
if !self.set_status_header(header) {
|
||||
self.request_redraw();
|
||||
}
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(super) fn on_agent_reasoning_final(&mut self) {
|
||||
@@ -245,6 +267,7 @@ impl ChatWidget {
|
||||
self.add_boxed_history(cell);
|
||||
}
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header = None;
|
||||
self.reasoning_summary_parts.clear();
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -255,6 +278,7 @@ impl ChatWidget {
|
||||
self.reasoning_summary_parts
|
||||
.push(std::mem::take(&mut self.reasoning_buffer));
|
||||
}
|
||||
self.reasoning_header = None;
|
||||
}
|
||||
|
||||
pub(super) fn on_stream_error(&mut self, message: String, additional_details: Option<String>) {
|
||||
@@ -352,7 +376,9 @@ impl ChatWidget {
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.add_boxed_history(cell);
|
||||
}
|
||||
self.sync_active_stream_tail();
|
||||
if scope == CommitTickScope::AnyMode || outcome.has_controller {
|
||||
self.sync_active_stream_tail();
|
||||
}
|
||||
|
||||
if outcome.has_controller && outcome.all_idle {
|
||||
self.maybe_restore_status_indicator_after_stream_idle();
|
||||
@@ -434,8 +460,10 @@ impl ChatWidget {
|
||||
self.app_event_tx.send(AppEvent::StartCommitAnimation);
|
||||
self.run_catch_up_commit_tick();
|
||||
}
|
||||
self.sync_active_stream_tail();
|
||||
self.request_redraw();
|
||||
// Unterminated source is buffered by the controller and cannot change the visible tail.
|
||||
if delta.contains('\n') && self.sync_active_stream_tail() {
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn active_cell_is_stream_tail(&self) -> bool {
|
||||
@@ -450,47 +478,74 @@ impl ChatWidget {
|
||||
&& self.active_cell_is_stream_tail()
|
||||
}
|
||||
|
||||
pub(super) fn sync_active_stream_tail(&mut self) {
|
||||
pub(super) fn sync_active_stream_tail(&mut self) -> bool {
|
||||
if let Some(controller) = self.stream_controller.as_ref() {
|
||||
let tail_lines = controller.current_tail_lines();
|
||||
if tail_lines.is_empty() {
|
||||
self.clear_active_stream_tail();
|
||||
return;
|
||||
return self.clear_active_stream_tail();
|
||||
}
|
||||
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.transcript.active_cell =
|
||||
Some(Box::new(history_cell::StreamingAgentTailCell::new(
|
||||
tail_lines,
|
||||
controller.tail_starts_stream(),
|
||||
)));
|
||||
let cell = history_cell::StreamingAgentTailCell::new(
|
||||
tail_lines,
|
||||
controller.tail_starts_stream(),
|
||||
);
|
||||
if self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.and_then(|active| {
|
||||
active
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::StreamingAgentTailCell>()
|
||||
})
|
||||
.is_some_and(|active| active == &cell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.transcript.active_cell = Some(Box::new(cell));
|
||||
self.bump_active_cell_revision();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(controller) = self.plan_stream_controller.as_ref() {
|
||||
let tail_lines = controller.current_tail_display_lines();
|
||||
if tail_lines.is_empty() {
|
||||
self.clear_active_stream_tail();
|
||||
return;
|
||||
return self.clear_active_stream_tail();
|
||||
}
|
||||
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.transcript.active_cell = Some(Box::new(history_cell::StreamingPlanTailCell::new(
|
||||
let cell = history_cell::StreamingPlanTailCell::new(
|
||||
tail_lines,
|
||||
!controller.tail_starts_stream(),
|
||||
)));
|
||||
);
|
||||
if self
|
||||
.transcript
|
||||
.active_cell
|
||||
.as_ref()
|
||||
.and_then(|active| {
|
||||
active
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::StreamingPlanTailCell>()
|
||||
})
|
||||
.is_some_and(|active| active == &cell)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.transcript.active_cell = Some(Box::new(cell));
|
||||
self.bump_active_cell_revision();
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
self.clear_active_stream_tail();
|
||||
self.clear_active_stream_tail()
|
||||
}
|
||||
|
||||
pub(super) fn clear_active_stream_tail(&mut self) {
|
||||
pub(super) fn clear_active_stream_tail(&mut self) -> bool {
|
||||
if self.active_cell_is_stream_tail() {
|
||||
self.transcript.active_cell = None;
|
||||
self.bump_active_cell_revision();
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,7 @@ pub(super) async fn make_chatwidget_manual(
|
||||
model_override,
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ false,
|
||||
FrameRequester::test_dummy(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -162,6 +163,7 @@ pub(super) async fn make_chatwidget_manual_with_auth(
|
||||
model_override: Option<&str>,
|
||||
has_chatgpt_account: bool,
|
||||
has_codex_backend_auth: bool,
|
||||
frame_requester: FrameRequester,
|
||||
) -> (
|
||||
ChatWidget,
|
||||
tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
@@ -181,7 +183,7 @@ pub(super) async fn make_chatwidget_manual_with_auth(
|
||||
let model_catalog = test_model_catalog(&cfg);
|
||||
let common = ChatWidgetInit {
|
||||
config: cfg,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
frame_requester,
|
||||
app_event_tx,
|
||||
workspace_command_runner: None,
|
||||
initial_user_message: None,
|
||||
|
||||
@@ -1174,6 +1174,28 @@ async fn plan_completion_restores_status_indicator_after_streaming_plan_output()
|
||||
assert_eq!(chat.bottom_pane.is_task_running(), true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unterminated_plan_delta_does_not_redraw_unchanged_stream_tail() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
|
||||
.expect("expected plan collaboration mask");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
chat.on_plan_delta("| Step | Owner |\n".to_string());
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
let revision = chat.transcript.active_cell_revision;
|
||||
|
||||
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
|
||||
chat.frame_requester = frame_requester;
|
||||
chat.on_plan_delta("| partial".to_string());
|
||||
|
||||
assert_eq!(chat.transcript.active_cell_revision, revision);
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn submit_user_message_queues_while_compaction_turn_is_running() {
|
||||
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -1308,8 +1308,10 @@ async fn usage_command_runs_with_backend_auth_without_chatgpt_account_flag() {
|
||||
#[tokio::test]
|
||||
async fn usage_command_runs_with_backend_auth_from_widget_init() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual_with_auth(
|
||||
/*model_override*/ None, /*has_chatgpt_account*/ false,
|
||||
/*model_override*/ None,
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ true,
|
||||
FrameRequester::test_dummy(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -3767,6 +3767,190 @@ async fn deltas_then_same_final_message_are_rendered_snapshot() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unterminated_agent_delta_does_not_redraw_unchanged_stream_tail() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.handle_streaming_delta("| Step | Owner |\n".to_string());
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
let revision = chat.transcript.active_cell_revision;
|
||||
|
||||
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
|
||||
chat.frame_requester = frame_requester;
|
||||
chat.handle_streaming_delta("| partial".to_string());
|
||||
|
||||
assert_eq!(chat.transcript.active_cell_revision, revision);
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn newline_agent_delta_redraws_stream_tail_after_noop_catch_up() {
|
||||
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual_with_auth(
|
||||
/*model_override*/ None,
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ false,
|
||||
frame_requester,
|
||||
)
|
||||
.await;
|
||||
chat.on_task_started();
|
||||
chat.handle_streaming_delta("Earlier line\n".to_string());
|
||||
chat.on_commit_tick();
|
||||
assert!(!chat.bottom_pane.status_indicator_visible());
|
||||
while draw_rx.try_recv().is_ok() {}
|
||||
|
||||
chat.handle_streaming_delta("Intro line\n| Step | Owner |\n".to_string());
|
||||
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
assert!(
|
||||
draw_rx.try_recv().is_ok(),
|
||||
"expected the changed assistant stream tail to schedule a redraw",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn newline_plan_delta_redraws_stream_tail_after_noop_catch_up() {
|
||||
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual_with_auth(
|
||||
/*model_override*/ Some("gpt-5"),
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ false,
|
||||
frame_requester,
|
||||
)
|
||||
.await;
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
|
||||
.expect("expected plan collaboration mask");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
chat.on_task_started();
|
||||
chat.on_plan_delta("Earlier line\n".to_string());
|
||||
chat.on_commit_tick();
|
||||
assert!(!chat.bottom_pane.status_indicator_visible());
|
||||
while draw_rx.try_recv().is_ok() {}
|
||||
|
||||
chat.on_plan_delta("Intro line\n| Step | Owner |\n".to_string());
|
||||
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
assert!(
|
||||
draw_rx.try_recv().is_ok(),
|
||||
"expected the changed plan stream tail to schedule a redraw",
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regular_commit_tick_clears_orphaned_plan_stream_tail() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(Some("gpt-5")).await;
|
||||
chat.set_feature_enabled(Feature::CollaborationModes, /*enabled*/ true);
|
||||
let plan_mask = collaboration_modes::mask_for_kind(chat.model_catalog.as_ref(), ModeKind::Plan)
|
||||
.expect("expected plan collaboration mask");
|
||||
chat.set_collaboration_mask(plan_mask);
|
||||
chat.on_task_started();
|
||||
chat.on_plan_delta("| Step | Owner |\n".to_string());
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
|
||||
chat.on_task_started();
|
||||
assert!(chat.active_cell_is_stream_tail());
|
||||
chat.on_commit_tick();
|
||||
|
||||
assert!(!chat.active_cell_is_stream_tail());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_delta_redraws_only_when_header_becomes_visible() {
|
||||
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.on_agent_reasoning_delta("still looking".to_string());
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
assert_eq!(chat.reasoning_header, None);
|
||||
|
||||
chat.on_agent_reasoning_delta(" **Checking".to_string());
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
|
||||
chat.on_agent_reasoning_delta(" files**".to_string());
|
||||
assert!(draw_rx.try_recv().is_ok());
|
||||
assert_eq!(chat.reasoning_header.as_deref(), Some("Checking files"));
|
||||
|
||||
chat.on_agent_reasoning_delta(" and preparing a response".to_string());
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_delta_does_not_double_schedule_visible_status_redraw() {
|
||||
let (frame_requester, mut draw_rx) = FrameRequester::test_channel();
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual_with_auth(
|
||||
/*model_override*/ None,
|
||||
/*has_chatgpt_account*/ false,
|
||||
/*has_codex_backend_auth*/ false,
|
||||
frame_requester,
|
||||
)
|
||||
.await;
|
||||
chat.on_task_started();
|
||||
assert!(chat.bottom_pane.status_indicator_visible());
|
||||
while draw_rx.try_recv().is_ok() {}
|
||||
|
||||
chat.on_agent_reasoning_delta("**Checking files**".to_string());
|
||||
|
||||
assert_eq!(chat.reasoning_header.as_deref(), Some("Checking files"));
|
||||
assert!(draw_rx.try_recv().is_ok());
|
||||
assert!(matches!(
|
||||
draw_rx.try_recv(),
|
||||
Err(tokio::sync::mpsc::error::TryRecvError::Empty)
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reasoning_delta_restores_recreated_status_indicator_header() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.on_task_started();
|
||||
chat.on_agent_reasoning_delta("**Checking files**".to_string());
|
||||
|
||||
chat.on_agent_message_delta("Preamble line\n".to_string());
|
||||
chat.on_commit_tick();
|
||||
drain_insert_history(&mut rx);
|
||||
assert!(!chat.bottom_pane.status_indicator_visible());
|
||||
|
||||
begin_unified_exec_startup(&mut chat, "call-1", "proc-1", "sleep 2");
|
||||
let status = chat
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.expect("status indicator should be recreated");
|
||||
assert_eq!(status.header(), "Working");
|
||||
|
||||
chat.on_agent_reasoning_delta(" and preparing a response".to_string());
|
||||
|
||||
let status = chat
|
||||
.bottom_pane
|
||||
.status_widget()
|
||||
.expect("status indicator should remain visible");
|
||||
assert_eq!(status.header(), "Checking files");
|
||||
|
||||
let width: u16 = 80;
|
||||
let height = chat.desired_height(width);
|
||||
let mut terminal = ratatui::Terminal::new(ratatui::backend::TestBackend::new(width, height))
|
||||
.expect("create terminal");
|
||||
terminal.set_viewport_area(Rect::new(/*x*/ 0, /*y*/ 0, width, height));
|
||||
terminal
|
||||
.draw(|frame| chat.render(frame.area(), frame.buffer_mut()))
|
||||
.expect("draw restored reasoning status");
|
||||
assert_chatwidget_snapshot!(
|
||||
"reasoning_delta_restores_recreated_status_indicator",
|
||||
normalized_backend_snapshot(terminal.backend())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn user_prompt_submit_app_server_hook_notifications_render_snapshot() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
@@ -82,6 +82,7 @@ impl ChatWidget {
|
||||
}
|
||||
self.reasoning_summary_parts.clear();
|
||||
self.reasoning_buffer.clear();
|
||||
self.reasoning_header = None;
|
||||
self.set_ambient_pet_notification(
|
||||
crate::pets::PetNotificationKind::Running,
|
||||
/*body*/ None,
|
||||
|
||||
Reference in new issue
Block a user