From d42056091aded7feb1d88ac7e83972108b2aa478 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Wed, 30 Sep 2026 07:05:37 +0000 Subject: [PATCH] Add a fork shortcut to the TUI command center (#49517) ## What changed - Fork the selected conversation and open the new session with `f`, configurable through `agents.fork` in the TUI keymap. - Move the default search shortcuts to `F3` and `/`, preserving unshadowed defaults when custom bindings conflict. - Keep queued input on the source conversation while attaching and forking, and require closing an open side conversation before forking another task. - Expose the fork action in command-center help and the keymap picker. ## Testing Add regression coverage for queued-input preservation, side-conversation protection, configurable fork shortcuts, metadata editing, disconnected actions, and search fallback bindings. Update help and keymap snapshots. GitOrigin-RevId: b11ae21ef6a2053609b31ce97c646104f18806a3 --- codex-rs/config/src/tui_keymap.rs | 2 + codex-rs/core/config.schema.json | 10 ++ codex-rs/tui/src/app/agent_center/hints.rs | 2 + codex-rs/tui/src/app/agent_center_tests.rs | 30 ++-- codex-rs/tui/src/app/agents_overview.rs | 2 +- .../src/app/agents_overview_actions_tests.rs | 34 +++- codex-rs/tui/src/app/agents_overview_tests.rs | 158 +++++++++++++++++- codex-rs/tui/src/app/agents_overview_view.rs | 8 + codex-rs/tui/src/app/event_dispatch.rs | 53 +++++- codex-rs/tui/src/app/session_lifecycle.rs | 1 + ...mmand_center__live_center_custom_help.snap | 10 +- ...d_center__live_center_help_one_column.snap | 4 +- ...enter__live_center_help_three_columns.snap | 10 +- ..._center__live_center_help_two_columns.snap | 10 +- ..._center_navigation_and_complete_hints.snap | 2 +- ...k_preserves_an_open_side_conversation.snap | 12 ++ .../tui/src/app/tests/disconnect_tests.rs | 10 +- codex-rs/tui/src/app/tests/key_chords.rs | 2 +- codex-rs/tui/src/app_event.rs | 8 + codex-rs/tui/src/chatwidget/input_flow.rs | 1 + codex-rs/tui/src/keymap.rs | 57 ++++--- codex-rs/tui/src/keymap/bindings.rs | 1 + codex-rs/tui/src/keymap/conflict_tests.rs | 16 ++ codex-rs/tui/src/keymap_setup/actions.rs | 2 + ...__tests__keymap_picker_agents_actions.snap | 3 +- ...ap_setup__tests__keymap_picker_custom.snap | 2 +- ...ests__keymap_picker_fast_mode_enabled.snap | 2 +- ...p__tests__keymap_picker_first_actions.snap | 4 +- ...up__tests__keymap_picker_global_chord.snap | 2 +- ...ap_setup__tests__keymap_picker_narrow.snap | 2 +- ...ymap_setup__tests__keymap_picker_wide.snap | 2 +- ...setup__tests__keymap_question_actions.snap | 2 +- 32 files changed, 378 insertions(+), 86 deletions(-) create mode 100644 codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_fork_preserves_an_open_side_conversation.snap diff --git a/codex-rs/config/src/tui_keymap.rs b/codex-rs/config/src/tui_keymap.rs index 674b387702..d840967852 100644 --- a/codex-rs/config/src/tui_keymap.rs +++ b/codex-rs/config/src/tui_keymap.rs @@ -449,6 +449,8 @@ pub struct TuiAgentsKeymap { pub new_task: Option, /// Open a new session in a worktree from the project default branch. pub new_worktree: Option, + /// Fork the selected conversation and open the new session. + pub fork: Option, /// Rename the selected task. pub rename: Option, /// Stop the selected running task. diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 6aaa39c3f5..bcc4a8ecce 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -4435,6 +4435,7 @@ "agents": { "archive": null, "delete": null, + "fork": null, "hide": null, "new_task": null, "new_worktree": null, @@ -4789,6 +4790,14 @@ ], "description": "Permanently delete the selected task and its child agents after confirmation." }, + "fork": { + "allOf": [ + { + "$ref": "#/definitions/KeybindingsSpec" + } + ], + "description": "Fork the selected conversation and open the new session." + }, "hide": { "allOf": [ { @@ -5373,6 +5382,7 @@ "default": { "archive": null, "delete": null, + "fork": null, "hide": null, "new_task": null, "new_worktree": null, diff --git a/codex-rs/tui/src/app/agent_center/hints.rs b/codex-rs/tui/src/app/agent_center/hints.rs index 3a26f54746..5a4437dab9 100644 --- a/codex-rs/tui/src/app/agent_center/hints.rs +++ b/codex-rs/tui/src/app/agent_center/hints.rs @@ -29,6 +29,7 @@ impl AgentsOverviewView { &self.agents_keymap.search, &self.agents_keymap.new_task, &self.agents_keymap.new_worktree, + &self.agents_keymap.fork, &self.agents_keymap.rename, &self.agents_keymap.stop, &self.agents_keymap.archive, @@ -82,6 +83,7 @@ impl AgentsOverviewView { "New worktree", ), ("resume", &self.agents_keymap.resume, "Resume"), + ("fork", &self.agents_keymap.fork, "Fork"), ("rename", &self.agents_keymap.rename, "Rename"), ("stop", &self.agents_keymap.stop, "Stop"), ("archive", &self.agents_keymap.archive, "Archive"), diff --git a/codex-rs/tui/src/app/agent_center_tests.rs b/codex-rs/tui/src/app/agent_center_tests.rs index 3026c160cc..67e36e4668 100644 --- a/codex-rs/tui/src/app/agent_center_tests.rs +++ b/codex-rs/tui/src/app/agent_center_tests.rs @@ -137,7 +137,7 @@ async fn live_center_pages_visible_rows_without_wrapping() { .lock() .unwrap() .connection_notice = Some("Reconnecting…"); - view.handle_key_event(KeyCode::Char('f').into()); + view.handle_key_event(KeyCode::Char('/').into()); view.handle_paste("Paging".into()); } screen(&view, /*width*/ 110, /*height*/ 18); @@ -185,7 +185,7 @@ async fn live_center_metadata_clips_at_grapheme_boundaries() { /*selected_thread_id*/ None, ); let input = format!("{}日本語 e\u{301} 👩\u{200d}💻", "界".repeat(/*n*/ 100_000)); - for (key, label) in [('r', "Rename › "), ('f', "Search › ")] { + for (key, label) in [('r', "Rename › "), ('/', "Search › ")] { view.handle_key_event(KeyCode::Char(key).into()); view.handle_paste(input.clone()); let rendered = screen(&view, /*width*/ 40, /*height*/ 18); @@ -340,7 +340,7 @@ async fn live_center_fixed_shortcuts_yield_to_configured_actions() { let mut app = make_test_app().await; let config: TuiKeymap = toml::from_str("[list]\ncancel = 'q'").unwrap(); app.keymap = RuntimeKeymap::from_config(&config).unwrap(); - for editor_key in ['f', 'r'] { + for editor_key in ['/', 'r'] { let mut view = app.agents_overview_view( vec![overview_thread( ThreadId::from_u128(/*value*/ 42), @@ -411,7 +411,7 @@ async fn live_center_fixed_shortcuts_yield_to_configured_actions() { .lock() .unwrap() .server_version_notice = None; - view.handle_key_event(KeyCode::Char('f').into()); + view.handle_key_event(KeyCode::Char('/').into()); assert!(!screen(&view, /*width*/ 100, /*height*/ 24).contains("enter open")); } view.handle_key_event(key); @@ -479,14 +479,16 @@ async fn live_center_search_row_appears_only_while_editing() { ["Tasks", "Status", "Updated"].map(|label| header[text.find(label).unwrap()].style()); assert_eq!(styles, [styles[0]; 3]); let idle = screen(&view, area.width, area.height); - view.handle_key_event(KeyCode::Char('f').into()); - view.handle_paste("find".into()); - insta::assert_snapshot!( - "live_center_search_active", - screen(&view, area.width, area.height) - ); - view.handle_key_event(KeyCode::Esc.into()); - assert_eq!(screen(&view, area.width, area.height), idle); + for key in [KeyCode::Char('/'), KeyCode::F(3)] { + view.handle_key_event(key.into()); + view.handle_paste("find".into()); + insta::assert_snapshot!( + "live_center_search_active", + screen(&view, area.width, area.height) + ); + view.handle_key_event(KeyCode::Esc.into()); + assert_eq!(screen(&view, area.width, area.height), idle); + } } #[tokio::test] @@ -503,7 +505,7 @@ async fn backspace_edits_search_and_rename_without_deleting_tasks() { )], /*selected_thread_id*/ None, ); - for key in ['f', 'r'] { + for key in ['/', 'r'] { view.handle_key_event(KeyCode::Char(key).into()); let initial = screen(&view, /*width*/ 88, /*height*/ 16); view.handle_paste("!".into()); @@ -583,7 +585,7 @@ async fn live_center_show_more_navigation_search_and_loading() { state.loading = false; state.refresh_failed = true; } - view.handle_key_event(KeyCode::Char('f').into()); + view.handle_key_event(KeyCode::Char('/').into()); view.handle_paste("Older task".into()); insta::assert_snapshot!( "live_center_show_more_search_retry", diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 6d6f80c923..f7639dd86b 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -862,7 +862,7 @@ impl App { self.chat_widget.maybe_send_next_queued_input(); } } - if !read_only && !is_new_session { + if !read_only && !is_new_session && !self.chat_widget.fork_in_progress { self.maybe_prompt_resume_paused_goal_after_resume(app_server, root_thread_id) .await; } diff --git a/codex-rs/tui/src/app/agents_overview_actions_tests.rs b/codex-rs/tui/src/app/agents_overview_actions_tests.rs index f7269edccc..c0f4d992c7 100644 --- a/codex-rs/tui/src/app/agents_overview_actions_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_actions_tests.rs @@ -60,6 +60,7 @@ async fn lifecycle_shortcuts_target_filtered_task_in_any_state() { keymap.agents.archive = Some(KeybindingsSpec::One(KeybindingSpec("f5".into()))); keymap.agents.delete = Some(KeybindingsSpec::One(KeybindingSpec("f6".into()))); keymap.agents.hide = Some(KeybindingsSpec::One(KeybindingSpec("f7".into()))); + keymap.agents.fork = Some(KeybindingsSpec::One(KeybindingSpec("f8".into()))); app.keymap = RuntimeKeymap::from_config(&keymap).unwrap(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); app.app_event_tx = AppEventSender::new(tx); @@ -84,7 +85,7 @@ async fn lifecycle_shortcuts_target_filtered_task_in_any_state() { Some(target), ); view.handle_key_event(KeyCode::Esc.into()); - view.handle_key_event(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)); + view.handle_key_event(KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE)); for character in "Target".chars() { view.handle_key_event(KeyCode::Char(character).into()); } @@ -99,6 +100,10 @@ async fn lifecycle_shortcuts_target_filtered_task_in_any_state() { "unexpected event: {event:?}" ); } + view.handle_key_event(KeyCode::F(8).into()); + assert!( + matches!(rx.try_recv(), Ok(AppEvent::ForkAgentsOverviewThread { thread_id }) if thread_id == target) + ); view.handle_key_event(KeyCode::F(7).into()); assert!( matches!(rx.try_recv(), Ok(AppEvent::HideAgentsOverviewThread { thread_id }) if thread_id == target) @@ -163,7 +168,7 @@ async fn hiding_tasks_keeps_selection_adjacent_in_display_order() -> Result<()> // Clear retained search without dismissing the command center. view.on_ctrl_c(); if filtered { - view.handle_key_event(KeyCode::Char('f').into()); + view.handle_key_event(KeyCode::Char('/').into()); view.handle_paste("Task".into()); // Search selects the first match; move back to Task 3. view.handle_key_event(KeyCode::Down.into()); @@ -753,3 +758,28 @@ async fn lifecycle_removes_background_and_current_tasks_without_losing_the_dashb } Ok(()) } + +#[tokio::test] +async fn fork_shortcut_respects_metadata_editing() { + let (app, mut rx, _) = crate::app::tests::make_test_app_with_channels().await; + let target = ThreadId::new(); + let mut view = app.agents_overview_view( + vec![overview_thread( + target, + /*parent_thread_id*/ None, + "Target", + ThreadStatus::Idle, + )], + Some(target), + ); + for editor in ['r', '/'] { + view.handle_key_event(KeyCode::Char(editor).into()); + view.handle_key_event(KeyCode::Char('f').into()); + assert!(rx.try_recv().is_err()); + view.handle_key_event(KeyCode::Esc.into()); + } + view.handle_key_event(KeyCode::Char('f').into()); + assert!( + matches!(rx.try_recv(), Ok(AppEvent::ForkAgentsOverviewThread { thread_id }) if thread_id == target) + ); +} diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index c4b6bd3502..125a1366c2 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -6,6 +6,7 @@ mod command_center; use super::super::agents_overview_view::AgentsOverviewGrouping; use super::*; +use crate::app::tests::make_test_app_with_channels; use crate::chatwidget::tests::helpers::normalize_agent_center_snapshot; #[tokio::test] @@ -1263,7 +1264,7 @@ async fn shared_overview_shows_only_root_sessions() { state.lock().unwrap().grouping, AgentsOverviewGrouping::Project ); - action_view.handle_key_event(KeyCode::Char('f').into()); + action_view.handle_key_event(KeyCode::Char('/').into()); assert!(action_view.handle_paste("Repair authentication".into())); action_view.handle_key_event(KeyCode::Enter.into()); assert!( @@ -2088,7 +2089,7 @@ async fn cancelling_resume_picker_preserves_command_center_state() -> Result<()> app.chat_widget.show_bottom_pane_view(Box::new(view)); for key in [ KeyEvent::new(KeyCode::Char('g'), KeyModifiers::NONE), - KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE), + KeyEvent::new(KeyCode::Char('/'), KeyModifiers::NONE), KeyEvent::new(KeyCode::Char('S'), KeyModifiers::NONE), KeyCode::Esc.into(), ] { @@ -2139,7 +2140,7 @@ async fn command_center_cursor_tracks_fixed_footer() { )], /*selected_thread_id*/ None, ); - for (key, label) in [('f', "Search ›"), ('r', "Rename ›")] { + for (key, label) in [('/', "Search ›"), ('r', "Rename ›")] { view.handle_key_event(KeyCode::Esc.into()); view.handle_key_event(KeyEvent::new(KeyCode::Char(key), KeyModifiers::NONE)); for width in [48, 96, 120] { @@ -2620,7 +2621,7 @@ async fn command_center_refresh_failure_is_inline_and_clears_on_success() -> Res let mut app = make_test_app().await; let server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?; let mut view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); - view.handle_key_event(KeyCode::Char('f').into()); + view.handle_key_event(KeyCode::Char('/').into()); view.handle_paste("Keep searching".into()); app.chat_widget.show_bottom_pane_view(Box::new(view)); let before = render_bottom_popup(&app.chat_widget, /*width*/ 48); @@ -2720,6 +2721,155 @@ async fn command_center_action_failures_remain_visible() -> Result<()> { #[path = "agents_overview_actions_tests.rs"] mod actions; +#[tokio::test] +async fn overview_fork_preserves_an_open_side_conversation() -> Result<()> { + let mut app = Box::pin(make_test_app()).await; + let side = ThreadId::new(); + app.active_thread_id = Some(side); + app.side_threads.insert( + side, + super::super::side::SideThreadState::new(ThreadId::new()), + ); + let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + let mut tui = crate::tui::test_support::make_test_tui()?; + let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?; + let RequestId::Integer(request) = server.next_request_id() else { + panic!("integer request ID") + }; + Box::pin(app.handle_event( + &mut tui, + &mut server, + AppEvent::ForkAgentsOverviewThread { + thread_id: ThreadId::new(), + }, + )) + .await?; + assert_eq!(app.current_displayed_thread_id(), Some(side)); + assert!(app.side_threads.contains_key(&side)); + assert!(!app.chat_widget.fork_in_progress); + assert_eq!(server.next_request_id(), RequestId::Integer(request + 1)); + insta::assert_snapshot!(render_bottom_popup(&app.chat_widget, /*width*/ 80)); + server.shutdown().await?; + Ok(()) +} + +#[tokio::test] +async fn overview_fork_keeps_idle_source_input_queued() -> Result<()> { + for cached_primary in [false, true] { + let (app, mut events, _) = make_test_app_with_channels().await; + let mut app = Box::pin(app); + trust_fixture_folders(&mut app); + std::fs::write( + app.config.codex_home.join("config.toml"), + "[tui]\nresume_cwd = \"session\"\n", + )?; + let source = ThreadId::from_string( + &app_test_support::create_fake_rollout( + app.config.codex_home.as_path(), + "2025-01-05T12-00-00", + "2025-01-05T12:00:00Z", + "Source task", + Some(&app.config.model_provider_id), + /*git_info*/ None, + ) + .expect("saved rollout"), + )?; + let mut tui = crate::tui::test_support::make_test_tui()?; + let mut server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?; + if cached_primary { + Box::pin(app.select_agents_overview_thread(&mut tui, &mut server, source)).await?; + assert_eq!(app.current_displayed_thread_id(), Some(source)); + app.store_active_thread_receiver().await; + } + app.chat_widget + .set_queue_autosend_suppressed(/*suppressed*/ true); + app.chat_widget.queue_user_message_with_options( + "Keep this on the source".into(), + crate::bottom_pane::QueuedInputAction::Plain, + Vec::new(), + ); + let input = app.chat_widget.capture_thread_input_state().unwrap(); + app.agents_overview + .input_states + .insert(source, input.clone()); + if cached_primary { + app.ensure_thread_channel(source) + .store + .lock() + .await + .input_state = Some(input); + let child = ThreadId::new(); + app.active_thread_id = Some(child); + let mut session = app + .ensure_thread_channel(source) + .store + .lock() + .await + .session + .clone() + .unwrap(); + session.thread_id = child; + app.chat_widget.handle_thread_session(session); + } + while events.try_recv().is_ok() {} + Box::pin(app.handle_event( + &mut tui, + &mut server, + AppEvent::ForkAgentsOverviewThread { thread_id: source }, + )) + .await?; + assert_eq!( + app.current_displayed_thread_id(), + Some(source), + "{}", + render_bottom_popup(&app.chat_widget, /*width*/ 96) + ); + assert_eq!( + app.chat_widget.queued_user_message_texts(), + vec!["Keep this on the source"] + ); + let retained = app.agents_overview.input_states[&source].clone(); + let mut ready = None; + while let Ok(event) = events.try_recv() { + assert!(!matches!( + event, + AppEvent::CodexOp(AppCommand::UserTurn { .. }) + )); + if matches!(event, AppEvent::ForkAgentsOverviewThreadReady { .. }) { + ready = Some(event); + } + } + // Failed reattachment can leave the source displayed as a saved replay. + app.app_server_target = AppServerTarget::LocalDaemon { + allow_embedded_fallback: true, + endpoint: crate::resolve_remote_addr("ws://127.0.0.1:4500")?, + }; + app.ensure_thread_channel(source).mark_replay_only(); + let RequestId::Integer(request) = server.next_request_id() else { + panic!("integer request ID") + }; + Box::pin(app.handle_event( + &mut tui, + &mut server, + AppEvent::ForkAgentsOverviewThreadReady { thread_id: source }, + )) + .await?; + assert_eq!(app.current_displayed_thread_id(), Some(source)); + assert_eq!(server.next_request_id(), RequestId::Integer(request + 1)); + assert!(!app.chat_widget.fork_in_progress); + app.app_server_target = AppServerTarget::Embedded; + Box::pin(app.handle_event(&mut tui, &mut server, ready.expect("fork continuation"))) + .await?; + assert_ne!(app.current_displayed_thread_id(), Some(source)); + assert_eq!(app.agents_overview.input_states[&source], retained); + assert!(app.chat_widget.queued_user_message_texts().is_empty()); + assert!(!app.chat_widget.fork_in_progress); + server.shutdown().await?; + } + Ok(()) +} + fn trust_fixture_folders(app: &mut App) { let projects = serde_json::json!({ test_path_buf("/").display().to_string(): {"trust_level": "trusted"}, diff --git a/codex-rs/tui/src/app/agents_overview_view.rs b/codex-rs/tui/src/app/agents_overview_view.rs index 27ca540c3f..e3693176e1 100644 --- a/codex-rs/tui/src/app/agents_overview_view.rs +++ b/codex-rs/tui/src/app/agents_overview_view.rs @@ -600,6 +600,14 @@ impl BottomPaneView for AgentsOverviewView { } return; } + if self.agents_keymap.fork.is_pressed(key) { + if let Some(row) = self.selected_row() { + self.app_event_tx.send(AppEvent::ForkAgentsOverviewThread { + thread_id: row.thread_id, + }); + } + return; + } if self.agents_keymap.rename.is_pressed(key) { if let Some(row) = self.selected_row() { let mut state = self.state(); diff --git a/codex-rs/tui/src/app/event_dispatch.rs b/codex-rs/tui/src/app/event_dispatch.rs index 01c9626029..3866963064 100644 --- a/codex-rs/tui/src/app/event_dispatch.rs +++ b/codex-rs/tui/src/app/event_dispatch.rs @@ -31,6 +31,19 @@ impl App { app_server: &mut AppServerSession, event: AppEvent, ) -> Result { + let from_agents_overview = matches!(event, AppEvent::ForkAgentsOverviewThreadReady { .. }); + let event = if let AppEvent::ForkAgentsOverviewThreadReady { thread_id } = event { + if self.current_displayed_thread_id() != Some(thread_id) + || (self.thread_unavailable(thread_id) + && !self.chat_widget.is_external_writer_view()) + { + self.chat_widget.fork_in_progress = false; + return Ok(AppRunControl::Continue); + } + AppEvent::ForkCurrentSession { name: None } + } else { + event + }; // Release the shortcut's input guard even when a fork is rejected below. if matches!(event, AppEvent::ForkCurrentSession { .. }) { self.chat_widget.fork_in_progress = false; @@ -496,7 +509,9 @@ impl App { } AppEvent::ForkCurrentSession { name } => { let from_locked_thread = self.chat_widget.is_external_writer_view(); - let source = if from_locked_thread { + let source = if from_agents_overview { + "agents_overview_shortcut" + } else if from_locked_thread { "locked_thread_shortcut" } else { "slash_command" @@ -600,8 +615,8 @@ impl App { )); } } - if from_locked_thread { - // Repeated locked-view shortcuts must not act on the resulting view. + if from_locked_thread || from_agents_overview { + // Repeated fork shortcuts must not act on the resulting view. if let Err(err) = tui.discard_pending_input_before_interactive_screen() { tracing::warn!(%err, "failed to discard input after forking"); } @@ -615,7 +630,9 @@ impl App { } self.chat_widget.fork_in_progress = false; - self.chat_widget.maybe_send_next_queued_input(); + if !from_agents_overview { + self.chat_widget.maybe_send_next_queued_input(); + } tui.frame_requester().schedule_frame(); } AppEvent::RevertSessionForPromptEdit { @@ -2709,6 +2726,34 @@ impl App { AppRunControl::Exit(reason) => return Ok(AppRunControl::Exit(reason)), } } + AppEvent::ForkAgentsOverviewThread { thread_id } => { + if self.reconnect.offline { + return Ok(AppRunControl::Continue); + } + if !self.side_threads.is_empty() + && self.current_displayed_thread_id() != Some(thread_id) + { + self.add_agents_overview_error( + "Close the side conversation before forking another task.".into(), + ); + return Ok(AppRunControl::Continue); + } + // Attachment replay must not submit queued input on the source task. + self.chat_widget.fork_in_progress = true; + let control = self.select_agents_overview_thread(tui, app_server, thread_id).await; + if matches!(&control, Ok(AppRunControl::Continue)) + && self.current_displayed_thread_id() == Some(thread_id) + { + if let Some(input) = self.chat_widget.capture_thread_input_state() { + self.agents_overview.input_states.insert(thread_id, input); + } + self.app_event_tx.send(AppEvent::ForkAgentsOverviewThreadReady { thread_id }); + } else { + self.chat_widget.fork_in_progress = false; + } + return control; + } + AppEvent::ForkAgentsOverviewThreadReady { .. } => unreachable!("normalized above"), AppEvent::NewAgentsOverviewWorktree { cwd } => { Box::pin(self.new_agents_overview_worktree(tui, app_server, cwd)).await; } diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index 1516cba098..9cc9a84a0f 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -490,6 +490,7 @@ impl App { /// This helper copies every known nickname/role from `AgentNavigationState` into the /// replacement widget so that replayed collab items render agent names immediately. pub(super) fn replace_chat_widget(&mut self, mut chat_widget: ChatWidget) { + chat_widget.fork_in_progress = self.chat_widget.fork_in_progress; self.pending_right_click_paste = None; if !self.chat_widget.realtime_conversation_is_running() { self.retain_realtime_replay_state_before_replace(); diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap index c7c1e2b87e..6792b3390b 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap @@ -9,10 +9,11 @@ expression: "screen(&view, 100, 24)" Navigate Tasks View k Up n New tab/shift+tab Filter - j Down o Resume f Search - enter Open z r Rename g Group - pgdn Page down x Stop - ctrl+c Quit h Hide + j Down o Resume f3 Search + enter Open f Fork g Group + pgdn Page down z r Rename + ctrl+c Quit x Stop + h Hide backspace Delete @@ -24,5 +25,4 @@ expression: "screen(&view, 100, 24)" - esc back diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_one_column.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_one_column.snap index 992eaa37b8..b77c12819b 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_one_column.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_one_column.snap @@ -19,6 +19,7 @@ expression: "screen(&view, width, height)" n New w New worktree o Resume + f Fork r Rename x Stop a Archive @@ -27,10 +28,9 @@ expression: "screen(&view, width, height)" View tab/shift+tab Filter - f Search + f3 Search g Group - esc back diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_three_columns.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_three_columns.snap index 0e3d195ecc..7a120b8f04 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_three_columns.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_three_columns.snap @@ -9,11 +9,12 @@ expression: "screen(&view, width, height)" Navigate Tasks View ↑ Up n New tab/shift+tab Filter - ↓ Down w New worktree f Search + ↓ Down w New worktree f3 Search enter Open o Resume g Group - pgup Page up r Rename - pgdn Page down x Stop - ctrl+c Quit a Archive + pgup Page up f Fork + pgdn Page down r Rename + ctrl+c Quit x Stop + a Archive h Hide backspace Delete @@ -24,5 +25,4 @@ expression: "screen(&view, width, height)" - esc back diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_two_columns.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_two_columns.snap index 293f549dbc..b748287723 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_two_columns.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_help_two_columns.snap @@ -11,18 +11,18 @@ expression: "screen(&view, width, height)" ↑ Up n New ↓ Down w New worktree enter Open o Resume - pgup Page up r Rename - pgdn Page down x Stop - ctrl+c Quit a Archive + pgup Page up f Fork + pgdn Page down r Rename + ctrl+c Quit x Stop + a Archive h Hide backspace Delete View tab/shift+tab Filter - f Search + f3 Search g Group - esc back diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_navigation_and_complete_hints.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_navigation_and_complete_hints.snap index bf6297ac45..a51731cbb7 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_navigation_and_complete_hints.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_navigation_and_complete_hints.snap @@ -19,10 +19,10 @@ expression: "screen(&view, width, height)" n New w New worktree o Resume + f Fork r Rename x Stop a Archive h Hide - backspace Delete … resize to see all esc back diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_fork_preserves_an_open_side_conversation.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_fork_preserves_an_open_side_conversation.snap new file mode 100644 index 0000000000..c2af2c82bd --- /dev/null +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__overview_fork_preserves_an_open_side_conversation.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/app/agents_overview_tests.rs +assertion_line: 2740 +expression: "render_bottom_popup(&app.chat_widget, 80)" +--- + Unable to complete action + Close the side conversation before forking another task. + + +› 1. Return to command center + + enter select · esc back diff --git a/codex-rs/tui/src/app/tests/disconnect_tests.rs b/codex-rs/tui/src/app/tests/disconnect_tests.rs index b7150740b2..2cdaea45e3 100644 --- a/codex-rs/tui/src/app/tests/disconnect_tests.rs +++ b/codex-rs/tui/src/app/tests/disconnect_tests.rs @@ -201,12 +201,8 @@ async fn disconnected_command_center_keeps_input_and_blocks_actions() -> Result< let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); app.chat_widget.show_bottom_pane_view(Box::new(view)); let mut tui = crate::tui::test_support::make_test_tui()?; - app.handle_tui_event( - &mut tui, - &mut session, - TuiEvent::Key(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)), - ) - .await?; + app.handle_tui_event(&mut tui, &mut session, TuiEvent::Key(KeyCode::F(3).into())) + .await?; app.handle_tui_event( &mut tui, &mut session, @@ -224,6 +220,7 @@ async fn disconnected_command_center_keeps_input_and_blocks_actions() -> Result< KeyEvent::new(KeyCode::Char('n'), KeyModifiers::NONE), KeyEvent::new(KeyCode::Char('w'), KeyModifiers::NONE), KeyEvent::new(KeyCode::Char('o'), KeyModifiers::NONE), + KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE), ] { app.handle_tui_event(&mut tui, &mut session, TuiEvent::Key(key)) .await?; @@ -240,6 +237,7 @@ async fn disconnected_command_center_keeps_input_and_blocks_actions() -> Result< AppEvent::NewAgentsOverviewSession { .. } | AppEvent::NewAgentsOverviewWorktree { .. } | AppEvent::OpenResumePicker + | AppEvent::ForkAgentsOverviewThread { .. } )) ); assert!(app.chat_widget.has_active_view()); diff --git a/codex-rs/tui/src/app/tests/key_chords.rs b/codex-rs/tui/src/app/tests/key_chords.rs index 2143be9401..95fcee0a52 100644 --- a/codex-rs/tui/src/app/tests/key_chords.rs +++ b/codex-rs/tui/src/app/tests/key_chords.rs @@ -713,7 +713,7 @@ async fn command_center_chords_do_not_capture_search_text() -> Result<()> { let mut tui = crate::tui::test_support::make_test_tui()?; let view = app.agents_overview_view(Vec::new(), /*selected_thread_id*/ None); app.chat_widget.show_bottom_pane_view(Box::new(view)); - app.chat_widget.handle_key_event(KeyCode::Char('f').into()); + app.chat_widget.handle_key_event(KeyCode::Char('/').into()); for key in "new".chars() { let event = KeyCode::Char(key).into(); assert_eq!(app.route_key_chord_event(&mut tui, event), Some(event)); diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index dfedc79733..c77c55841d 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -306,6 +306,14 @@ pub(crate) enum AppEvent { cwd: Option, }, AgentsOverviewWorktreeCreated(Result), + /// Fork the selected dashboard conversation and open the new session. + ForkAgentsOverviewThread { + thread_id: ThreadId, + }, + /// Run the existing fork action after selection events have been processed. + ForkAgentsOverviewThreadReady { + thread_id: ThreadId, + }, /// Rename a task directly from the shared dashboard. RenameAgentsOverviewThread { thread_id: ThreadId, diff --git a/codex-rs/tui/src/chatwidget/input_flow.rs b/codex-rs/tui/src/chatwidget/input_flow.rs index b4d45daf13..7b841baf64 100644 --- a/codex-rs/tui/src/chatwidget/input_flow.rs +++ b/codex-rs/tui/src/chatwidget/input_flow.rs @@ -235,6 +235,7 @@ impl ChatWidget { /// If idle and there are queued inputs, submit exactly one to start the next turn. pub(crate) fn maybe_send_next_queued_input(&mut self) -> bool { if !self.is_session_configured() + || self.fork_in_progress || self.has_misalignment_policy_violation() || self.input_queue.suppress_queue_autosend || self.input_queue.rate_limit_recovery_pending diff --git a/codex-rs/tui/src/keymap.rs b/codex-rs/tui/src/keymap.rs index 2ff06d2296..3628773dd5 100644 --- a/codex-rs/tui/src/keymap.rs +++ b/codex-rs/tui/src/keymap.rs @@ -425,6 +425,7 @@ pub(crate) struct AgentsKeymap { pub(crate) search: Vec, pub(crate) new_task: Vec, pub(crate) new_worktree: Vec, + pub(crate) fork: Vec, pub(crate) rename: Vec, pub(crate) stop: Vec, pub(crate) archive: Vec, @@ -1353,6 +1354,7 @@ impl RuntimeKeymap { search: resolve_local!(keymap, defaults, agents, search), new_task: resolve_local!(keymap, defaults, agents, new_task), new_worktree: resolve_local!(keymap, defaults, agents, new_worktree), + fork: resolve_local!(keymap, defaults, agents, fork), rename: resolve_local!(keymap, defaults, agents, rename), stop: resolve_local!(keymap, defaults, agents, stop), archive: resolve_local!(keymap, defaults, agents, archive), @@ -1363,40 +1365,35 @@ impl RuntimeKeymap { }; // New defaults yield to explicit bindings, including existing list shortcuts. - for (configured, bindings, alias) in [ - (keymap.agents.resume.as_ref(), &mut agents.resume, "o"), - (keymap.agents.search.as_ref(), &mut agents.search, "f"), - (keymap.agents.new_task.as_ref(), &mut agents.new_task, "n"), + for (configured, bindings) in [ + (keymap.agents.resume.as_ref(), &mut agents.resume), + (keymap.agents.search.as_ref(), &mut agents.search), + (keymap.agents.new_task.as_ref(), &mut agents.new_task), ( keymap.agents.new_worktree.as_ref(), &mut agents.new_worktree, - "w", ), - (keymap.agents.rename.as_ref(), &mut agents.rename, "r"), - (keymap.agents.stop.as_ref(), &mut agents.stop, "x"), - (keymap.agents.archive.as_ref(), &mut agents.archive, "a"), - ( - keymap.agents.delete.as_ref(), - &mut agents.delete, - "backspace", - ), - (keymap.agents.hide.as_ref(), &mut agents.hide, "h"), + (keymap.agents.fork.as_ref(), &mut agents.fork), + (keymap.agents.rename.as_ref(), &mut agents.rename), + (keymap.agents.stop.as_ref(), &mut agents.stop), + (keymap.agents.archive.as_ref(), &mut agents.archive), + (keymap.agents.delete.as_ref(), &mut agents.delete), + (keymap.agents.hide.as_ref(), &mut agents.hide), ( keymap.agents.toggle_grouping.as_ref(), &mut agents.toggle_grouping, - "g", ), ] { - if configured.is_none() - && (configured_context_alias_is_used(&keymap.agents, alias) - || configured_context_alias_is_used(&keymap.list, alias) - || configured_context_alias_is_used(&keymap.global, alias) - || chords.bindings.iter().any(|chord| { - chord.action.context.overlaps(KeymapContext::Agents) - && bindings.contains(&chord.chord.prefix) - })) - { - bindings.clear(); + if configured.is_none() { + bindings.retain(|binding| { + !configured_context_binding_is_used(&keymap.agents, *binding) + && !configured_context_binding_is_used(&keymap.list, *binding) + && !configured_context_binding_is_used(&keymap.global, *binding) + && !chords.bindings.iter().any(|chord| { + chord.action.context.overlaps(KeymapContext::Agents) + && chord.chord.prefix == *binding + }) + }); } } @@ -1542,6 +1539,7 @@ impl RuntimeKeymap { keymap.agents.new_worktree.as_ref(), &mut agents.new_worktree, ), + (keymap.agents.fork.as_ref(), &mut agents.fork), (keymap.agents.rename.as_ref(), &mut agents.rename), (keymap.agents.stop.as_ref(), &mut agents.stop), (keymap.agents.archive.as_ref(), &mut agents.archive), @@ -1912,9 +1910,10 @@ impl RuntimeKeymap { }, agents: AgentsKeymap { resume: default_bindings![plain(KeyCode::Char('o'))], - search: default_bindings![plain(KeyCode::Char('f'))], + search: default_bindings![plain(KeyCode::F(3)), plain(KeyCode::Char('/'))], new_task: default_bindings![plain(KeyCode::Char('n'))], new_worktree: default_bindings![plain(KeyCode::Char('w'))], + fork: default_bindings![plain(KeyCode::Char('f'))], rename: default_bindings![plain(KeyCode::Char('r'))], stop: default_bindings![plain(KeyCode::Char('x'))], archive: default_bindings![plain(KeyCode::Char('a'))], @@ -2312,7 +2311,11 @@ impl RuntimeKeymap { "agents", context_bindings(KeymapContext::Agents), MAIN_RESERVED_BINDINGS, - [], + [( + "search", + "fixed.slash_command", + key_hint::plain(KeyCode::Char('/')), + )], )?; for (action, bindings) in context_bindings(KeymapContext::Agents) { #[cfg(unix)] diff --git a/codex-rs/tui/src/keymap/bindings.rs b/codex-rs/tui/src/keymap/bindings.rs index 3f521a4c3d..0613f0d1fa 100644 --- a/codex-rs/tui/src/keymap/bindings.rs +++ b/codex-rs/tui/src/keymap/bindings.rs @@ -422,6 +422,7 @@ define_runtime_action_bindings! { search, new_task, new_worktree, + fork, rename, stop, archive, diff --git a/codex-rs/tui/src/keymap/conflict_tests.rs b/codex-rs/tui/src/keymap/conflict_tests.rs index 583e99e4be..c927337523 100644 --- a/codex-rs/tui/src/keymap/conflict_tests.rs +++ b/codex-rs/tui/src/keymap/conflict_tests.rs @@ -45,9 +45,11 @@ fn new_agents_defaults_preserve_existing_custom_bindings() { ("delete", "backspace"), ("hide", "h"), ("new_worktree", "w"), + ("fork", "f"), ] { for (context, existing, suffix) in [ ("agents", "stop", ""), + ("agents", "search", ""), ("list", "move_down", ""), ("global", "copy", ""), ("agents", "stop", " f12"), @@ -74,6 +76,7 @@ fn new_agents_defaults_preserve_existing_custom_bindings() { "delete" => &runtime.agents.delete, "hide" => &runtime.agents.hide, "new_worktree" => &runtime.agents.new_worktree, + "fork" => &runtime.agents.fork, _ => unreachable!(), }; assert!( @@ -150,3 +153,16 @@ fn warnings_defaults_preserve_custom_keys_and_chord_prefixes() { assert!(runtime.app.open_warnings.is_empty()); } } + +#[test] +fn agents_search_keeps_unshadowed_fallbacks() { + use crate::key_hint; + use crossterm::event::KeyCode; + + for (configured, remaining) in [("/", KeyCode::F(3)), ("f3", KeyCode::Char('/'))] { + let keymap: TuiKeymap = + serde_json::from_value(json!({"list": {"move_up": configured}})).unwrap(); + let runtime = RuntimeKeymap::from_config(&keymap).unwrap(); + assert_eq!(runtime.agents.search, vec![key_hint::plain(remaining)]); + } +} diff --git a/codex-rs/tui/src/keymap_setup/actions.rs b/codex-rs/tui/src/keymap_setup/actions.rs index 6359ecac8a..46ec0ac847 100644 --- a/codex-rs/tui/src/keymap_setup/actions.rs +++ b/codex-rs/tui/src/keymap_setup/actions.rs @@ -223,6 +223,7 @@ pub(super) const KEYMAP_ACTIONS: &[KeymapActionDescriptor] = &[ action("agents", "Agents", "search", "Search the available agent tasks."), action("agents", "Agents", "new_task", "Open a new session in the selected checkout."), action("agents", "Agents", "new_worktree", "Open a new session in a worktree from the project default branch."), + action("agents", "Agents", "fork", "Fork the selected conversation and open the new session."), action("agents", "Agents", "rename", "Rename the selected task."), action("agents", "Agents", "stop", "Stop the selected running task."), action("agents", "Agents", "archive", "Archive the selected task and its child agents."), @@ -408,6 +409,7 @@ pub(super) fn binding_slot<'a>( ("agents", "search") => Some(&mut keymap.agents.search), ("agents", "new_task") => Some(&mut keymap.agents.new_task), ("agents", "new_worktree") => Some(&mut keymap.agents.new_worktree), + ("agents", "fork") => Some(&mut keymap.agents.fork), ("agents", "rename") => Some(&mut keymap.agents.rename), ("agents", "stop") => Some(&mut keymap.agents.stop), ("agents", "archive") => Some(&mut keymap.agents.archive), diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_agents_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_agents_actions.snap index ea2df0d0cc..eb1c30444b 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_agents_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_agents_actions.snap @@ -3,9 +3,10 @@ source: tui/src/keymap_setup.rs expression: agents --- Resume | o | Agents resume Resume Open the session resume picker. o Default -Search | f | Agents search Search Search the available agent tasks. f Default +Search | f3, / | Agents search Search Search the available agent tasks. f3, / Default New Task | n | Agents new_task New Task Open a new session in the selected checkout. n Default New Worktree | w | Agents new_worktree New Worktree Open a new session in a worktree from the project default branch. w Default +Fork | f | Agents fork Fork Fork the selected conversation and open the new session. f Default Rename | r | Agents rename Rename Rename the selected task. r Default Stop | x | Agents stop Stop Stop the selected running task. x Default Archive | a | Agents archive Archive Archive the selected task and its child agents. a Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap index 07cd8e07f1..0e761ed368 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_custom.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 150 actions, 1 customized, 5 unbound. + 151 actions, 1 customized, 5 unbound. All Common Customized (1) Unbound (5) App Composer Editor Vim Navigation Agents Approval › diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap index 9ed6743cbd..ba5ba36700 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_fast_mode_enabled.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 151 actions, 0 customized, 6 unbound. + 152 actions, 0 customized, 6 unbound. All Common Customized (0) Unbound (6) App Composer Editor Vim Navigation Agents Approval › diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap index 9b9e267b4b..19f0e027b3 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_first_actions.snap @@ -2,7 +2,7 @@ source: tui/src/keymap_setup.rs expression: snapshot --- -tab: All (150 selectable) +tab: All (151 selectable) tab: Common (20 selectable) tab: Customized (0) (0 selectable) tab: Unbound (5) (5 selectable) @@ -11,7 +11,7 @@ tab: Composer (5 selectable) tab: Editor (17 selectable) tab: Vim (69 selectable) tab: Navigation (21 selectable) -tab: Agents (10 selectable) +tab: Agents (11 selectable) tab: Approval (8 selectable) tab: Debug (1 selectable) Open Agents | unbound | Global open_agents Open Agents Open the shared agent-session overview. unbound Default diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap index e5e27f4066..cc3222de83 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_global_chord.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 150 actions, 1 customized, 6 unbound. + 151 actions, 1 customized, 6 unbound. All Common Customized (1) Unbound (6) App Composer Editor Vim Navigation Agents Approval › diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap index 32f1127699..953b11dfff 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_narrow.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 78)" Keymap All configurable shortcuts. - 150 actions, 0 customized, 5 unbound. + 151 actions, 0 customized, 5 unbound. All Common Customized (0) Unbound (5) App Composer Editor › diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap index a3ecd77419..2296ebc618 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_picker_wide.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 150 actions, 0 customized, 5 unbound. + 151 actions, 0 customized, 5 unbound. All Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval › diff --git a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap index 459e314dc2..3785b7bf0f 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__keymap_setup__tests__keymap_question_actions.snap @@ -5,7 +5,7 @@ expression: "render_picker(params, 120)" Keymap All configurable shortcuts. - 150 actions, 0 customized, 5 unbound. + 151 actions, 0 customized, 5 unbound. All Common Customized (0) Unbound (5) App Composer Editor Vim Navigation Agents Approval ›