From a606e85859e88c56ba47fe8c2884fd15358ef722 Mon Sep 17 00:00:00 2001 From: Won Park Date: Mon, 23 Feb 2026 09:11:05 -0800 Subject: [PATCH] tweaked /clear to support clear + new chat, also fix minor bug for macos terminal (#12520) # /clear feature! Use /clear to start a new chat with Codex on a clean terminal! --- codex-rs/tui/src/app.rs | 120 +++++++++++++++------------- codex-rs/tui/src/app_event.rs | 3 +- codex-rs/tui/src/custom_terminal.rs | 9 ++- codex-rs/tui/src/slash_command.rs | 2 +- 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 5f765e24e..a5cfbdb63 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -704,27 +704,18 @@ impl App { self.clear_ui_header_lines_with_version(width, CODEX_CLI_VERSION) } - fn clear_terminal_ui(&mut self, tui: &mut tui::Tui) -> Result<()> { + fn clear_terminal_ui(&mut self, tui: &mut tui::Tui, redraw_header: bool) -> Result<()> { let is_alt_screen_active = tui.is_alt_screen_active(); - let use_apple_terminal_clear_workaround = !is_alt_screen_active - && matches!( - codex_core::terminal::terminal_info().name, - codex_core::terminal::TerminalName::AppleTerminal - ); // Drop queued history insertions so stale transcript lines cannot be flushed after /clear. tui.clear_pending_history_lines(); if is_alt_screen_active { tui.terminal.clear_visible_screen()?; - } else if use_apple_terminal_clear_workaround { - // Terminal.app can leave mixed old/new glyphs behind when we purge + clear. - // Use a stricter ANSI reset, then redraw only a fresh session header box instead of - // replaying the initialization transcript preamble. - tui.terminal.clear_scrollback_and_visible_screen_ansi()?; } else { - tui.terminal.clear_scrollback()?; - tui.terminal.clear_visible_screen()?; + // Some terminals (Terminal.app, Warp) do not reliably drop scrollback when purge and + // clear are emitted as separate backend commands. Prefer a single ANSI sequence. + tui.terminal.clear_scrollback_and_visible_screen_ansi()?; } let mut area = tui.terminal.viewport_area; @@ -736,11 +727,13 @@ impl App { } self.has_emitted_history_lines = false; - let width = tui.terminal.last_known_screen_size.width; - let header_lines = self.clear_ui_header_lines(width); - if !header_lines.is_empty() { - tui.insert_history_lines(header_lines); - self.has_emitted_history_lines = true; + if redraw_header { + let width = tui.terminal.last_known_screen_size.width; + let header_lines = self.clear_ui_header_lines(width); + if !header_lines.is_empty() { + tui.insert_history_lines(header_lines); + self.has_emitted_history_lines = true; + } } Ok(()) } @@ -1048,6 +1041,48 @@ impl App { self.pending_primary_events.clear(); } + async fn start_fresh_session_with_summary_hint(&mut self, tui: &mut tui::Tui) { + // Start a fresh in-memory session while preserving resumability via persisted rollout + // history. + let model = self.chat_widget.current_model().to_string(); + let summary = session_summary( + self.chat_widget.token_usage(), + self.chat_widget.thread_id(), + self.chat_widget.thread_name(), + ); + self.shutdown_current_thread().await; + if let Err(err) = self.server.remove_and_close_all_threads().await { + tracing::warn!(error = %err, "failed to close all threads"); + } + let init = crate::chatwidget::ChatWidgetInit { + config: self.config.clone(), + frame_requester: tui.frame_requester(), + app_event_tx: self.app_event_tx.clone(), + // New sessions start without prefilled message content. + initial_user_message: None, + enhanced_keys_supported: self.enhanced_keys_supported, + auth_manager: self.auth_manager.clone(), + models_manager: self.server.get_models_manager(), + feedback: self.feedback.clone(), + is_first_run: false, + feedback_audience: self.feedback_audience, + model: Some(model), + status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(), + otel_manager: self.otel_manager.clone(), + }; + self.chat_widget = ChatWidget::new(init, self.server.clone()); + self.reset_thread_event_state(); + if let Some(summary) = summary { + let mut lines: Vec> = vec![summary.usage_line.clone().into()]; + if let Some(command) = summary.resume_command { + let spans = vec!["To continue this session, run ".into(), command.cyan()]; + lines.push(spans.into()); + } + self.chat_widget.add_plain_history_lines(lines); + } + tui.frame_requester().schedule_frame(); + } + async fn drain_active_thread_events(&mut self, tui: &mut tui::Tui) -> Result<()> { let Some(mut rx) = self.active_thread_rx.take() else { return Ok(()); @@ -1535,47 +1570,18 @@ impl App { async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result { match event { AppEvent::NewSession => { - let model = self.chat_widget.current_model().to_string(); - let summary = session_summary( - self.chat_widget.token_usage(), - self.chat_widget.thread_id(), - self.chat_widget.thread_name(), - ); - self.shutdown_current_thread().await; - if let Err(err) = self.server.remove_and_close_all_threads().await { - tracing::warn!(error = %err, "failed to close all threads"); - } - let init = crate::chatwidget::ChatWidgetInit { - config: self.config.clone(), - frame_requester: tui.frame_requester(), - app_event_tx: self.app_event_tx.clone(), - // New sessions start without prefilled message content. - initial_user_message: None, - enhanced_keys_supported: self.enhanced_keys_supported, - auth_manager: self.auth_manager.clone(), - models_manager: self.server.get_models_manager(), - feedback: self.feedback.clone(), - is_first_run: false, - feedback_audience: self.feedback_audience, - model: Some(model), - status_line_invalid_items_warned: self.status_line_invalid_items_warned.clone(), - otel_manager: self.otel_manager.clone(), - }; - self.chat_widget = ChatWidget::new(init, self.server.clone()); - self.reset_thread_event_state(); - if let Some(summary) = summary { - let mut lines: Vec> = vec![summary.usage_line.clone().into()]; - if let Some(command) = summary.resume_command { - let spans = vec!["To continue this session, run ".into(), command.cyan()]; - lines.push(spans.into()); - } - self.chat_widget.add_plain_history_lines(lines); - } - tui.frame_requester().schedule_frame(); + self.start_fresh_session_with_summary_hint(tui).await; } AppEvent::ClearUi => { - self.clear_terminal_ui(tui)?; - tui.frame_requester().schedule_frame(); + self.clear_terminal_ui(tui, false)?; + self.overlay = None; + self.transcript_cells.clear(); + self.deferred_history_lines.clear(); + self.has_emitted_history_lines = false; + self.backtrack = BacktrackState::default(); + self.backtrack_render_pending = false; + + self.start_fresh_session_with_summary_hint(tui).await; } AppEvent::OpenResumePicker => { match crate::resume_picker::run_resume_picker(tui, &self.config, false).await? { diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 865ddb29e..3434bc1c9 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -54,7 +54,8 @@ pub(crate) enum AppEvent { /// Start a new session. NewSession, - /// Clear the terminal UI (screen + scrollback) without changing session state. + /// Clear the terminal UI (screen + scrollback), start a fresh session, and keep the + /// previous chat resumable. ClearUi, /// Open the resume picker inside the running TUI session. diff --git a/codex-rs/tui/src/custom_terminal.rs b/codex-rs/tui/src/custom_terminal.rs index 1b16f5915..c51cc726b 100644 --- a/codex-rs/tui/src/custom_terminal.rs +++ b/codex-rs/tui/src/custom_terminal.rs @@ -457,15 +457,16 @@ where /// Hard-reset scrollback + visible screen using an explicit ANSI sequence. /// - /// This is a compatibility fallback for terminals that misbehave when purge - /// and full-screen clear are issued as separate backend commands. + /// Some terminals behave more reliably when purge + clear are emitted as a + /// single ANSI sequence instead of separate backend commands. pub fn clear_scrollback_and_visible_screen_ansi(&mut self) -> io::Result<()> { if self.viewport_area.is_empty() { return Ok(()); } - // Reset scroll region + style state, purge scrollback, clear screen, home cursor. - write!(self.backend, "\x1b[r\x1b[0m\x1b[3J\x1b[2J\x1b[H")?; + // Reset scroll region + style state, home cursor, clear screen, purge scrollback. + // The order matches the common shell `clear && printf '\\e[3J'` behavior. + write!(self.backend, "\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H")?; std::io::Write::flush(&mut self.backend)?; self.last_known_cursor_pos = Position { x: 0, y: 0 }; self.visible_history_rows = 0; diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index a512060e2..e4ee2dc76 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -68,7 +68,7 @@ impl SlashCommand { SlashCommand::Review => "review my current changes and find issues", SlashCommand::Rename => "rename the current thread", SlashCommand::Resume => "resume a saved chat", - SlashCommand::Clear => "clear the terminal screen and scrollback", + SlashCommand::Clear => "clear the terminal and start a new chat", SlashCommand::Fork => "fork the current chat", // SlashCommand::Undo => "ask Codex to undo a turn", SlashCommand::Quit | SlashCommand::Exit => "exit Codex",