From 35f653cc29ee532ab24e3fb855d2c9767d0e65ab Mon Sep 17 00:00:00 2001 From: Kevin Liu Date: Thu, 22 Jan 2026 14:01:59 -0800 Subject: [PATCH] up --- codex-rs/cli/src/main.rs | 3 ++ codex-rs/tui/src/app.rs | 6 +++ codex-rs/tui/src/chatwidget.rs | 61 ++++++++++++---------------- codex-rs/tui/src/chatwidget/tests.rs | 3 +- codex-rs/tui/src/cli.rs | 4 ++ codex-rs/tui/src/lib.rs | 2 + codex-rs/tui/src/slash_command.rs | 4 +- 7 files changed, 44 insertions(+), 39 deletions(-) diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 1cc4fcdaa7..6dfadf6a87 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -998,6 +998,9 @@ fn merge_interactive_cli_flags(interactive: &mut TuiCli, subcommand_cli: TuiCli) if subcommand_cli.full_auto { interactive.full_auto = true; } + if subcommand_cli.immortal { + interactive.immortal = true; + } if subcommand_cli.dangerously_bypass_approvals_and_sandbox { interactive.dangerously_bypass_approvals_and_sandbox = true; } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 6294df8347..f94ed80c38 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -610,6 +610,7 @@ impl App { model: Some(self.chat_widget.current_model().to_string()), status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(), otel_manager: self.otel_manager.clone(), + immortality_enable_after_first_turn: false, } } @@ -930,6 +931,7 @@ impl App { active_profile: Option, initial_prompt: Option, initial_images: Vec, + immortal: bool, session_selection: SessionSelection, feedback: codex_feedback::CodexFeedback, is_first_run: bool, @@ -1030,6 +1032,7 @@ impl App { model: Some(model.clone()), status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), + immortality_enable_after_first_turn: immortal, }; ChatWidget::new(init, thread_manager.clone()) } @@ -1060,6 +1063,7 @@ impl App { model: config.model.clone(), status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), + immortality_enable_after_first_turn: immortal, }; ChatWidget::new_from_existing(init, resumed.thread, resumed.session_configured) } @@ -1091,6 +1095,7 @@ impl App { model: config.model.clone(), status_line_invalid_items_warned: status_line_invalid_items_warned.clone(), otel_manager: otel_manager.clone(), + immortality_enable_after_first_turn: immortal, }; ChatWidget::new_from_existing(init, forked.thread, forked.session_configured) } @@ -1332,6 +1337,7 @@ impl App { model: Some(model), status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(), otel_manager: self.otel_manager.clone(), + immortality_enable_after_first_turn: false, }; self.chat_widget = ChatWidget::new(init, self.server.clone()); self.reset_thread_event_state(); diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index cd96a2b221..8d22b81162 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -147,7 +147,7 @@ use tokio::task::JoinHandle; use tracing::debug; use tracing::warn; -const IMMORTALITY_FOLLOWUP_PROMPT: &str = "Write the next user message to keep the conversation going. Speak as the user in first person. Be concise and specific. Output only the user message with no quotes or extra commentary."; +const IMMORTALITY_FOLLOWUP_PROMPT: &str = "Please keep going. Work as autonomously as possible. Ask the user for clarification, but don't block on their response. Worst case, if you have nothing useful to do, poll every 30s for a response from the user for 5 minutes and then end your current turn."; const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading"; const PLAN_IMPLEMENTATION_TITLE: &str = "Implement this plan?"; @@ -419,6 +419,7 @@ pub(crate) struct ChatWidgetInit { // Shared latch so we only warn once about invalid status-line item IDs. pub(crate) status_line_invalid_items_warned: Arc, pub(crate) otel_manager: OtelManager, + pub(crate) immortality_enable_after_first_turn: bool, } #[derive(Default)] @@ -581,10 +582,10 @@ pub(crate) struct ChatWidget { is_review_mode: bool, // Snapshot of token usage to restore after review mode exits. pre_review_token_info: Option>, - /// Auto-continue by submitting "Keep going" after each completed turn. + /// Auto-continue by submitting a follow-up after each completed turn. immortality_mode: bool, - /// True when the auto-followup generation turn is in-flight. - immortality_generation_inflight: bool, + /// Enable immortality after the next completed turn (used by --immortal). + immortality_enable_after_first_turn: bool, // Whether the next streamed assistant content should be preceded by a final message separator. // // This is set whenever we insert a visible history cell that conceptually belongs to a turn. @@ -1370,9 +1371,20 @@ impl ChatWidget { if !from_replay { self.saw_plan_item_this_turn = false; } + + if self.immortality_enable_after_first_turn && !self.is_review_mode { + self.immortality_enable_after_first_turn = false; + if !self.immortality_mode { + self.immortality_mode = true; + self.add_info_message( + "Immortality mode enabled (from --immortal).".to_string(), + Some("Press Ctrl+C to interrupt.".to_string()), + ); + } + } // If there is a queued user message, send exactly one now to begin the next turn. self.maybe_send_next_queued_input(); - self.handle_immortality_followup(last_agent_message.as_deref()); + self.maybe_send_immortality_prompt(); // Emit a notification when the turn completes (suppressed if focused). self.notify(Notification::AgentTurnComplete { response: last_agent_message.unwrap_or_default(), @@ -1701,7 +1713,7 @@ impl ChatWidget { if reason == TurnAbortReason::Interrupted && self.immortality_mode { self.immortality_mode = false; - self.immortality_generation_inflight = false; + self.immortality_enable_after_first_turn = false; self.add_info_message( "Immortality mode disabled after interrupt.".to_string(), None, @@ -2567,6 +2579,7 @@ impl ChatWidget { model, status_line_invalid_items_warned, otel_manager, + immortality_enable_after_first_turn, } = common; let model = model.filter(|m| !m.trim().is_empty()); let mut config = config; @@ -2830,7 +2843,7 @@ impl ChatWidget { is_review_mode: false, pre_review_token_info: None, immortality_mode: false, - immortality_generation_inflight: false, + immortality_enable_after_first_turn, needs_final_message_separator: false, had_work_activity: false, last_separator_elapsed_secs: None, @@ -2887,6 +2900,8 @@ impl ChatWidget { model, status_line_invalid_items_warned, otel_manager, + immortality_enable_after_first_turn, + .. } = common; let model = model.filter(|m| !m.trim().is_empty()); let mut rng = rand::rng(); @@ -2981,7 +2996,7 @@ impl ChatWidget { is_review_mode: false, pre_review_token_info: None, immortality_mode: false, - immortality_generation_inflight: false, + immortality_enable_after_first_turn, needs_final_message_separator: false, had_work_activity: false, saw_plan_update_this_turn: false, @@ -3419,9 +3434,6 @@ impl ChatWidget { } SlashCommand::Immortality => { self.immortality_mode = !self.immortality_mode; - if !self.immortality_mode { - self.immortality_generation_inflight = false; - } let status = if self.immortality_mode { "enabled" } else { @@ -3429,7 +3441,7 @@ impl ChatWidget { }; self.add_info_message( format!( - "Immortality mode {status}. I will ask the model to generate the next user followup after each turn until interrupted.", + "Immortality mode {status}. I will keep the agent running after each turn until interrupted.", ), Some("Press Ctrl+C to interrupt.".to_string()), ); @@ -4235,35 +4247,14 @@ impl ChatWidget { self.refresh_queued_user_messages(); } - fn handle_immortality_followup(&mut self, last_agent_message: Option<&str>) { + fn maybe_send_immortality_prompt(&mut self) { if !self.immortality_mode || self.is_review_mode { return; } if self.bottom_pane.is_task_running() || !self.queued_user_messages.is_empty() { return; } - if self.immortality_generation_inflight { - self.immortality_generation_inflight = false; - let followup = last_agent_message.unwrap_or_default().trim(); - if followup.is_empty() { - self.add_info_message( - "Immortality mode: model returned an empty followup; retrying.".to_string(), - None, - ); - self.maybe_send_immortality_prompt(); - return; - } - self.submit_user_message(UserMessage::from(followup)); - return; - } - self.maybe_send_immortality_prompt(); - } - - fn maybe_send_immortality_prompt(&mut self) { - if self.immortality_mode && !self.immortality_generation_inflight { - self.immortality_generation_inflight = true; - self.submit_user_message(UserMessage::from(IMMORTALITY_FOLLOWUP_PROMPT)); - } + self.submit_user_message(UserMessage::from(IMMORTALITY_FOLLOWUP_PROMPT)); } /// Rebuild and update the queued user messages from the current queue. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 5db5a3e604..69bc89d78e 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -974,6 +974,7 @@ async fn helpers_are_available_and_do_not_panic() { model: Some(resolved_model), status_line_invalid_items_warned: Arc::new(AtomicBool::new(false)), otel_manager, + immortality_enable_after_first_turn: false, }; let mut w = ChatWidget::new(init, thread_manager); // Basic construction sanity. @@ -1094,7 +1095,7 @@ async fn make_chatwidget_manual( is_review_mode: false, pre_review_token_info: None, immortality_mode: false, - immortality_generation_inflight: false, + immortality_enable_after_first_turn: false, needs_final_message_separator: false, had_work_activity: false, saw_plan_update_this_turn: false, diff --git a/codex-rs/tui/src/cli.rs b/codex-rs/tui/src/cli.rs index e6880437e6..9931199c39 100644 --- a/codex-rs/tui/src/cli.rs +++ b/codex-rs/tui/src/cli.rs @@ -110,6 +110,10 @@ pub struct Cli { #[arg(long = "no-alt-screen", default_value_t = false)] pub no_alt_screen: bool, + /// Enable immortality mode after the first user turn. + #[arg(long = "immortal", default_value_t = false)] + pub immortal: bool, + #[clap(skip)] pub config_overrides: CliConfigOverrides, } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 9cb7bee210..53eb71268b 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -688,6 +688,7 @@ async fn run_ratatui_app( prompt, images, no_alt_screen, + immortal, .. } = cli; @@ -703,6 +704,7 @@ async fn run_ratatui_app( active_profile, prompt, images, + immortal, session_selection, feedback, should_show_trust_screen, // Proxy to: is it a first run in this directory? diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 0e530ebf75..00c28a8d44 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -57,9 +57,7 @@ impl SlashCommand { SlashCommand::New => "start a new chat during a conversation", SlashCommand::Init => "create an AGENTS.md file with instructions for Codex", SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", - SlashCommand::Immortality => { - "auto-continue by generating the next user message each turn" - } + SlashCommand::Immortality => "auto-continue after each turn", SlashCommand::Review => "review my current changes and find issues", SlashCommand::Rename => "rename the current thread", SlashCommand::Resume => "resume a saved chat",