@@ -34,6 +34,7 @@ use crate::pager_overlay::Overlay;
3434use crate :: render:: highlight:: highlight_bash_to_lines;
3535use crate :: render:: renderable:: Renderable ;
3636use crate :: resume_picker:: SessionSelection ;
37+ use crate :: resume_picker:: SessionTarget ;
3738use crate :: tui;
3839use crate :: tui:: TuiEvent ;
3940use crate :: update_action:: UpdateAction ;
@@ -51,6 +52,7 @@ use codex_core::config::edit::ConfigEditsBuilder;
5152use codex_core:: config:: types:: ModelAvailabilityNuxConfig ;
5253use codex_core:: config_loader:: ConfigLayerStackOrdering ;
5354use codex_core:: features:: Feature ;
55+ use codex_core:: find_thread_path_by_id_str;
5456use codex_core:: models_manager:: collaboration_mode_presets:: CollaborationModesConfig ;
5557use codex_core:: models_manager:: manager:: RefreshStrategy ;
5658use codex_core:: models_manager:: model_presets:: HIDE_GPT_5_1_CODEX_MAX_MIGRATION_PROMPT_CONFIG ;
@@ -812,6 +814,99 @@ impl App {
812814 }
813815 }
814816
817+ async fn resume_session_target (
818+ & mut self ,
819+ tui : & mut tui:: Tui ,
820+ target_session : SessionTarget ,
821+ ) -> Result < ( ) > {
822+ let current_cwd = self . config . cwd . clone ( ) ;
823+ let resume_cwd = match crate :: resolve_cwd_for_resume_or_fork (
824+ tui,
825+ & self . config ,
826+ & current_cwd,
827+ target_session. thread_id ,
828+ & target_session. path ,
829+ CwdPromptAction :: Resume ,
830+ true ,
831+ )
832+ . await ?
833+ {
834+ crate :: ResolveCwdOutcome :: Continue ( Some ( cwd) ) => cwd,
835+ crate :: ResolveCwdOutcome :: Continue ( None ) => current_cwd. clone ( ) ,
836+ crate :: ResolveCwdOutcome :: Exit => return Ok ( ( ) ) ,
837+ } ;
838+ let mut resume_config = match self
839+ . rebuild_config_for_resume_or_fallback ( & current_cwd, resume_cwd)
840+ . await
841+ {
842+ Ok ( cfg) => cfg,
843+ Err ( err) => {
844+ self . chat_widget . add_error_message ( format ! (
845+ "Failed to rebuild configuration for resume: {err}"
846+ ) ) ;
847+ return Ok ( ( ) ) ;
848+ }
849+ } ;
850+ self . apply_runtime_policy_overrides ( & mut resume_config) ;
851+ let summary = session_summary (
852+ self . chat_widget . token_usage ( ) ,
853+ self . chat_widget . thread_id ( ) ,
854+ self . chat_widget . thread_name ( ) ,
855+ ) ;
856+ match self
857+ . server
858+ . resume_thread_from_rollout (
859+ resume_config. clone ( ) ,
860+ target_session. path . clone ( ) ,
861+ self . auth_manager . clone ( ) ,
862+ )
863+ . await
864+ {
865+ Ok ( resumed) => {
866+ self . shutdown_current_thread ( ) . await ;
867+ self . config = resume_config;
868+ tui. set_notification_method ( self . config . tui_notification_method ) ;
869+ self . file_search . update_search_dir ( self . config . cwd . clone ( ) ) ;
870+ let init =
871+ self . chatwidget_init_for_forked_or_resumed_thread ( tui, self . config . clone ( ) ) ;
872+ self . chat_widget =
873+ ChatWidget :: new_from_existing ( init, resumed. thread , resumed. session_configured ) ;
874+ self . reset_thread_event_state ( ) ;
875+ if let Some ( summary) = summary {
876+ let mut lines: Vec < Line < ' static > > = vec ! [ summary. usage_line. clone( ) . into( ) ] ;
877+ if let Some ( command) = summary. resume_command {
878+ let spans = vec ! [ "To continue this session, run " . into( ) , command. cyan( ) ] ;
879+ lines. push ( spans. into ( ) ) ;
880+ }
881+ self . chat_widget . add_plain_history_lines ( lines) ;
882+ }
883+ }
884+ Err ( err) => {
885+ let path_display = target_session. path . display ( ) ;
886+ self . chat_widget . add_error_message ( format ! (
887+ "Failed to resume session from {path_display}: {err}"
888+ ) ) ;
889+ }
890+ }
891+ Ok ( ( ) )
892+ }
893+
894+ async fn resume_session_by_thread_id (
895+ & mut self ,
896+ tui : & mut tui:: Tui ,
897+ thread_id : ThreadId ,
898+ ) -> Result < ( ) > {
899+ let Some ( path) =
900+ find_thread_path_by_id_str ( & self . config . codex_home , & thread_id. to_string ( ) ) . await ?
901+ else {
902+ self . chat_widget
903+ . add_error_message ( format ! ( "No saved session found for thread {thread_id}." ) ) ;
904+ return Ok ( ( ) ) ;
905+ } ;
906+ self . resume_session_target ( tui, SessionTarget { path, thread_id } )
907+ . await
908+ }
909+
815910 fn apply_runtime_policy_overrides ( & mut self , config : & mut Config ) {
816911 if let Some ( policy) = self . runtime_approval_policy_override . as_ref ( )
817912 && let Err ( err) = config. permissions . approval_policy . set ( * policy)
@@ -1426,7 +1521,13 @@ impl App {
14261521 description : Some ( uuid. clone ( ) ) ,
14271522 is_current : self . active_thread_id == Some ( * thread_id) ,
14281523 actions : vec ! [ Box :: new( move |tx| {
1429- tx. send( AppEvent :: SelectAgentThread ( id) ) ;
1524+ tx. send( AppEvent :: HandleSlashCommandDraft (
1525+ format!(
1526+ "/{} {id}" ,
1527+ crate :: slash_command:: SlashCommand :: Agent . command( )
1528+ )
1529+ . into( ) ,
1530+ ) ) ;
14301531 } ) ] ,
14311532 dismiss_on_select : true ,
14321533 search_value : Some ( format ! ( "{name} {uuid}" ) ) ,
@@ -2150,87 +2251,15 @@ impl App {
21502251 AppEvent :: OpenResumePicker => {
21512252 match crate :: resume_picker:: run_resume_picker ( tui, & self . config , false ) . await ? {
21522253 SessionSelection :: Resume ( target_session) => {
2153- let current_cwd = self . config . cwd . clone ( ) ;
2154- let resume_cwd = match crate :: resolve_cwd_for_resume_or_fork (
2155- tui,
2156- & self . config ,
2157- & current_cwd,
2158- target_session. thread_id ,
2159- & target_session. path ,
2160- CwdPromptAction :: Resume ,
2161- true ,
2162- )
2163- . await ?
2164- {
2165- crate :: ResolveCwdOutcome :: Continue ( Some ( cwd) ) => cwd,
2166- crate :: ResolveCwdOutcome :: Continue ( None ) => current_cwd. clone ( ) ,
2167- crate :: ResolveCwdOutcome :: Exit => {
2168- return Ok ( AppRunControl :: Exit ( ExitReason :: UserRequested ) ) ;
2169- }
2170- } ;
2171- let mut resume_config = match self
2172- . rebuild_config_for_resume_or_fallback ( & current_cwd, resume_cwd)
2173- . await
2174- {
2175- Ok ( cfg) => cfg,
2176- Err ( err) => {
2177- self . chat_widget . add_error_message ( format ! (
2178- "Failed to rebuild configuration for resume: {err}"
2179- ) ) ;
2180- return Ok ( AppRunControl :: Continue ) ;
2181- }
2182- } ;
2183- self . apply_runtime_policy_overrides ( & mut resume_config) ;
2184- let summary = session_summary (
2185- self . chat_widget . token_usage ( ) ,
2186- self . chat_widget . thread_id ( ) ,
2187- self . chat_widget . thread_name ( ) ,
2188- ) ;
2189- match self
2190- . server
2191- . resume_thread_from_rollout (
2192- resume_config. clone ( ) ,
2193- target_session. path . clone ( ) ,
2194- self . auth_manager . clone ( ) ,
2195- None ,
2254+ self . chat_widget . handle_serialized_slash_command (
2255+ format ! (
2256+ "/{} {}" ,
2257+ crate :: slash_command:: SlashCommand :: Resume . command( ) ,
2258+ target_session. thread_id
21962259 )
2197- . await
2198- {
2199- Ok ( resumed) => {
2200- self . shutdown_current_thread ( ) . await ;
2201- self . config = resume_config;
2202- tui. set_notification_method ( self . config . tui_notification_method ) ;
2203- self . file_search . update_search_dir ( self . config . cwd . clone ( ) ) ;
2204- let init = self . chatwidget_init_for_forked_or_resumed_thread (
2205- tui,
2206- self . config . clone ( ) ,
2207- ) ;
2208- self . chat_widget = ChatWidget :: new_from_existing (
2209- init,
2210- resumed. thread ,
2211- resumed. session_configured ,
2212- ) ;
2213- self . reset_thread_event_state ( ) ;
2214- if let Some ( summary) = summary {
2215- let mut lines: Vec < Line < ' static > > =
2216- vec ! [ summary. usage_line. clone( ) . into( ) ] ;
2217- if let Some ( command) = summary. resume_command {
2218- let spans = vec ! [
2219- "To continue this session, run " . into( ) ,
2220- command. cyan( ) ,
2221- ] ;
2222- lines. push ( spans. into ( ) ) ;
2223- }
2224- self . chat_widget . add_plain_history_lines ( lines) ;
2225- }
2226- }
2227- Err ( err) => {
2228- let path_display = target_session. path . display ( ) ;
2229- self . chat_widget . add_error_message ( format ! (
2230- "Failed to resume session from {path_display}: {err}"
2231- ) ) ;
2232- }
2233- }
2260+ . into ( ) ,
2261+ ) ;
2262+ self . refresh_status_line ( ) ;
22342263 }
22352264 SessionSelection :: Exit
22362265 | SessionSelection :: StartFresh
@@ -2240,6 +2269,9 @@ impl App {
22402269 // Leaving alt-screen may blank the inline viewport; force a redraw either way.
22412270 tui. frame_requester ( ) . schedule_frame ( ) ;
22422271 }
2272+ AppEvent :: ResumeSession ( thread_id) => {
2273+ self . resume_session_by_thread_id ( tui, thread_id) . await ?;
2274+ }
22432275 AppEvent :: ForkCurrentSession => {
22442276 self . session_telemetry . counter (
22452277 "codex.thread.fork" ,
@@ -3176,9 +3208,6 @@ impl App {
31763208 AppEvent :: SelectAgentThread ( thread_id) => {
31773209 self . select_agent_thread ( tui, thread_id) . await ?;
31783210 }
3179- AppEvent :: OpenSkillsList => {
3180- self . chat_widget . open_skills_list ( ) ;
3181- }
31823211 AppEvent :: OpenManageSkillsPopup => {
31833212 self . chat_widget . open_manage_skills_popup ( ) ;
31843213 }
@@ -3875,6 +3904,7 @@ mod tests {
38753904 use crate :: app_backtrack:: BacktrackSelection ;
38763905 use crate :: app_backtrack:: BacktrackState ;
38773906 use crate :: app_backtrack:: user_count;
3907+ use crate :: chatwidget:: UserMessage ;
38783908 use crate :: chatwidget:: tests:: make_chatwidget_manual_with_sender;
38793909 use crate :: chatwidget:: tests:: set_chatgpt_auth;
38803910 use crate :: file_search:: FileSearchManager ;
@@ -5166,10 +5196,12 @@ mod tests {
51665196 app. chat_widget
51675197 . handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: NONE ) ) ;
51685198
5169- assert_matches ! (
5170- app_event_rx. try_recv( ) ,
5171- Ok ( AppEvent :: SelectAgentThread ( selected_thread_id) ) if selected_thread_id == thread_id
5172- ) ;
5199+ match app_event_rx. try_recv ( ) {
5200+ Ok ( AppEvent :: HandleSlashCommandDraft ( draft) ) => {
5201+ assert_eq ! ( draft, UserMessage :: from( format!( "/agent {thread_id}" ) ) ) ;
5202+ }
5203+ other => panic ! ( "expected serialized agent slash draft, got {other:?}" ) ,
5204+ }
51735205 Ok ( ( ) )
51745206 }
51755207
0 commit comments