From bed3ed7e3daa1059b554a26b62a3b56ea92c057b Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Sat, 30 May 2026 18:15:21 -0300 Subject: [PATCH] fix(tui): order transient token cards after active cells --- codex-rs/tui/src/chatwidget.rs | 2 + codex-rs/tui/src/chatwidget/rendering.rs | 10 +-- .../src/chatwidget/tests/slash_commands.rs | 64 +++++++++++++++++++ codex-rs/tui/src/chatwidget/tokens.rs | 4 +- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 111972d3ac..7db4e94644 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1177,6 +1177,7 @@ impl ChatWidget { if let Some(active) = self.transcript.active_cell.take() { self.transcript.needs_final_message_separator = true; self.app_event_tx.send(AppEvent::InsertHistoryCell(active)); + self.request_completed_token_activity_output_insertion(); } } @@ -1388,6 +1389,7 @@ impl ChatWidget { tool.mark_failed(); } self.add_boxed_history(cell); + self.request_completed_token_activity_output_insertion(); } } diff --git a/codex-rs/tui/src/chatwidget/rendering.rs b/codex-rs/tui/src/chatwidget/rendering.rs index 37af21a012..eab7fd5b93 100644 --- a/codex-rs/tui/src/chatwidget/rendering.rs +++ b/codex-rs/tui/src/chatwidget/rendering.rs @@ -23,11 +23,11 @@ impl ChatWidget { } _ => RenderableItem::Owned(Box::new(())), }; - let mut flex = FlexRenderable::new(); - flex.push(/*flex*/ 1, active_cell_renderable); - flex.push(/*flex*/ 0, active_hook_cell_renderable); + let mut content = FlexRenderable::new(); + content.push(/*flex*/ 0, active_cell_renderable); + content.push(/*flex*/ 0, active_hook_cell_renderable); if let Some(cell) = self.pending_token_activity_output() { - flex.push( + content.push( /*flex*/ 1, RenderableItem::Owned(Box::new(TranscriptAreaRenderable { child: cell, @@ -36,6 +36,8 @@ impl ChatWidget { })), ); } + let mut flex = FlexRenderable::new(); + flex.push(/*flex*/ 1, RenderableItem::Owned(Box::new(content))); flex.push( /*flex*/ 0, RenderableItem::Owned(Box::new(BottomPaneComposerReserveRenderable { diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 6f8fee6b0d..577b65f1a3 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1298,6 +1298,36 @@ async fn completed_token_activity_refresh_waits_for_active_stream() { assert!(chat.take_completed_token_activity_output().is_some()); } +#[tokio::test] +async fn completed_token_activity_refresh_waits_for_active_history_cell() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + set_chatgpt_auth(&mut chat); + + chat.dispatch_command(SlashCommand::Tokens); + let request_id = match rx.try_recv() { + Ok(AppEvent::RefreshTokenActivity { request_id }) => request_id, + other => panic!("expected token activity refresh request, got {other:?}"), + }; + chat.transcript.active_cell = Some(Box::new(PlainHistoryCell::new(vec![Line::from( + "active tool", + )]))); + assert!( + chat.finish_token_activity_refresh( + request_id, + Err("token activity unavailable".to_string()), + ) + ); + assert!(chat.token_activity_history_insertion_blocked()); + + chat.flush_active_cell(); + + assert_matches!(rx.try_recv(), Ok(AppEvent::InsertHistoryCell(_))); + assert_matches!( + rx.try_recv(), + Ok(AppEvent::CommitCompletedTokenActivityOutput) + ); +} + #[tokio::test] async fn completed_token_activity_refresh_retries_after_plan_item_completion() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; @@ -1330,6 +1360,40 @@ async fn completed_token_activity_refresh_retries_after_plan_item_completion() { ); } +#[tokio::test] +async fn pending_token_activity_refresh_keeps_composer_visible_in_short_viewport() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + set_chatgpt_auth(&mut chat); + chat.transcript.active_cell = Some(Box::new(PlainHistoryCell::new( + std::iter::repeat_n(Line::from("active output"), /*n*/ 20).collect(), + ))); + + chat.dispatch_command(SlashCommand::Tokens); + assert_matches!( + rx.try_recv(), + Ok(AppEvent::RefreshTokenActivity { .. }), + "expected token activity refresh request" + ); + + let width: u16 = 80; + let height: u16 = 8; + let backend = VT100Backend::new(width, height); + let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal"); + term.set_viewport_area(Rect::new(/*x*/ 0, /*y*/ 0, width, height)); + term.draw(|f| { + chat.render(f.area(), f.buffer_mut()); + }) + .unwrap(); + + assert!( + term.backend() + .vt100() + .screen() + .contents() + .contains("Ask Codex to do anything") + ); +} + #[tokio::test] async fn repeated_token_activity_refreshes_keep_only_latest_card() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tokens.rs b/codex-rs/tui/src/chatwidget/tokens.rs index 5eb0e34ff0..ca0412ac38 100644 --- a/codex-rs/tui/src/chatwidget/tokens.rs +++ b/codex-rs/tui/src/chatwidget/tokens.rs @@ -685,7 +685,9 @@ impl ChatWidget { } pub(crate) fn token_activity_history_insertion_blocked(&self) -> bool { - self.stream_controller.is_some() || self.plan_stream_controller.is_some() + self.stream_controller.is_some() + || self.plan_stream_controller.is_some() + || self.transcript.active_cell.is_some() } pub(crate) fn take_completed_token_activity_output(&mut self) -> Option {