@@ -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 ;
@@ -809,6 +811,99 @@ impl App {
809811 }
810812 }
811813
814+ async fn resume_session_target (
815+ & mut self ,
816+ tui : & mut tui:: Tui ,
817+ target_session : SessionTarget ,
818+ ) -> Result < ( ) > {
819+ let current_cwd = self . config . cwd . clone ( ) ;
820+ let resume_cwd = match crate :: resolve_cwd_for_resume_or_fork (
821+ tui,
822+ & self . config ,
823+ & current_cwd,
824+ target_session. thread_id ,
825+ & target_session. path ,
826+ CwdPromptAction :: Resume ,
827+ true ,
828+ )
829+ . await ?
830+ {
831+ crate :: ResolveCwdOutcome :: Continue ( Some ( cwd) ) => cwd,
832+ crate :: ResolveCwdOutcome :: Continue ( None ) => current_cwd. clone ( ) ,
833+ crate :: ResolveCwdOutcome :: Exit => return Ok ( ( ) ) ,
834+ } ;
835+ let mut resume_config = match self
836+ . rebuild_config_for_resume_or_fallback ( & current_cwd, resume_cwd)
837+ . await
838+ {
839+ Ok ( cfg) => cfg,
840+ Err ( err) => {
841+ self . chat_widget . add_error_message ( format ! (
842+ "Failed to rebuild configuration for resume: {err}"
843+ ) ) ;
844+ return Ok ( ( ) ) ;
845+ }
846+ } ;
847+ self . apply_runtime_policy_overrides ( & mut resume_config) ;
848+ let summary = session_summary (
849+ self . chat_widget . token_usage ( ) ,
850+ self . chat_widget . thread_id ( ) ,
851+ self . chat_widget . thread_name ( ) ,
852+ ) ;
853+ match self
854+ . server
855+ . resume_thread_from_rollout (
856+ resume_config. clone ( ) ,
857+ target_session. path . clone ( ) ,
858+ self . auth_manager . clone ( ) ,
859+ )
860+ . await
861+ {
862+ Ok ( resumed) => {
863+ self . shutdown_current_thread ( ) . await ;
864+ self . config = resume_config;
865+ tui. set_notification_method ( self . config . tui_notification_method ) ;
866+ self . file_search . update_search_dir ( self . config . cwd . clone ( ) ) ;
867+ let init =
868+ self . chatwidget_init_for_forked_or_resumed_thread ( tui, self . config . clone ( ) ) ;
869+ self . chat_widget =
870+ ChatWidget :: new_from_existing ( init, resumed. thread , resumed. session_configured ) ;
871+ self . reset_thread_event_state ( ) ;
872+ if let Some ( summary) = summary {
873+ let mut lines: Vec < Line < ' static > > = vec ! [ summary. usage_line. clone( ) . into( ) ] ;
874+ if let Some ( command) = summary. resume_command {
875+ let spans = vec ! [ "To continue this session, run " . into( ) , command. cyan( ) ] ;
876+ lines. push ( spans. into ( ) ) ;
877+ }
878+ self . chat_widget . add_plain_history_lines ( lines) ;
879+ }
880+ }
881+ Err ( err) => {
882+ let path_display = target_session. path . display ( ) ;
883+ self . chat_widget . add_error_message ( format ! (
884+ "Failed to resume session from {path_display}: {err}"
885+ ) ) ;
886+ }
887+ }
888+ Ok ( ( ) )
889+ }
890+
891+ async fn resume_session_by_thread_id (
892+ & mut self ,
893+ tui : & mut tui:: Tui ,
894+ thread_id : ThreadId ,
895+ ) -> Result < ( ) > {
896+ let Some ( path) =
897+ find_thread_path_by_id_str ( & self . config . codex_home , & thread_id. to_string ( ) ) . await ?
898+ else {
899+ self . chat_widget
900+ . add_error_message ( format ! ( "No saved session found for thread {thread_id}." ) ) ;
901+ return Ok ( ( ) ) ;
902+ } ;
903+ self . resume_session_target ( tui, SessionTarget { path, thread_id } )
904+ . await
905+ }
906+
812907 fn apply_runtime_policy_overrides ( & mut self , config : & mut Config ) {
813908 if let Some ( policy) = self . runtime_approval_policy_override . as_ref ( )
814909 && let Err ( err) = config. permissions . approval_policy . set ( * policy)
@@ -1397,7 +1492,13 @@ impl App {
13971492 description : Some ( uuid. clone ( ) ) ,
13981493 is_current : self . active_thread_id == Some ( * thread_id) ,
13991494 actions : vec ! [ Box :: new( move |tx| {
1400- tx. send( AppEvent :: SelectAgentThread ( id) ) ;
1495+ tx. send( AppEvent :: HandleSlashCommandDraft (
1496+ format!(
1497+ "/{} {id}" ,
1498+ crate :: slash_command:: SlashCommand :: Agent . command( )
1499+ )
1500+ . into( ) ,
1501+ ) ) ;
14011502 } ) ] ,
14021503 dismiss_on_select : true ,
14031504 search_value : Some ( format ! ( "{name} {uuid}" ) ) ,
@@ -2110,86 +2211,15 @@ impl App {
21102211 AppEvent :: OpenResumePicker => {
21112212 match crate :: resume_picker:: run_resume_picker ( tui, & self . config , false ) . await ? {
21122213 SessionSelection :: Resume ( target_session) => {
2113- let current_cwd = self . config . cwd . clone ( ) ;
2114- let resume_cwd = match crate :: resolve_cwd_for_resume_or_fork (
2115- tui,
2116- & self . config ,
2117- & current_cwd,
2118- target_session. thread_id ,
2119- & target_session. path ,
2120- CwdPromptAction :: Resume ,
2121- true ,
2122- )
2123- . await ?
2124- {
2125- crate :: ResolveCwdOutcome :: Continue ( Some ( cwd) ) => cwd,
2126- crate :: ResolveCwdOutcome :: Continue ( None ) => current_cwd. clone ( ) ,
2127- crate :: ResolveCwdOutcome :: Exit => {
2128- return Ok ( AppRunControl :: Exit ( ExitReason :: UserRequested ) ) ;
2129- }
2130- } ;
2131- let mut resume_config = match self
2132- . rebuild_config_for_resume_or_fallback ( & current_cwd, resume_cwd)
2133- . await
2134- {
2135- Ok ( cfg) => cfg,
2136- Err ( err) => {
2137- self . chat_widget . add_error_message ( format ! (
2138- "Failed to rebuild configuration for resume: {err}"
2139- ) ) ;
2140- return Ok ( AppRunControl :: Continue ) ;
2141- }
2142- } ;
2143- self . apply_runtime_policy_overrides ( & mut resume_config) ;
2144- let summary = session_summary (
2145- self . chat_widget . token_usage ( ) ,
2146- self . chat_widget . thread_id ( ) ,
2147- self . chat_widget . thread_name ( ) ,
2148- ) ;
2149- match self
2150- . server
2151- . resume_thread_from_rollout (
2152- resume_config. clone ( ) ,
2153- target_session. path . clone ( ) ,
2154- self . auth_manager . clone ( ) ,
2214+ self . chat_widget . handle_serialized_slash_command (
2215+ format ! (
2216+ "/{} {}" ,
2217+ crate :: slash_command:: SlashCommand :: Resume . command( ) ,
2218+ target_session. thread_id
21552219 )
2156- . await
2157- {
2158- Ok ( resumed) => {
2159- self . shutdown_current_thread ( ) . await ;
2160- self . config = resume_config;
2161- tui. set_notification_method ( self . config . tui_notification_method ) ;
2162- self . file_search . update_search_dir ( self . config . cwd . clone ( ) ) ;
2163- let init = self . chatwidget_init_for_forked_or_resumed_thread (
2164- tui,
2165- self . config . clone ( ) ,
2166- ) ;
2167- self . chat_widget = ChatWidget :: new_from_existing (
2168- init,
2169- resumed. thread ,
2170- resumed. session_configured ,
2171- ) ;
2172- self . reset_thread_event_state ( ) ;
2173- if let Some ( summary) = summary {
2174- let mut lines: Vec < Line < ' static > > =
2175- vec ! [ summary. usage_line. clone( ) . into( ) ] ;
2176- if let Some ( command) = summary. resume_command {
2177- let spans = vec ! [
2178- "To continue this session, run " . into( ) ,
2179- command. cyan( ) ,
2180- ] ;
2181- lines. push ( spans. into ( ) ) ;
2182- }
2183- self . chat_widget . add_plain_history_lines ( lines) ;
2184- }
2185- }
2186- Err ( err) => {
2187- let path_display = target_session. path . display ( ) ;
2188- self . chat_widget . add_error_message ( format ! (
2189- "Failed to resume session from {path_display}: {err}"
2190- ) ) ;
2191- }
2192- }
2220+ . into ( ) ,
2221+ ) ;
2222+ self . refresh_status_line ( ) ;
21932223 }
21942224 SessionSelection :: Exit
21952225 | SessionSelection :: StartFresh
@@ -2199,6 +2229,9 @@ impl App {
21992229 // Leaving alt-screen may blank the inline viewport; force a redraw either way.
22002230 tui. frame_requester ( ) . schedule_frame ( ) ;
22012231 }
2232+ AppEvent :: ResumeSession ( thread_id) => {
2233+ self . resume_session_by_thread_id ( tui, thread_id) . await ?;
2234+ }
22022235 AppEvent :: ForkCurrentSession => {
22032236 self . session_telemetry . counter (
22042237 "codex.thread.fork" ,
@@ -3132,9 +3165,6 @@ impl App {
31323165 AppEvent :: SelectAgentThread ( thread_id) => {
31333166 self . select_agent_thread ( tui, thread_id) . await ?;
31343167 }
3135- AppEvent :: OpenSkillsList => {
3136- self . chat_widget . open_skills_list ( ) ;
3137- }
31383168 AppEvent :: OpenManageSkillsPopup => {
31393169 self . chat_widget . open_manage_skills_popup ( ) ;
31403170 }
@@ -3793,6 +3823,7 @@ mod tests {
37933823 use crate :: app_backtrack:: BacktrackSelection ;
37943824 use crate :: app_backtrack:: BacktrackState ;
37953825 use crate :: app_backtrack:: user_count;
3826+ use crate :: chatwidget:: UserMessage ;
37963827 use crate :: chatwidget:: tests:: make_chatwidget_manual_with_sender;
37973828 use crate :: file_search:: FileSearchManager ;
37983829 use crate :: history_cell:: AgentMessageCell ;
@@ -5083,10 +5114,12 @@ mod tests {
50835114 app. chat_widget
50845115 . handle_key_event ( KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: NONE ) ) ;
50855116
5086- assert_matches ! (
5087- app_event_rx. try_recv( ) ,
5088- Ok ( AppEvent :: SelectAgentThread ( selected_thread_id) ) if selected_thread_id == thread_id
5089- ) ;
5117+ match app_event_rx. try_recv ( ) {
5118+ Ok ( AppEvent :: HandleSlashCommandDraft ( draft) ) => {
5119+ assert_eq ! ( draft, UserMessage :: from( format!( "/agent {thread_id}" ) ) ) ;
5120+ }
5121+ other => panic ! ( "expected serialized agent slash draft, got {other:?}" ) ,
5122+ }
50905123 Ok ( ( ) )
50915124 }
50925125
0 commit comments