Centralize TUI read-only view checks (#61353)

## What changed

Add `ChatWidget::is_read_only_view()` and use it for read-only guards across input handling, navigation, transcript rendering, and session restoration. The new helper delegates to `is_external_writer_view()`, preserving existing behavior.

GitOrigin-RevId: c8fd66526f502d79be306a19ee261e79c5abcd57
This commit is contained in:
Benjamin Carlsson
2026-11-13 18:50:28 +00:00
committed by copyberry
parent 4ccf87254a
commit 2bad26e03f
12 changed files with 22 additions and 20 deletions
+1 -1
View File
@@ -1208,7 +1208,7 @@ impl App {
{
if self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
&& self.chat_widget.is_external_writer_view()
&& self.chat_widget.is_read_only_view()
&& (crate::key_hint::plain(KeyCode::Esc).is_press(*key)
|| (crate::key_hint::plain(KeyCode::Left).is_press(*key)
&& self.chat_widget.agents_navigation_key_available()))
+4 -6
View File
@@ -562,8 +562,7 @@ impl App {
}
let is_new_session = started.is_some();
if self.current_displayed_thread_id() == Some(root_thread_id)
&& (!self.thread_unavailable(root_thread_id)
|| self.chat_widget.is_external_writer_view())
&& (!self.thread_unavailable(root_thread_id) || self.chat_widget.is_read_only_view())
{
// Keep the displayed read-only snapshot frozen; R explicitly retries attachment.
if let Ok(mut state) = self.agents_overview.view_state.lock() {
@@ -1003,8 +1002,7 @@ impl App {
}
if self.current_displayed_thread_id() != Some(root_thread_id)
|| (self.thread_unavailable(root_thread_id)
&& !self.chat_widget.is_external_writer_view())
|| (self.thread_unavailable(root_thread_id) && !self.chat_widget.is_read_only_view())
{
self.select_agent_thread(tui, app_server, root_thread_id)
.await?;
@@ -1012,7 +1010,7 @@ impl App {
if self.current_displayed_thread_id() == Some(root_thread_id) {
self.remember_attached_host_project(app_server);
}
let read_only = self.chat_widget.is_external_writer_view();
let read_only = self.chat_widget.is_read_only_view();
if !read_only {
self.replay_agents_overview_requests(app_server, root_thread_id)
.await;
@@ -1049,7 +1047,7 @@ impl App {
mut input: ThreadInputState,
pending_settings: Option<codex_app_server_protocol::ThreadSettingsUpdatedNotification>,
) {
let preserve_in_flight_turn = !self.chat_widget.is_external_writer_view()
let preserve_in_flight_turn = !self.chat_widget.is_read_only_view()
&& self.active_turn_id_for_thread(thread_id).await.is_some();
self.reconcile_saved_session_input(thread_id, &mut input)
.await;
+3 -3
View File
@@ -476,7 +476,7 @@ impl App {
_ => {}
}
}
if self.chat_widget.is_external_writer_view()
if self.chat_widget.is_read_only_view()
&& self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
&& key_event.kind == KeyEventKind::Press
@@ -680,7 +680,7 @@ impl App {
return;
}
if self.chat_widget.is_external_writer_view()
if self.chat_widget.is_read_only_view()
&& self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
{
@@ -852,7 +852,7 @@ impl App {
}
pub(crate) fn should_handle_backtrack_esc(&self, key_event: KeyEvent) -> bool {
!self.chat_widget.is_external_writer_view()
!self.chat_widget.is_read_only_view()
&& !self.chat_widget.has_provider_error()
&& !self.chat_widget.side_conversation_active()
&& !self.chat_widget.shortcut_overlay_visible()
+2 -2
View File
@@ -562,7 +562,7 @@ impl App {
None => (None, None),
};
let mut composer_gap = (!self.chat_widget.has_active_view()
&& !self.chat_widget.is_external_writer_view())
&& !self.chat_widget.is_read_only_view())
.then(crate::bottom_pane::ComposerGap::default);
let prompt_footer = if self.pane_resize.is_some() {
Some(crate::bottom_pane::TranscriptFooter {
@@ -986,7 +986,7 @@ impl App {
// Search and selection still consume Escape first to dismiss their interaction.
if let TuiEvent::Key(key) = event
&& !self.transcript_view.has_active_interaction()
&& (self.chat_widget.is_external_writer_view() || self.chat_widget.has_provider_error())
&& (self.chat_widget.is_read_only_view() || self.chat_widget.has_provider_error())
&& crate::key_hint::plain(KeyCode::Esc).is_press(*key)
{
return Ok(false);
+1 -1
View File
@@ -552,7 +552,7 @@ impl App {
return Err(err);
}
self.config = self.chat_widget.config_ref().clone();
if self.thread_unavailable(id) && !self.chat_widget.is_external_writer_view() {
if self.thread_unavailable(id) && !self.chat_widget.is_read_only_view() {
self.agent_navigation.mark_stopped(id);
self.chat_widget.pause_unavailable_thread();
self.chat_widget.add_info_message("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.".into(), /*hint*/ None);
+1 -1
View File
@@ -1825,7 +1825,7 @@ impl App {
&& owned_channel.is_some()
&& (refreshing_history
|| was_reconnecting
|| self.chat_widget.is_external_writer_view()
|| self.chat_widget.is_read_only_view()
|| self.chat_widget.has_provider_error()))
.then(|| self.chat_widget.capture_thread_input_state())
.flatten();
+1 -1
View File
@@ -156,7 +156,7 @@ impl App {
if !self.local_settings.tui.show_tooltips
|| !self.chat_widget.no_modal_or_popup_active()
|| !self.chat_widget.composer_is_empty()
|| self.chat_widget.is_external_writer_view()
|| self.chat_widget.is_read_only_view()
|| self.chat_widget.has_queued_follow_up_messages()
|| self.backtrack.primed
|| self.backtrack.overlay_preview_active
@@ -96,7 +96,7 @@ impl App {
kind: KeyEventKind::Press,
..
}) => {
if self.chat_widget.is_external_writer_view() {
if self.chat_widget.is_read_only_view() {
self.close_transcript_overlay(tui);
} else {
self.begin_overlay_backtrack_preview(tui);
+4
View File
@@ -1899,6 +1899,10 @@ impl ChatWidget {
self.external_writer_view
}
pub(crate) fn is_read_only_view(&self) -> bool {
self.is_external_writer_view()
}
fn refresh_skills_for_current_cwd(&mut self, force_reload: bool) {
self.submit_op(AppCommand::list_skills(
vec![self.config.cwd.to_path_buf()],
@@ -29,7 +29,7 @@ impl ChatWidget {
match self.external_editor_state {
ExternalEditorState::Requested | ExternalEditorState::Active => None,
ExternalEditorState::Closed
if !self.external_writer_view && !self.has_provider_error() =>
if !self.is_read_only_view() && !self.has_provider_error() =>
{
self.bottom_pane.empty_state_composer()
}
+2 -2
View File
@@ -471,7 +471,7 @@ impl ChatWidget {
}
pub(crate) fn can_launch_external_editor(&self) -> bool {
!self.external_writer_view && self.bottom_pane.can_launch_external_editor()
!self.is_read_only_view() && self.bottom_pane.can_launch_external_editor()
}
pub(crate) fn can_run_ctrl_l_clear_now(&mut self) -> bool {
@@ -596,7 +596,7 @@ impl ChatWidget {
pub(crate) fn handle_paste(&mut self, text: String) {
self.cancel_quick_dial_menu();
if (self.external_writer_view || self.has_provider_error())
if (self.is_read_only_view() || self.has_provider_error())
&& !self.bottom_pane.has_active_view()
{
return;
+1 -1
View File
@@ -203,7 +203,7 @@ impl ChatWidget {
key: KeyEvent,
mode: RestrictedInputMode,
) -> KeyEventAction {
if self.external_writer_view {
if self.is_read_only_view() {
return KeyEventAction::None;
}
if mode == RestrictedInputMode::DraftHandoff {