Skip to content

Commit 5ebed7e

Browse files
authored
Merge pull request #254 from thomasjfox/clear-spinner-on-run-finish
Redraw prompt input char when run finished
2 parents 1540d72 + ba8aa82 commit 5ebed7e

3 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/tests/tui_loop_tests.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,43 @@ async fn submit_prompt_streams_response_and_updates_session() {
173173
app.teardown().await;
174174
}
175175

176+
#[tokio::test]
177+
async fn spinner_cleared_when_run_finishes() {
178+
let _guard = acquire();
179+
let (mut app, _model) = headless_app(vec![vec!["done"]]).await;
180+
181+
type_and_submit(&app, "hi").await;
182+
step_until(&mut app, |a| a.is_running()).await;
183+
184+
// Positive control: confirm a spinner was actually painted mid-run, so the
185+
// idle check below is a real "spinner then cleared" regression rather than
186+
// a test that never saw a spinner at all.
187+
let mid = app.backend_output();
188+
assert!(
189+
mid.chars()
190+
.any(|c| ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].contains(&c)),
191+
"a spinner frame should be painted while running"
192+
);
193+
194+
// Regression: Done/Error set is_running false without repainting the bottom
195+
// row, and the 100ms refresh is gated on is_running, so the spinner froze on
196+
// screen until the next keypress. The last bottom draw must now be idle.
197+
step_until(&mut app, |a| !a.is_running()).await;
198+
let snap = app
199+
.last_bottom_snapshot()
200+
.expect("bottom should have been drawn at least once");
201+
assert!(
202+
!snap.is_running,
203+
"last bottom draw should be idle after the run finishes"
204+
);
205+
assert!(
206+
snap.prompt == crate::ui::renderer::PromptSnapshot::Input,
207+
"last bottom draw should show the input prompt after the run finishes"
208+
);
209+
210+
app.teardown().await;
211+
}
212+
176213
#[tokio::test]
177214
async fn queued_input_replays_after_current_run() {
178215
let _guard = acquire();

src/ui/app.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,11 @@ impl<'a> App<'a> {
537537
self.renderer.is_scrolling()
538538
}
539539

540+
#[cfg(test)]
541+
pub(crate) fn last_bottom_snapshot(&self) -> Option<&crate::ui::renderer::BottomSnapshot> {
542+
self.renderer.last_bottom_snapshot()
543+
}
544+
540545
/// All feed lines (wrapped to 80 cols) joined with newlines.
541546
#[cfg(test)]
542547
pub(crate) fn feed_text(&self) -> String {
@@ -1030,6 +1035,9 @@ impl<'a> App<'a> {
10301035
.await?;
10311036

10321037
self.finalize_turn(turn_errored).await?;
1038+
if !self.run.is_running {
1039+
self.refresh()?;
1040+
}
10331041
Ok(())
10341042
}
10351043

src/ui/renderer.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,21 @@ impl Renderer {
426426
self.backend.captured().unwrap_or_default()
427427
}
428428

429+
/// Test helper: snapshot of everything the last successful `draw_bottom`
430+
/// painted (input area + statusline), or `None` before the first draw.
431+
///
432+
/// This is the renderer's recorded *draw intent*, not the raw byte log, so
433+
/// it reflects the bottom row regardless of later chat writes that bury the
434+
/// bottom's bytes under newer ones in the append-only `FakeBackend`. That
435+
/// makes it the right thing to assert against when checking whether the
436+
/// bottom is left idle (or a spinner frozen) after a run ends: a byte-scan
437+
/// helper cannot observe a frozen spinner the chat later overwrote in the
438+
/// tail, but `last_bottom_snapshot().is_running` records the truth.
439+
#[cfg(test)]
440+
pub(crate) fn last_bottom_snapshot(&self) -> Option<&BottomSnapshot> {
441+
self.last_bottom_snapshot.as_ref()
442+
}
443+
429444
/// Whether the renderer drives a headless test backend instead of a real
430445
/// terminal. Used to skip code paths that bypass the backend abstraction
431446
/// and write straight to stdout (e.g. the pickers), which break on CI

0 commit comments

Comments
 (0)