diff --git a/codex-rs/tui/src/app/config_persistence.rs b/codex-rs/tui/src/app/config_persistence.rs index 220026e4d4..0e0a1402f4 100644 --- a/codex-rs/tui/src/app/config_persistence.rs +++ b/codex-rs/tui/src/app/config_persistence.rs @@ -1942,6 +1942,7 @@ enabled = false app.chat_widget .handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/app/owned_transcript_tests.rs b/codex-rs/tui/src/app/owned_transcript_tests.rs index e8b1919943..8ffbc10d30 100644 --- a/codex-rs/tui/src/app/owned_transcript_tests.rs +++ b/codex-rs/tui/src/app/owned_transcript_tests.rs @@ -35,6 +35,7 @@ pub(in crate::app) fn user_cell(message: &str) -> Arc { pub(in crate::app) fn attach_thread(app: &mut App, thread_id: ThreadId) { app.chat_widget.handle_thread_session(ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/reconnect.rs b/codex-rs/tui/src/app/reconnect.rs index e08871c026..fb82daa3fc 100644 --- a/codex-rs/tui/src/app/reconnect.rs +++ b/codex-rs/tui/src/app/reconnect.rs @@ -485,6 +485,9 @@ impl App { store.pending_interactive_replay = Default::default(); store.pending_interrupt_turn_id = None; store.active = false; + if let Some(session) = &mut store.session { + session.read_only_history = true; + } store .buffer .retain(|event| matches!(event, ThreadBufferedEvent::Notification(_))); diff --git a/codex-rs/tui/src/app/session_lifecycle.rs b/codex-rs/tui/src/app/session_lifecycle.rs index b68e011730..e6590b5227 100644 --- a/codex-rs/tui/src/app/session_lifecycle.rs +++ b/codex-rs/tui/src/app/session_lifecycle.rs @@ -837,6 +837,9 @@ impl App { display, }); } + if is_replay_only && let Some(session) = snapshot.session.as_mut() { + session.read_only_history = true; + } self.render_thread_snapshot(tui, app_server, thread_id, snapshot, !is_replay_only)?; if !is_replay_only && let Some(message) = self diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index e0c965f7d7..0544ded881 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -7469,6 +7469,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re let primary_cwd = test_path_buf("/tmp/main").abs(); let shared_root = test_path_buf("/tmp/shared").abs(); let primary_session = ThreadSessionState { + read_only_history: true, daybreak_enabled: false, windows_sandbox_host: WindowsSandboxHost::Local, approval_policy: AskForApproval::OnRequest, @@ -7546,6 +7547,7 @@ async fn inactive_thread_started_notification_initializes_replay_session() -> Re let session = store.session.clone().expect("inferred session"); drop(store); + assert!(!session.read_only_history); assert_eq!(session.thread_id, agent_thread_id); assert!(session.daybreak_enabled); assert_eq!(session.windows_sandbox_host, WindowsSandboxHost::Remote); @@ -7674,7 +7676,6 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { runtime_workspace_roots: vec![primary_cwd.clone()], ..test_thread_session(main_thread_id, primary_cwd.to_path_buf()) }; - app.primary_session_configured = Some(primary_session); let thread = Thread { originator: None, @@ -7712,10 +7713,17 @@ async fn thread_read_session_state_does_not_reuse_primary_permission_profile() { turns: Vec::new(), }; + let cold_session = app + .session_state_for_thread_read(read_thread_id, &thread) + .await; + assert!(cold_session.read_only_history); + + app.primary_session_configured = Some(primary_session); let session = app .session_state_for_thread_read(read_thread_id, &thread) .await; + assert!(session.read_only_history); assert_eq!(session.thread_id, read_thread_id); assert_eq!(session.model, "server-reported-model"); assert_eq!(session.cwd.as_path(), test_path_buf("/tmp/read").as_path()); @@ -8820,6 +8828,7 @@ async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String { }; let make_header = |is_first| -> Arc { let session = ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -9533,6 +9542,7 @@ async fn replace_goal_confirmation_snapshot() { pub(super) fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState { ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -10849,6 +10859,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() { let make_header = |is_first| { let session = ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -10922,6 +10933,7 @@ async fn backtrack_selection_preserves_selected_prompt_and_requests_branch() { let base_id = ThreadId::new(); app.chat_widget .handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: base_id, @@ -12318,6 +12330,7 @@ async fn new_session_requests_shutdown_for_previous_conversation() { let thread_id = ThreadId::new(); let event = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -13097,6 +13110,7 @@ async fn clear_only_ui_reset_preserves_chat_session_state() { let thread_id = ThreadId::new(); app.chat_widget .handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index 0f52a18283..0e3a2b6094 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -1873,6 +1873,73 @@ async fn make_history_test_app() -> Result<(Box, tempfile::TempDir)> { Ok((app, codex_home)) } +#[tokio::test] +async fn read_only_history_hides_the_statusline_until_interactive_resume() -> Result<()> { + let (mut app, _codex_home) = make_history_test_app().await?; + let thread_id = create_history_rollout(&app.config, ThreadHistoryMode::Legacy, "saved prompt")?; + app.config + .permissions + .set_permission_profile(PermissionProfile::read_only())?; + let mut server = crate::start_embedded_app_server_for_picker(&app.config).await?; + app.chat_widget.setup_status_line( + vec![crate::bottom_pane::StatusLineItem::SessionId], + /*use_theme_colors*/ false, + ); + + app.enqueue_primary_thread_session( + test_thread_session(thread_id, app.config.cwd.to_path_buf()), + Vec::new(), + ) + .await?; + assert!(render_bottom_popup(&app.chat_widget, /*width*/ 100).contains(&thread_id.to_string())); + + let (view, _) = server + .read_thread_for_viewing(&app.config, &app.local_settings, thread_id) + .await?; + app.enqueue_primary_thread_session(view.session, view.turns) + .await?; + let footer = render_bottom_popup(&app.chat_widget, /*width*/ 100); + insta::assert_snapshot!(footer, @r" + › Ask Codex to do anything + + ? for shortcuts + "); + + for window in [Some(100_000), None] { + app.chat_widget.handle_server_notification( + token_usage_notification(thread_id, "turn-1", window), + /*replay_kind*/ None, + ); + assert_eq!(render_bottom_popup(&app.chat_widget, /*width*/ 100), footer); + } + + let live = server + .resume_thread( + &app.local_settings, + app.config.clone(), + thread_id, + app.resume_model_settings(), + ) + .await?; + assert_eq!( + live.session.permission_profile, + PermissionProfile::read_only() + ); + app.enqueue_primary_thread_session(live.session, live.turns) + .await?; + let footer = render_bottom_popup(&app.chat_widget, /*width*/ 100); + assert!(footer.contains(&thread_id.to_string()), "{footer}"); + app.chat_widget + .setup_status_line(Vec::new(), /*use_theme_colors*/ false); + app.chat_widget.handle_server_notification( + token_usage_notification(thread_id, "turn-2", /*model_context_window*/ None), + /*replay_kind*/ None, + ); + assert!(render_bottom_popup(&app.chat_widget, /*width*/ 100).contains("10 used")); + server.shutdown().await?; + Ok(()) +} + #[tokio::test] async fn cloud_resume_with_current_directory_needs_no_cwd_override() -> Result<()> { use codex_config::types::ResumeCwdMode; diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__unavailable_conversation.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__unavailable_conversation.snap index 335071a077..b3e67c6251 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__unavailable_conversation.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__active_reconnect__unavailable_conversation.snap @@ -6,5 +6,3 @@ expression: "format!(\"{history}\\n{}\",\nrender_bottom_popup(&app.chat_widget,\ ⚠ A background Codex service is running v2.0.0, different from your Codex CLI v0.0.0. › unavailable draft - - gpt-test default · /tmp/project diff --git a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__reconnected_unavailable_conversation.snap b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__reconnected_unavailable_conversation.snap index 432d5cf464..001edd53b3 100644 --- a/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__reconnected_unavailable_conversation.snap +++ b/codex-rs/tui/src/app/tests/snapshots/codex_tui__app__tests__navigation_reconnect__reconnected_unavailable_conversation.snap @@ -7,5 +7,3 @@ Cached previous conversation • This conversation is unavailable. Its cached transcript and draft remain here; input is paused. Open the agent picker or return to the parent to continue. • The rename target disappeared. Unsubmitted title: Keep this task draft! › latest draft - - gpt-test default · /tmp/project diff --git a/codex-rs/tui/src/app/thread_events.rs b/codex-rs/tui/src/app/thread_events.rs index 05439859cf..3d0079c2a4 100644 --- a/codex-rs/tui/src/app/thread_events.rs +++ b/codex-rs/tui/src/app/thread_events.rs @@ -744,6 +744,7 @@ mod tests { fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState { ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app/thread_routing.rs b/codex-rs/tui/src/app/thread_routing.rs index 44a1f64e0e..c579aef87d 100644 --- a/codex-rs/tui/src/app/thread_routing.rs +++ b/codex-rs/tui/src/app/thread_routing.rs @@ -1430,6 +1430,7 @@ impl App { notification: &ThreadStartedNotification, ) -> Option { let mut session = self.primary_session_configured.clone()?; + session.read_only_history = false; session.thread_id = thread_id; // The primary session supplies defaults. The new thread supplies its own fork parent. session.set_fork_parent(notification.thread.forked_from_id.as_deref()); diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index b1ab8ad1af..ea3b3b1138 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -78,6 +78,7 @@ impl App { let permission_profile = self.current_permission_profile(); let active_permission_profile = self.current_active_permission_profile(); let mut session = if let Some(mut session) = self.primary_session_configured.clone() { + session.read_only_history = true; if session.thread_id != thread_id { // `thread/read` does not include all thread settings, so do not carry // thread-scoped state from the currently active session. @@ -87,6 +88,7 @@ impl App { session } else { ThreadSessionState { + read_only_history: true, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -174,6 +176,7 @@ mod tests { fn test_thread_session(thread_id: ThreadId, cwd: PathBuf) -> ThreadSessionState { ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ec48e09b41..d2676ef21b 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -3111,6 +3111,7 @@ async fn thread_session_state_from_thread_response( ); let (log_id, entry_count) = codex_message_history::history_metadata(&history_config).await; Ok(ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host, thread_id, diff --git a/codex-rs/tui/src/app_server_session/rollout_history.rs b/codex-rs/tui/src/app_server_session/rollout_history.rs index b0dcacca88..7ed5abcbe8 100644 --- a/codex-rs/tui/src/app_server_session/rollout_history.rs +++ b/codex-rs/tui/src/app_server_session/rollout_history.rs @@ -76,7 +76,7 @@ impl AppServerSession { "Showing up to 100 recent prompts and final replies. Intermediate messages and tool activity are unavailable.", ); } - let session = thread_session_state_from_thread_response( + let mut session = thread_session_state_from_thread_response( &thread.id, crate::windows_sandbox::host_from_environments(thread.environments.as_deref()), thread.forked_from_id.clone(), @@ -97,6 +97,7 @@ impl AppServerSession { ) .await .map_err(color_eyre::eyre::Report::msg)?; + session.read_only_history = true; self.freeze_viewed_receipt(thread_id); Ok(( AppServerStartedThread { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e489134a1b..23e499ebb1 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -790,7 +790,7 @@ impl ChatComposer { flash: None, context_window_percent: None, context_window_used_tokens: None, - context_window_pending: false, + context_window_hidden: false, collaboration_mode_indicator: None, goal_status_indicator: None, ide_context_active: false, @@ -1496,7 +1496,7 @@ impl ChatComposer { } fn right_footer_line_with_context(&self) -> Line<'static> { - let mut line = if self.footer.context_window_pending { + let mut line = if self.footer.context_window_hidden { Line::default() } else { context_window_line( @@ -1505,7 +1505,9 @@ impl ChatComposer { ) }; if let Some(vim_mode) = self.vim_mode_indicator_span() { - line.spans.push(" | ".dim()); + if !line.spans.is_empty() { + line.spans.push(" | ".dim()); + } line.spans.push(vim_mode); } line @@ -4472,8 +4474,8 @@ impl ChatComposer { self.footer.context_window_used_tokens = used_tokens; } - pub(crate) fn set_context_window_pending(&mut self, pending: bool) { - self.footer.context_window_pending = pending; + pub(crate) fn set_context_window_hidden(&mut self, hidden: bool) { + self.footer.context_window_hidden = hidden; } pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/embedded_input_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/embedded_input_tests.rs index 0a79c52063..b5589bfee1 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/embedded_input_tests.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/embedded_input_tests.rs @@ -55,7 +55,7 @@ fn embedded_composer_shows_validation_error_without_losing_draft() { let (mut composer, mut rx) = new_test_composer(); composer.config = ChatComposerConfig::plain_text(); composer.set_footer_hint_override(Some(vec![("enter".into(), "submit".into())])); - composer.set_context_window_pending(/*pending*/ true); + composer.set_context_window_hidden(/*hidden*/ true); let text = "x".repeat(MAX_USER_INPUT_TEXT_CHARS + 1); composer.handle_paste(text.clone()); assert_eq!( diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs index 6d6efe7d02..a2daadb6c2 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/footer_state.rs @@ -379,7 +379,7 @@ pub(super) struct FooterState { pub(super) flash: Option, pub(super) context_window_percent: Option, pub(super) context_window_used_tokens: Option, - pub(super) context_window_pending: bool, + pub(super) context_window_hidden: bool, pub(super) collaboration_mode_indicator: Option, pub(super) goal_status_indicator: Option, pub(super) ide_context_active: bool, diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/passive_hints.rs b/codex-rs/tui/src/bottom_pane/chat_composer/passive_hints.rs index 4aac82f219..51ce6f3fec 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/passive_hints.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/passive_hints.rs @@ -329,7 +329,7 @@ impl ChatComposer { } let show_warning = self.show_warning_notice(options); if !self.footer.status_line_enabled - && !self.footer.context_window_pending + && !self.footer.context_window_hidden && self.footer.side_conversation_context_label.is_none() && !self.is_bang_shell_command() { diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs index 3ad9a7aeff..9a02def3c6 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/status_surface_tests.rs @@ -871,7 +871,7 @@ fn oversized_goal_yields_to_an_eligible_whole_hint_with_status_disabled() { Some("100% context left") ); - composer.set_context_window_pending(/*pending*/ true); + composer.set_context_window_hidden(/*hidden*/ true); composer.set_goal_status_indicator(Some(GoalStatusIndicator::Paused)); let (text, _) = render(&composer, /*width*/ 18, /*footer*/ None); assert_eq!(text.lines().last().map(str::trim), Some("? for shortcuts")); diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 005dc50be8..b4e79d173e 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -1503,8 +1503,8 @@ impl BottomPane { self.request_redraw(); } - pub(crate) fn set_context_window_pending(&mut self, pending: bool) { - self.composer.set_context_window_pending(pending); + pub(crate) fn set_context_window_hidden(&mut self, hidden: bool) { + self.composer.set_context_window_hidden(hidden); self.request_redraw(); } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index fe00942844..4380cb2353 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -688,6 +688,7 @@ pub(crate) struct ChatWidget { active_side_conversation: bool, blocks_direct_input: bool, external_writer_view: bool, + read_only_history: bool, provider_error: Option, /// Covers both queued and executing forks so repeated shortcuts cannot queue another one. pub(crate) fork_in_progress: bool, @@ -1161,13 +1162,17 @@ impl ChatWidget { self.config.memories.generate_memories = generate_memories; } + fn sync_context_window_visibility(&mut self) { + self.bottom_pane + .set_context_window_hidden(self.read_only_history || self.token_usage_pending); + } + pub(crate) fn set_token_info(&mut self, info: Option) { match info { Some(info) => self.apply_token_info(info), None => { self.token_usage_pending = true; - self.bottom_pane - .set_context_window_pending(/*pending*/ true); + self.sync_context_window_visibility(); self.bottom_pane .set_context_window(/*percent*/ None, /*used_tokens*/ None); self.token_info = None; @@ -1177,8 +1182,7 @@ impl ChatWidget { fn apply_token_info(&mut self, info: TokenUsageInfo) { self.token_usage_pending = false; - self.bottom_pane - .set_context_window_pending(/*pending*/ false); + self.sync_context_window_visibility(); let percent = self.context_remaining_percent(&info); let used_tokens = self.context_used_tokens(&info, percent.is_some()); self.bottom_pane.set_context_window(percent, used_tokens); diff --git a/codex-rs/tui/src/chatwidget/constructor.rs b/codex-rs/tui/src/chatwidget/constructor.rs index 7f23d54cde..8eff912f62 100644 --- a/codex-rs/tui/src/chatwidget/constructor.rs +++ b/codex-rs/tui/src/chatwidget/constructor.rs @@ -246,6 +246,7 @@ impl ChatWidget { active_side_conversation: false, blocks_direct_input: false, external_writer_view: false, + read_only_history: false, provider_error: None, fork_in_progress: false, misalignment_policy_violation: None, diff --git a/codex-rs/tui/src/chatwidget/reconnect.rs b/codex-rs/tui/src/chatwidget/reconnect.rs index dc2855b78b..262d1ad920 100644 --- a/codex-rs/tui/src/chatwidget/reconnect.rs +++ b/codex-rs/tui/src/chatwidget/reconnect.rs @@ -130,6 +130,9 @@ impl ChatWidget { } pub(crate) fn pause_unavailable_thread(&mut self) { + self.read_only_history = true; + self.sync_context_window_visibility(); + self.refresh_status_line(); if let Some(questions) = &mut self.bottom_pane.questions { questions.delivery_enabled = false; } diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index f9d4ca9827..c6e21f61eb 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -96,6 +96,8 @@ impl ChatWidget { let connector_scope_changed = previous_thread_id != Some(session.thread_id) || self.config.cwd.as_path() != session.cwd.as_path(); self.thread_id = Some(session.thread_id); + self.read_only_history = session.read_only_history; + self.sync_context_window_visibility(); self.daybreak_enabled = self.config.features.enabled(Feature::CliDaybreak) && session.daybreak_enabled && !matches!(display, SessionConfiguredDisplay::SideConversation); diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 551cc6d238..79f1392f01 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -465,6 +465,9 @@ impl ChatWidget { /// /// Unknown ids are deduplicated in insertion order for warning messages. fn status_line_items_with_invalids(&self) -> (Vec, Vec) { + if self.read_only_history { + return Default::default(); + } parse_items_with_invalids(self.configured_status_line_items()) } diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index f10acfc397..b282c88491 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -47,6 +47,7 @@ fn thread_settings_for_test( fn configured_thread_session(thread_id: ThreadId) -> crate::session_state::ThreadSessionState { crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index bc41cfa14e..23834399dc 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -629,6 +629,7 @@ async fn submission_preserves_text_elements_and_local_images() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -739,6 +740,7 @@ async fn submission_includes_configured_active_permission_profile() { }; let expected_active_permission_profile = ActivePermissionProfile::new("custom"); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -795,6 +797,7 @@ async fn submission_omits_active_permission_profile_for_legacy_snapshot() { file_system: ManagedFileSystemPermissions::Unrestricted, }; let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -841,6 +844,7 @@ async fn submission_with_remote_and_local_images_keeps_local_placeholder_numberi let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -942,6 +946,7 @@ async fn enter_with_only_remote_images_submits_user_turn() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1012,6 +1017,7 @@ async fn shift_enter_with_only_remote_images_does_not_submit_user_turn() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1054,6 +1060,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_modal_is_active() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1096,6 +1103,7 @@ async fn enter_with_only_remote_images_does_not_submit_when_input_disabled() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1141,6 +1149,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 735e01f04c..229667c4af 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1269,6 +1269,7 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/history_replay.rs b/codex-rs/tui/src/chatwidget/tests/history_replay.rs index 0eac898bc3..59ac521103 100644 --- a/codex-rs/tui/src/chatwidget/tests/history_replay.rs +++ b/codex-rs/tui/src/chatwidget/tests/history_replay.rs @@ -22,6 +22,7 @@ async fn resumed_initial_messages_render_history() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let mut configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: true, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -184,6 +185,7 @@ async fn restored_conversation_ultra_remains_selected_after_switching_to_plan() chat.set_plan_mode_reasoning_effort(Some(ReasoningEffortConfig::High)); chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -507,6 +509,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -580,6 +583,7 @@ async fn replayed_user_message_preserves_remote_image_urls() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -687,6 +691,7 @@ async fn session_configured_syncs_widget_config_permissions_and_cwd() { .expect("permission profile should project to legacy sandbox policy"); let expected_sandbox = SandboxPolicy::from(expected_core_sandbox); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -768,6 +773,7 @@ async fn session_configured_preserves_profile_workspace_roots() { let session_permission_profile = PermissionProfile::workspace_write() .materialize_project_roots_with_workspace_roots(&session_effective_workspace_roots); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -816,6 +822,7 @@ async fn session_configured_external_sandbox_keeps_external_runtime_policy() { network_access: NetworkAccess::Restricted, }; let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -858,6 +865,7 @@ async fn replayed_user_message_with_only_remote_images_renders_history_cell() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -919,6 +927,7 @@ async fn replayed_user_message_with_only_local_images_renders_history_cell() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1277,6 +1286,7 @@ async fn replayed_reasoning_item_preserves_summary_parts_and_hides_raw_reasoning let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.show_raw_agent_reasoning = false; chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1329,6 +1339,7 @@ async fn replayed_reasoning_item_shows_raw_reasoning_when_enabled() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.config.show_raw_agent_reasoning = true; chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index ba06f624ed..635962a9a0 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -1162,6 +1162,7 @@ async fn required_windows_sandbox_setup_defers_configured_initial_prompt() { create_initial_user_message(Some(initial_prompt.clone()), Vec::new(), Vec::new()); chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1612,6 +1613,7 @@ async fn permissions_selection_marks_auto_review_current_after_session_configure .set_enabled(Feature::GuardianApproval, /*enabled*/ true); chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), @@ -1666,6 +1668,7 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w let permission_profile = app_server_workspace_write_profile(extra_root); chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs index a87c0edbb6..d3864cc99d 100644 --- a/codex-rs/tui/src/chatwidget/tests/plan_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/plan_mode.rs @@ -16,6 +16,7 @@ fn paste_hidden_plan_shell_payload(chat: &mut ChatWidget) -> String { fn plan_test_session(thread_id: ThreadId) -> crate::session_state::ThreadSessionState { crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, @@ -1229,6 +1230,7 @@ async fn submit_user_message_emits_structured_plugin_mentions_from_bindings() { let thread_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); let configured = crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 2b56756645..f75df543d4 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -4719,6 +4719,7 @@ async fn session_configured_clears_goal_status_footer() { let rollout_file = NamedTempFile::new().unwrap(); chat.handle_thread_session(crate::session_state::ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 14ac4d99f5..dc317ebbad 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -622,6 +622,7 @@ fn image_generation_call_renders_saved_path() { fn session_configured_event(model: &str) -> ThreadSessionState { ThreadSessionState { + read_only_history: false, daybreak_enabled: false, windows_sandbox_host: crate::app::WindowsSandboxHost::Local, thread_id: ThreadId::new(), diff --git a/codex-rs/tui/src/session_state.rs b/codex-rs/tui/src/session_state.rs index 62e55d2732..3c13dc0dfa 100644 --- a/codex-rs/tui/src/session_state.rs +++ b/codex-rs/tui/src/session_state.rs @@ -27,6 +27,8 @@ pub(crate) struct MessageHistoryMetadata { #[derive(Debug, Clone, PartialEq)] pub(crate) struct ThreadSessionState { + /// This TUI opened saved history without a live attachment. + pub(crate) read_only_history: bool, pub(crate) daybreak_enabled: bool, pub(crate) windows_sandbox_host: crate::app::WindowsSandboxHost, pub(crate) thread_id: ThreadId, diff --git a/codex-rs/tui/src/startup_draft.rs b/codex-rs/tui/src/startup_draft.rs index e55d014b18..95ad288d80 100644 --- a/codex-rs/tui/src/startup_draft.rs +++ b/codex-rs/tui/src/startup_draft.rs @@ -544,7 +544,7 @@ fn startup_draft_bottom_pane( ..ChatComposerConfig::plain_text() }, ); - bottom_pane.set_context_window_pending(/*pending*/ true); + bottom_pane.set_context_window_hidden(/*hidden*/ true); bottom_pane }