Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions codex-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5411,6 +5411,12 @@ impl ChatWidget {
let should_submit_now =
self.is_session_configured() && !self.is_plan_streaming_in_tui();
if should_submit_now {
if self.only_user_shell_commands_running()
&& !user_message.text.starts_with('!')
{
self.queue_user_message(user_message);
return;
}
// Submitted is emitted when user submits.
// Reset any reasoning header only when we are actually submitting a turn.
self.reasoning_buffer.clear();
Expand Down Expand Up @@ -7476,6 +7482,15 @@ impl ChatWidget {
self.user_turn_pending_start || self.bottom_pane.is_task_running()
}

fn only_user_shell_commands_running(&self) -> bool {
self.agent_turn_running
&& !self.running_commands.is_empty()
&& self
.running_commands
.values()
.all(|command| command.source == ExecCommandSource::UserShell)
}

/// Rebuild and update the bottom-pane pending-input preview.
fn refresh_pending_input_preview(&mut self) {
let queued_messages: Vec<String> = self
Expand Down
52 changes: 52 additions & 0 deletions codex-rs/tui/src/chatwidget/tests/exec_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1036,6 +1036,58 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() {
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
}

#[tokio::test]
async fn user_message_during_user_shell_command_is_queued_not_steered() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.thread_id = Some(ThreadId::new());
chat.handle_codex_event(Event {
id: "turn-start".into(),
msg: EventMsg::TurnStarted(TurnStartedEvent {
turn_id: "turn-1".to_string(),
started_at: None,
model_context_window: None,
collaboration_mode_kind: ModeKind::Default,
}),
});
let begin = begin_exec_with_source(
&mut chat,
"user-shell-sleep",
"sleep 10",
ExecCommandSource::UserShell,
);

assert!(chat.only_user_shell_commands_running());
chat.bottom_pane
.set_composer_text("hi".to_string(), Vec::new(), Vec::new());
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));

assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
assert_eq!(chat.queued_user_message_texts(), vec!["hi".to_string()]);

end_exec(&mut chat, begin, "", "", /*exit_code*/ 0);
chat.handle_codex_event(Event {
id: "turn-complete".into(),
msg: EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: "turn-1".to_string(),
last_agent_message: Some("done".to_string()),
completed_at: None,
duration_ms: None,
}),
});

match next_submit_op(&mut op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "hi".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected queued user message after shell completion, got {other:?}"),
}
assert!(chat.queued_user_messages.is_empty());
}

#[tokio::test]
async fn disabled_slash_command_while_task_running_snapshot() {
// Build a chat widget and simulate an active task
Expand Down
Loading