diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 87d76909d..adc48c1d1 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -758,73 +758,61 @@ impl App { // Leaving alt-screen may blank the inline viewport; force a redraw either way. tui.frame_requester().schedule_frame(); } - AppEvent::OpenForkPicker => { - match crate::resume_picker::run_fork_picker( - tui, - &self.config.codex_home, - &self.config.model_provider_id, - false, - ) - .await? - { - SessionSelection::Fork(path) => { - let summary = session_summary( - self.chat_widget.token_usage(), - self.chat_widget.thread_id(), - ); - match self - .server - .fork_thread(usize::MAX, self.config.clone(), path.clone()) - .await - { - Ok(forked) => { - self.shutdown_current_thread().await; - let init = crate::chatwidget::ChatWidgetInit { - config: self.config.clone(), - frame_requester: tui.frame_requester(), - app_event_tx: self.app_event_tx.clone(), - initial_prompt: None, - initial_images: Vec::new(), - enhanced_keys_supported: self.enhanced_keys_supported, - auth_manager: self.auth_manager.clone(), - models_manager: self.server.get_models_manager(), - feedback: self.feedback.clone(), - is_first_run: false, - model: Some(self.current_model.clone()), - }; - self.chat_widget = ChatWidget::new_from_existing( - init, - forked.thread, - forked.session_configured, - ); - self.current_model = model_info.slug.clone(); - if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec![ - "To continue this session, run ".into(), - command.cyan(), - ]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); + AppEvent::ForkCurrentSession => { + let summary = + session_summary(self.chat_widget.token_usage(), self.chat_widget.thread_id()); + if let Some(path) = self.chat_widget.rollout_path() { + match self + .server + .fork_thread(usize::MAX, self.config.clone(), path.clone()) + .await + { + Ok(forked) => { + self.shutdown_current_thread().await; + let init = crate::chatwidget::ChatWidgetInit { + config: self.config.clone(), + frame_requester: tui.frame_requester(), + app_event_tx: self.app_event_tx.clone(), + initial_prompt: None, + initial_images: Vec::new(), + enhanced_keys_supported: self.enhanced_keys_supported, + auth_manager: self.auth_manager.clone(), + models_manager: self.server.get_models_manager(), + feedback: self.feedback.clone(), + is_first_run: false, + model: Some(self.current_model.clone()), + }; + self.chat_widget = ChatWidget::new_from_existing( + init, + forked.thread, + forked.session_configured, + ); + self.current_model = model_info.slug.clone(); + if let Some(summary) = summary { + let mut lines: Vec> = + vec![summary.usage_line.clone().into()]; + if let Some(command) = summary.resume_command { + let spans = vec![ + "To continue this session, run ".into(), + command.cyan(), + ]; + lines.push(spans.into()); } - } - Err(err) => { - let path_display = path.display(); - self.chat_widget.add_error_message(format!( - "Failed to fork session from {path_display}: {err}" - )); + self.chat_widget.add_plain_history_lines(lines); } } + Err(err) => { + let path_display = path.display(); + self.chat_widget.add_error_message(format!( + "Failed to fork current session from {path_display}: {err}" + )); + } } - SessionSelection::Exit - | SessionSelection::StartFresh - | SessionSelection::Resume(_) => {} + } else { + self.chat_widget + .add_error_message("Current session is not ready to fork yet.".to_string()); } - // Leaving alt-screen may blank the inline viewport; force a redraw either way. tui.frame_requester().schedule_frame(); } AppEvent::InsertHistoryCell(cell) => { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 785f3ae2c..f34e1d7d5 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -53,8 +53,8 @@ pub(crate) enum AppEvent { /// Open the resume picker inside the running TUI session. OpenResumePicker, - /// Open the fork picker inside the running TUI session. - OpenForkPicker, + /// Fork the current session into a new thread. + ForkCurrentSession, /// Request to exit the application. /// diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3d566356e..d301243ca 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1994,7 +1994,7 @@ impl ChatWidget { self.app_event_tx.send(AppEvent::OpenResumePicker); } SlashCommand::Fork => { - self.app_event_tx.send(AppEvent::OpenForkPicker); + self.app_event_tx.send(AppEvent::ForkCurrentSession); } SlashCommand::Init => { let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 40d30c895..6b216e6d7 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -1532,12 +1532,12 @@ async fn slash_resume_opens_picker() { } #[tokio::test] -async fn slash_fork_opens_picker() { +async fn slash_fork_requests_current_fork() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; chat.dispatch_command(SlashCommand::Fork); - assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker)); + assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession)); } #[tokio::test] diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 0d274f8eb..d7cbd7fc1 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -48,7 +48,7 @@ impl SlashCommand { SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", SlashCommand::Review => "review my current changes and find issues", SlashCommand::Resume => "resume a saved chat", - SlashCommand::Fork => "fork a saved chat", + SlashCommand::Fork => "fork the current chat", // SlashCommand::Undo => "ask Codex to undo a turn", SlashCommand::Quit | SlashCommand::Exit => "exit Codex", SlashCommand::Diff => "show git diff (including untracked files)", diff --git a/codex-rs/tui/tooltips.txt b/codex-rs/tui/tooltips.txt index 3ba7931ff..302a2b1ea 100644 --- a/codex-rs/tui/tooltips.txt +++ b/codex-rs/tui/tooltips.txt @@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation. Run /review to get a code review of your current changes. Use /skills to list available skills or ask Codex to use one. Use /status to see the current model, approvals, and token usage. -Use /fork to branch a saved chat into a new thread. +Use /fork to branch the current chat into a new thread. Use /init to create an AGENTS.md with project-specific guidance. Use /mcp to list configured MCP tools. You can run any shell command from Codex using `!` (e.g. `!ls`) diff --git a/codex-rs/tui2/src/app.rs b/codex-rs/tui2/src/app.rs index cd174fbbb..52bf41641 100644 --- a/codex-rs/tui2/src/app.rs +++ b/codex-rs/tui2/src/app.rs @@ -1531,72 +1531,62 @@ impl App { // Leaving alt-screen may blank the inline viewport; force a redraw either way. tui.frame_requester().schedule_frame(); } - AppEvent::OpenForkPicker => { - match crate::resume_picker::run_fork_picker( - tui, - &self.config.codex_home, - &self.config.model_provider_id, - false, - ) - .await? - { - SessionSelection::Fork(path) => { - let summary = session_summary( - self.chat_widget.token_usage(), - self.chat_widget.conversation_id(), - ); - match self - .server - .fork_thread(usize::MAX, self.config.clone(), path.clone()) - .await - { - Ok(forked) => { - self.shutdown_current_conversation().await; - let init = crate::chatwidget::ChatWidgetInit { - config: self.config.clone(), - frame_requester: tui.frame_requester(), - app_event_tx: self.app_event_tx.clone(), - initial_prompt: None, - initial_images: Vec::new(), - enhanced_keys_supported: self.enhanced_keys_supported, - auth_manager: self.auth_manager.clone(), - models_manager: self.server.get_models_manager(), - feedback: self.feedback.clone(), - is_first_run: false, - model: Some(self.current_model.clone()), - }; - self.chat_widget = ChatWidget::new_from_existing( - init, - forked.thread, - forked.session_configured, - ); - if let Some(summary) = summary { - let mut lines: Vec> = - vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec![ - "To continue this session, run ".into(), - command.cyan(), - ]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); + AppEvent::ForkCurrentSession => { + let summary = session_summary( + self.chat_widget.token_usage(), + self.chat_widget.conversation_id(), + ); + if let Some(path) = self.chat_widget.rollout_path() { + match self + .server + .fork_thread(usize::MAX, self.config.clone(), path.clone()) + .await + { + Ok(forked) => { + self.shutdown_current_conversation().await; + let init = crate::chatwidget::ChatWidgetInit { + config: self.config.clone(), + frame_requester: tui.frame_requester(), + app_event_tx: self.app_event_tx.clone(), + initial_prompt: None, + initial_images: Vec::new(), + enhanced_keys_supported: self.enhanced_keys_supported, + auth_manager: self.auth_manager.clone(), + models_manager: self.server.get_models_manager(), + feedback: self.feedback.clone(), + is_first_run: false, + model: Some(self.current_model.clone()), + }; + self.chat_widget = ChatWidget::new_from_existing( + init, + forked.thread, + forked.session_configured, + ); + if let Some(summary) = summary { + let mut lines: Vec> = + vec![summary.usage_line.clone().into()]; + if let Some(command) = summary.resume_command { + let spans = vec![ + "To continue this session, run ".into(), + command.cyan(), + ]; + lines.push(spans.into()); } - } - Err(err) => { - let path_display = path.display(); - self.chat_widget.add_error_message(format!( - "Failed to fork session from {path_display}: {err}" - )); + self.chat_widget.add_plain_history_lines(lines); } } + Err(err) => { + let path_display = path.display(); + self.chat_widget.add_error_message(format!( + "Failed to fork current session from {path_display}: {err}" + )); + } } - SessionSelection::Exit - | SessionSelection::StartFresh - | SessionSelection::Resume(_) => {} + } else { + self.chat_widget + .add_error_message("Current session is not ready to fork yet.".to_string()); } - // Leaving alt-screen may blank the inline viewport; force a redraw either way. tui.frame_requester().schedule_frame(); } AppEvent::InsertHistoryCell(cell) => { diff --git a/codex-rs/tui2/src/app_event.rs b/codex-rs/tui2/src/app_event.rs index f24ca1e07..b2a70cae1 100644 --- a/codex-rs/tui2/src/app_event.rs +++ b/codex-rs/tui2/src/app_event.rs @@ -47,8 +47,8 @@ pub(crate) enum AppEvent { /// Open the resume picker inside the running TUI session. OpenResumePicker, - /// Open the fork picker inside the running TUI session. - OpenForkPicker, + /// Fork the current session into a new thread. + ForkCurrentSession, /// Request to exit the application. /// diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index 5facd756f..ab40b19c7 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -1769,7 +1769,7 @@ impl ChatWidget { self.app_event_tx.send(AppEvent::OpenResumePicker); } SlashCommand::Fork => { - self.app_event_tx.send(AppEvent::OpenForkPicker); + self.app_event_tx.send(AppEvent::ForkCurrentSession); } SlashCommand::Init => { let init_target = self.config.cwd.join(DEFAULT_PROJECT_DOC_FILENAME); diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index 22871c39d..fa555e597 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -1338,12 +1338,12 @@ async fn slash_resume_opens_picker() { } #[tokio::test] -async fn slash_fork_opens_picker() { +async fn slash_fork_requests_current_fork() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; chat.dispatch_command(SlashCommand::Fork); - assert_matches!(rx.try_recv(), Ok(AppEvent::OpenForkPicker)); + assert_matches!(rx.try_recv(), Ok(AppEvent::ForkCurrentSession)); } #[tokio::test] diff --git a/codex-rs/tui2/src/slash_command.rs b/codex-rs/tui2/src/slash_command.rs index e2d776122..caac37b85 100644 --- a/codex-rs/tui2/src/slash_command.rs +++ b/codex-rs/tui2/src/slash_command.rs @@ -46,7 +46,7 @@ impl SlashCommand { SlashCommand::Compact => "summarize conversation to prevent hitting the context limit", SlashCommand::Review => "review my current changes and find issues", SlashCommand::Resume => "resume a saved chat", - SlashCommand::Fork => "fork a saved chat", + SlashCommand::Fork => "fork the current chat", // SlashCommand::Undo => "ask Codex to undo a turn", SlashCommand::Quit | SlashCommand::Exit => "exit Codex", SlashCommand::Diff => "show git diff (including untracked files)", diff --git a/codex-rs/tui2/tooltips.txt b/codex-rs/tui2/tooltips.txt index 3ba7931ff..302a2b1ea 100644 --- a/codex-rs/tui2/tooltips.txt +++ b/codex-rs/tui2/tooltips.txt @@ -6,7 +6,7 @@ Use /approvals to control when Codex asks for confirmation. Run /review to get a code review of your current changes. Use /skills to list available skills or ask Codex to use one. Use /status to see the current model, approvals, and token usage. -Use /fork to branch a saved chat into a new thread. +Use /fork to branch the current chat into a new thread. Use /init to create an AGENTS.md with project-specific guidance. Use /mcp to list configured MCP tools. You can run any shell command from Codex using `!` (e.g. `!ls`)