ctrl-L (clears terminal but does not start a new chat) (#12628)

# ctrl-L

- Clears your terminal window
- Does not start a new chat
This commit is contained in:
Won Park 2026-02-24 10:03:42 -08:00 committed by GitHub
parent f6053fdfb3
commit ca556fa313
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 118 additions and 14 deletions

View file

@ -730,6 +730,15 @@ impl App {
self.clear_ui_header_lines_with_version(width, CODEX_CLI_VERSION)
}
fn queue_clear_ui_header(&mut self, tui: &mut tui::Tui) {
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;
}
}
fn clear_terminal_ui(&mut self, tui: &mut tui::Tui, redraw_header: bool) -> Result<()> {
let is_alt_screen_active = tui.is_alt_screen_active();
@ -754,16 +763,20 @@ impl App {
self.has_emitted_history_lines = false;
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;
}
self.queue_clear_ui_header(tui);
}
Ok(())
}
fn reset_app_ui_state_after_clear(&mut self) {
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;
}
async fn shutdown_current_thread(&mut self) {
if let Some(thread_id) = self.chat_widget.thread_id() {
// Clear any in-flight rollback guard when switching threads.
@ -1616,12 +1629,7 @@ impl App {
}
AppEvent::ClearUi => {
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.reset_app_ui_state_after_clear();
self.start_fresh_session_with_summary_hint(tui).await;
}
@ -3111,6 +3119,25 @@ impl App {
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
tui.frame_requester().schedule_frame();
}
KeyEvent {
code: KeyCode::Char('l'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
kind: KeyEventKind::Press,
..
} => {
if !self.chat_widget.can_run_ctrl_l_clear_now() {
return;
}
if let Err(err) = self.clear_terminal_ui(tui, false) {
tracing::warn!(error = %err, "failed to clear terminal UI");
self.chat_widget
.add_error_message(format!("Failed to clear terminal UI: {err}"));
} else {
self.reset_app_ui_state_after_clear();
self.queue_clear_ui_header(tui);
tui.frame_requester().schedule_frame();
}
}
KeyEvent {
code: KeyCode::Char('g'),
modifiers: crossterm::event::KeyModifiers::CONTROL,
@ -3538,8 +3565,7 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn clear_ui_after_long_transcript_snapshots_fresh_header_only() {
async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
let mut app = make_test_app().await;
app.config.cwd = PathBuf::from("/tmp/project");
app.chat_widget.set_model("gpt-test");
@ -3644,6 +3670,18 @@ mod tests {
!rendered.contains("Bracken Ferry"),
"clear header should not replay prior conversation turns"
);
rendered
}
#[tokio::test]
async fn clear_ui_after_long_transcript_snapshots_fresh_header_only() {
let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;
assert_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);
}
#[tokio::test]
async fn ctrl_l_clear_ui_after_long_transcript_reuses_clear_header_snapshot() {
let rendered = render_clear_ui_header_after_long_transcript_for_snapshot().await;
assert_snapshot!("clear_ui_after_long_transcript_fresh_header_only", rendered);
}
@ -4519,6 +4557,59 @@ mod tests {
}
}
#[tokio::test]
async fn clear_only_ui_reset_preserves_chat_session_state() {
let mut app = make_test_app().await;
let thread_id = ThreadId::new();
app.chat_widget.handle_codex_event(Event {
id: String::new(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: thread_id,
forked_from_id: None,
thread_name: Some("keep me".to_string()),
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
cwd: PathBuf::from("/tmp/project"),
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
network_proxy: None,
rollout_path: Some(PathBuf::new()),
}),
});
app.chat_widget
.apply_external_edit("draft prompt".to_string());
app.transcript_cells = vec![Arc::new(UserHistoryCell {
message: "old message".to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
remote_image_urls: Vec::new(),
}) as Arc<dyn HistoryCell>];
app.overlay = Some(Overlay::new_transcript(app.transcript_cells.clone()));
app.deferred_history_lines = vec![Line::from("stale buffered line")];
app.has_emitted_history_lines = true;
app.backtrack.primed = true;
app.backtrack.overlay_preview_active = true;
app.backtrack.nth_user_message = 0;
app.backtrack_render_pending = true;
app.reset_app_ui_state_after_clear();
assert!(app.overlay.is_none());
assert!(app.transcript_cells.is_empty());
assert!(app.deferred_history_lines.is_empty());
assert!(!app.has_emitted_history_lines);
assert!(!app.backtrack.primed);
assert!(!app.backtrack.overlay_preview_active);
assert!(app.backtrack.pending_rollback.is_none());
assert!(!app.backtrack_render_pending);
assert_eq!(app.chat_widget.thread_id(), Some(thread_id));
assert_eq!(app.chat_widget.composer_text_with_pending(), "draft prompt");
}
#[tokio::test]
async fn session_summary_skip_zero_usage() {
assert!(session_summary(TokenUsage::default(), None, None).is_none());

View file

@ -3403,6 +3403,19 @@ impl ChatWidget {
self.bottom_pane.can_launch_external_editor()
}
pub(crate) fn can_run_ctrl_l_clear_now(&mut self) -> bool {
// Ctrl+L is not a slash command, but it follows /clear's current rule:
// block while a task is running.
if !self.bottom_pane.is_task_running() {
return true;
}
let message = "Ctrl+L is disabled while a task is in progress.".to_string();
self.add_to_history(history_cell::new_error_event(message));
self.request_redraw();
false
}
fn dispatch_command(&mut self, cmd: SlashCommand) {
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
let message = format!(