@@ -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]
177214async fn queued_input_replays_after_current_run ( ) {
178215 let _guard = acquire ( ) ;
0 commit comments