Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ mod transcript_export;
mod user_verification;
mod user_verification_errors;
mod user_verification_requests;
#[cfg(test)]
#[path = "app/warnings_tests.rs"]
mod warnings_tests;
mod working_directory;

use self::agent_navigation::AgentNavigationDirection;
Expand Down Expand Up @@ -866,6 +869,7 @@ impl App {
};
if self.reconnect.offline
&& !transcript_owns_input
&& !self.chat_widget.keymap_contexts().is_warnings()
&& let TuiEvent::Key(key) = &event
&& matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat)
&& key.modifiers.contains(KeyModifiers::CONTROL)
Expand All @@ -890,6 +894,15 @@ impl App {
self.cancel_pending_key_chord();
}

if self.overlay.is_none()
&& self
.chat_widget
.handle_warning_event(&event, &self.transcript_cells)
{
self.cancel_primed_browsing_for_event(&event);
return Ok(AppRunControl::Continue);
}

let mut event = if let TuiEvent::Key(mut key_event) = event {
let escape = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE);
if self.should_recover_vim_insert_escape(key_event)
Expand Down Expand Up @@ -935,8 +948,12 @@ impl App {
}
}
if self.reconnect.offline
&& !self.chat_widget.keymap_contexts().is_warnings()
&& !matches!(&self.overlay, Some(Overlay::Transcript(_)))
&& let TuiEvent::Key(key) = &event
&& !(self.overlay.is_none()
&& self.chat_widget.no_modal_or_popup_active()
&& self.keymap.app.open_warnings.is_pressed(*key))
{
if self.reconnect.presentation == reconnect::ReconnectPresentation::Overview {
self.chat_widget.handle_disconnected_view_key(*key);
Expand Down Expand Up @@ -991,6 +1008,7 @@ impl App {
}
self.chat_widget.handle_paste(pasted);
if self.reconnect.offline
&& !self.chat_widget.keymap_contexts().is_warnings()
&& self.reconnect.presentation
== reconnect::ReconnectPresentation::Conversation
{
Expand All @@ -1014,7 +1032,7 @@ impl App {
return Ok(AppRunControl::Continue);
}
// Allow widgets to process any pending timers before rendering.
let had_active_view = self.chat_widget.has_active_view();
let had_active_modal = self.chat_widget.has_active_modal();
self.chat_widget.pre_draw_tick();
self.refresh_agents_overview_usage(app_server, tui.frame_requester());
let rendered_area = self.render_chat_widget_frame(tui, screen_size)?;
Expand All @@ -1024,8 +1042,8 @@ impl App {
{
self.request_owned_history(tui, app_server);
}
if !had_active_view
&& self.chat_widget.has_active_view()
if !had_active_modal
&& self.chat_widget.has_active_modal()
&& self.startup_protected_input_boundary
{
tui.discard_pending_input_before_interactive_screen()?;
Expand Down Expand Up @@ -1083,6 +1101,7 @@ impl App {
if tui.is_owned_screen() {
return self.render_owned_transcript(tui, screen_size);
}
self.chat_widget.sync_warnings(&self.transcript_cells);
let dashboard_visible = self
.chat_widget
.selected_index_for_present_view(AGENTS_OVERVIEW_VIEW_ID)
Expand Down
6 changes: 6 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ impl App {
&& !matches!(
&event,
AppEvent::OpenDaemonMenu
| AppEvent::OpenWarnings
| AppEvent::CopyWarning(_)
| AppEvent::ConfirmDaemonUpdate(_)
| AppEvent::RunDaemonUpdate(_)
| AppEvent::InsertHistoryCell(_)
Expand Down Expand Up @@ -309,6 +311,10 @@ impl App {
tracing::warn!(%thread_id, error = %err, "failed to load older transcript history");
}
}
AppEvent::OpenWarnings => self.chat_widget.open_warnings(&self.transcript_cells),
AppEvent::CopyWarning(text) => {
let _ = self.chat_widget.copy_transcript_selection(&text);
}
AppEvent::OpenTranscriptExportFilePrompt => {
self.chat_widget.show_transcript_export_file_prompt();
}
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/tui/src/app/file_change_approvals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl App {
grant_root: params.grant_root,
},
);
if self.startup_protected_input_boundary && !self.chat_widget.has_active_view() {
if self.startup_protected_input_boundary && !self.chat_widget.has_active_modal() {
self.startup_pending_protected_request = true;
}
}
Expand Down
5 changes: 3 additions & 2 deletions codex-rs/tui/src/app/history_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ impl App {
lines: lines.clone(),
});
}
if deferred {
// Invisible diagnostics still update the badge without replacing the rendered tail.
if deferred || lines.is_empty() {
tui.frame_requester().schedule_frame();
return;
}
Expand All @@ -85,7 +86,7 @@ impl App {
self.last_rendered_history_tail = None;
} else {
self.insert_history_cell_lines(tui, cell.as_ref(), width);
self.last_rendered_history_tail = if self.overlay.is_none() && !lines.is_empty() {
self.last_rendered_history_tail = if self.overlay.is_none() {
Some(RenderedHistoryTail {
cell: Arc::downgrade(cell),
lines,
Expand Down
15 changes: 15 additions & 0 deletions codex-rs/tui/src/app/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ impl App {
}
let contexts = self.active_keymap_contexts();
let was_pending = self.key_chord_matcher.is_pending();
if !was_pending
&& contexts.is_warnings()
&& !crate::key_hint::is_plain_text_key_event(key_event)
&& self
.keymap
.list
.action_for(key_event)
.is_some_and(|action| action != crate::keymap::ListAction::Accept)
{
return Some(key_event);
}
if !was_pending
&& contexts.contains(crate::keymap::KeymapContext::Agents)
&& self
Expand Down Expand Up @@ -602,6 +613,10 @@ impl App {
app_server: &mut AppServerSession,
key_event: KeyEvent,
) -> bool {
if self.keymap.app.open_warnings.is_pressed(key_event) {
self.chat_widget.open_warnings(&self.transcript_cells);
return true;
}
let side_toggle_bindings = &self.keymap.app.toggle_side_conversation;
if side_toggle_bindings.is_pressed(key_event)
|| side_toggle_bindings.contains(&crate::key_hint::ctrl(KeyCode::Char('/')))
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/tui/src/app/owned_transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ impl App {
tui: &mut tui::Tui,
screen_size: Size,
) -> Result<Rect> {
self.chat_widget.sync_warnings(&self.transcript_cells);
let motion = MotionMode::from_animations_enabled(self.local_settings.tui.animations);
let latest_navigation = if self.enter_returns_to_latest() {
"enter/esc latest"
Expand Down Expand Up @@ -419,6 +420,10 @@ mod tests;
#[path = "owned_transcript_input_tests.rs"]
mod input_tests;

#[cfg(test)]
#[path = "warning_notice_tests.rs"]
mod warning_notice_tests;

#[cfg(test)]
#[path = "owned_transcript_follow_tests.rs"]
mod follow_tests;
13 changes: 0 additions & 13 deletions codex-rs/tui/src/app/replay_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,6 @@ pub(super) fn snapshot_has_pending_interactive_request(snapshot: &ThreadEventSna
})
}

pub(super) fn event_is_notice(event: &ThreadBufferedEvent) -> bool {
matches!(
event,
ThreadBufferedEvent::Notification(notification)
if matches!(
notification.as_ref(),
ServerNotification::Warning(_)
| ServerNotification::GuardianWarning(_)
| ServerNotification::ConfigWarning(_)
)
)
}

/// A later turn resolves an earlier precaution; replay must preserve restored input.
pub(super) fn omit_resolved_misalignment_errors(
snapshot: &mut ThreadEventSnapshot,
Expand Down
14 changes: 9 additions & 5 deletions codex-rs/tui/src/app/resize_reflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,17 @@ impl App {
self.last_rendered_history_tail = self
.native_history
.replayable_cells(&self.transcript_cells)
.last()
.map(|cell| super::history_ui::RenderedHistoryTail {
cell: Arc::downgrade(cell),
lines: cell.display_hyperlink_lines_for_mode(
.iter()
.rev()
.find_map(|cell| {
let lines = cell.display_hyperlink_lines_for_mode(
width,
self.chat_widget.history_render_mode(),
),
);
(!lines.is_empty()).then(|| super::history_ui::RenderedHistoryTail {
cell: Arc::downgrade(cell),
lines,
})
});
if let Some(status_history) = self.last_thread_usage_status_cell.as_mut()
&& let Some(cell) = status_history.cell.upgrade()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
source: tui/src/app/warnings_tests.rs
expression: "crate::chatwidget::tests::helpers::normalize_snapshot_paths(screen)"
---
Earlier transcript row 12
Earlier transcript row 13
Earlier transcript row 14
Earlier transcript row 15
Earlier transcript row 16
Earlier transcript row 17
Earlier transcript row 18
Earlier transcript row 19
Earlier transcript row 20
Earlier transcript row 21
Earlier transcript row 22

Warnings · 1 of 1 · Warning

Example warning
Full diagnostic with remediation.







esc back · ctrl+o copy · ←/→ warning · ↓ scroll
14 changes: 7 additions & 7 deletions codex-rs/tui/src/app/startup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ impl App {
}
self.chat_widget.pre_draw_tick();
self.render_chat_widget_frame(tui, tui.terminal.last_known_screen_size)?;
if self.chat_widget.has_active_view() && self.startup_protected_input_boundary {
if self.chat_widget.has_active_modal() && self.startup_protected_input_boundary {
tui.discard_pending_input_before_interactive_screen()?;
self.startup_pending_protected_request = false;
}
Expand Down Expand Up @@ -1100,14 +1100,14 @@ See the Codex keymap documentation for supported actions and examples."
AppEvent::InsertHistoryCell(cell)
if cell.as_any().is::<history_cell::SessionInfoCell>()
);
let had_active_view = app.chat_widget.has_active_view();
let had_active_modal = app.chat_widget.has_active_modal();
match Box::pin(app.handle_event(tui, &mut app_server, event)).await {
Ok(AppRunControl::Continue) => {
if is_initial_session_header {
waiting_for_initial_session_header = false;
}
if !had_active_view
&& app.chat_widget.has_active_view()
if !had_active_modal
&& app.chat_widget.has_active_modal()
&& let Err(err) = app.render_startup_frame(tui, &app_event_rx)
{
break Err(err);
Expand Down Expand Up @@ -1244,12 +1244,12 @@ See the Codex keymap documentation for supported actions and examples."
app.primary_thread_id,
) {
waiting_for_initial_session_configured = false;
let had_active_view = app.chat_widget.has_active_view();
let had_active_modal = app.chat_widget.has_active_modal();
if let Err(err) = app.drain_active_thread_events(tui).await {
break Err(err);
}
if !had_active_view
&& app.chat_widget.has_active_view()
if !had_active_modal
&& app.chat_widget.has_active_modal()
&& let Err(err) = app.render_startup_frame(tui, &app_event_rx)
{
break Err(err);
Expand Down
30 changes: 11 additions & 19 deletions codex-rs/tui/src/app/startup_warnings.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Coalesces startup diagnostics directly below the session splash.
//! Coalesce retained startup diagnostics without adding live transcript rows or forcing reflow.

use super::*;
use crate::history_cell::StartupWarningsCell;
Expand Down Expand Up @@ -28,25 +28,29 @@ impl App {
warnings
.mcp_servers
.extend(incoming.mcp_servers.iter().cloned());
for (server, messages) in &incoming.mcp_details {
let details = warnings.mcp_details.entry(server.clone()).or_default();
for message in messages {
if !details.contains(message) {
details.push(message.clone());
}
}
}
// The final MCP summary must retain the individual diagnostics' sign-in subset.
warnings
.sign_in_servers
.extend(incoming.sign_in_servers.iter().cloned());
if warnings.messages.is_empty() {
return;
}
warnings.transcript_hint = crate::keymap::primary_binding(&self.keymap.app.open_transcript)
.map(|binding| binding.display_label());
let header = self
.transcript_cells
.iter()
.rposition(|cell| cell.as_any().is::<history_cell::SessionInfoCell>());
warnings.pending_header = header.is_none() && self.chat_widget.thread_id().is_none();
let ready_to_render = !warnings.pending_header;
let warnings: Arc<dyn HistoryCell> = Arc::new(warnings);
self.transcript_cells.insert(
header.map_or(/*default*/ 0, |index| index + 1),
warnings.clone(),
Arc::new(warnings),
);
self.native_history.retain(&self.transcript_cells);
if let Some(Overlay::Transcript(overlay)) = &mut self.overlay {
Expand All @@ -55,18 +59,6 @@ impl App {
if self.backtrack.overlay_preview_active {
self.apply_backtrack_selection_internal(self.backtrack.nth_user_message);
}
if ready_to_render {
if let Some(buffer) = self.initial_history_replay_buffer.as_mut() {
buffer.retained_lines.clear();
buffer.render_from_transcript_tail = true;
return;
}
self.schedule_immediate_resize_reflow(tui);
if let Err(error) =
self.maybe_run_resize_reflow(tui, tui.terminal.last_known_screen_size)
{
tracing::warn!(%error, "failed to refresh startup warnings");
}
}
tui.frame_requester().schedule_frame();
}
}
Loading
Loading