@@ -607,6 +607,7 @@ pub(crate) struct ChatWidget {
607607 pending_status_indicator_restore : bool ,
608608 thread_id : Option < ThreadId > ,
609609 thread_name : Option < String > ,
610+ title_override : Option < String > ,
610611 forked_from : Option < ThreadId > ,
611612 frame_requester : FrameRequester ,
612613 // Whether to include the initial welcome banner on session configured
@@ -2913,6 +2914,7 @@ impl ChatWidget {
29132914 pending_status_indicator_restore : false ,
29142915 thread_id : None ,
29152916 thread_name : None ,
2917+ title_override : None ,
29162918 forked_from : None ,
29172919 queued_user_messages : VecDeque :: new ( ) ,
29182920 queued_message_edit_binding,
@@ -3093,6 +3095,7 @@ impl ChatWidget {
30933095 pending_status_indicator_restore : false ,
30943096 thread_id : None ,
30953097 thread_name : None ,
3098+ title_override : None ,
30963099 forked_from : None ,
30973100 saw_plan_update_this_turn : false ,
30983101 saw_plan_item_this_turn : false ,
@@ -3262,6 +3265,7 @@ impl ChatWidget {
32623265 pending_status_indicator_restore : false ,
32633266 thread_id : None ,
32643267 thread_name : None ,
3268+ title_override : None ,
32653269 forked_from : None ,
32663270 queued_user_messages : VecDeque :: new ( ) ,
32673271 queued_message_edit_binding,
@@ -3607,6 +3611,9 @@ impl ChatWidget {
36073611 self . otel_manager . counter ( "codex.thread.rename" , 1 , & [ ] ) ;
36083612 self . show_rename_prompt ( ) ;
36093613 }
3614+ SlashCommand :: Title => {
3615+ self . show_title_prompt ( ) ;
3616+ }
36103617 SlashCommand :: Model => {
36113618 self . open_model_popup ( ) ;
36123619 }
@@ -3936,6 +3943,13 @@ impl ChatWidget {
39363943 . send ( AppEvent :: CodexOp ( Op :: SetThreadName { name } ) ) ;
39373944 self . bottom_pane . drain_pending_submission_state ( ) ;
39383945 }
3946+ SlashCommand :: Title => {
3947+ let title = codex_core:: util:: normalize_thread_name ( trimmed) ;
3948+ let cell = Self :: title_confirmation_cell ( title. as_deref ( ) ) ;
3949+ self . add_boxed_history ( Box :: new ( cell) ) ;
3950+ self . set_title_override ( title) ;
3951+ self . request_redraw ( ) ;
3952+ }
39393953 SlashCommand :: Plan if !trimmed. is_empty ( ) => {
39403954 self . dispatch_command ( cmd) ;
39413955 if self . active_mode_kind ( ) != ModeKind :: Plan {
@@ -4030,6 +4044,23 @@ impl ChatWidget {
40304044 self . bottom_pane . show_view ( Box :: new ( view) ) ;
40314045 }
40324046
4047+ fn show_title_prompt ( & mut self ) {
4048+ let tx = self . app_event_tx . clone ( ) ;
4049+ let view = CustomPromptView :: new (
4050+ "Set title" . to_string ( ) ,
4051+ "Type a title and press Enter. Leave blank to clear it." . to_string ( ) ,
4052+ None ,
4053+ Box :: new ( move |name : String | {
4054+ let title = codex_core:: util:: normalize_thread_name ( & name) ;
4055+ let cell = Self :: title_confirmation_cell ( title. as_deref ( ) ) ;
4056+ tx. send ( AppEvent :: InsertHistoryCell ( Box :: new ( cell) ) ) ;
4057+ tx. send ( AppEvent :: SetTitle ( title) ) ;
4058+ } ) ,
4059+ ) ;
4060+
4061+ self . bottom_pane . show_view ( Box :: new ( view) ) ;
4062+ }
4063+
40334064 pub ( crate ) fn handle_paste ( & mut self , text : String ) {
40344065 self . bottom_pane . handle_paste ( text) ;
40354066 }
@@ -7306,6 +7337,18 @@ impl ChatWidget {
73067337 PlainHistoryCell :: new ( vec ! [ line. into( ) ] )
73077338 }
73087339
7340+ fn title_confirmation_cell ( title : Option < & str > ) -> PlainHistoryCell {
7341+ let line = match title {
7342+ Some ( title) => vec ! [
7343+ "• " . into( ) ,
7344+ "Title set to " . into( ) ,
7345+ title. to_string( ) . cyan( ) ,
7346+ ] ,
7347+ None => vec ! [ "• " . into( ) , "Title cleared" . into( ) ] ,
7348+ } ;
7349+ PlainHistoryCell :: new ( vec ! [ line. into( ) ] )
7350+ }
7351+
73097352 pub ( crate ) fn add_mcp_output ( & mut self ) {
73107353 let mcp_manager = McpManager :: new ( Arc :: new ( PluginsManager :: new (
73117354 self . config . codex_home . clone ( ) ,
@@ -7984,6 +8027,14 @@ impl ChatWidget {
79848027 self . thread_name . clone ( )
79858028 }
79868029
8030+ pub ( crate ) fn title_override ( & self ) -> Option < String > {
8031+ self . title_override . clone ( )
8032+ }
8033+
8034+ pub ( crate ) fn set_title_override ( & mut self , title : Option < String > ) {
8035+ self . title_override = title;
8036+ }
8037+
79878038 /// Returns the current thread's precomputed rollout path.
79888039 ///
79898040 /// For fresh non-ephemeral threads this path may exist before the file is
0 commit comments