mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
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:
+105
-14
@@ -730,6 +730,15 @@ impl App {
|
|||||||
self.clear_ui_header_lines_with_version(width, CODEX_CLI_VERSION)
|
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<()> {
|
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 is_alt_screen_active = tui.is_alt_screen_active();
|
||||||
|
|
||||||
@@ -754,16 +763,20 @@ impl App {
|
|||||||
self.has_emitted_history_lines = false;
|
self.has_emitted_history_lines = false;
|
||||||
|
|
||||||
if redraw_header {
|
if redraw_header {
|
||||||
let width = tui.terminal.last_known_screen_size.width;
|
self.queue_clear_ui_header(tui);
|
||||||
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(())
|
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) {
|
async fn shutdown_current_thread(&mut self) {
|
||||||
if let Some(thread_id) = self.chat_widget.thread_id() {
|
if let Some(thread_id) = self.chat_widget.thread_id() {
|
||||||
// Clear any in-flight rollback guard when switching threads.
|
// Clear any in-flight rollback guard when switching threads.
|
||||||
@@ -1616,12 +1629,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
AppEvent::ClearUi => {
|
AppEvent::ClearUi => {
|
||||||
self.clear_terminal_ui(tui, false)?;
|
self.clear_terminal_ui(tui, false)?;
|
||||||
self.overlay = None;
|
self.reset_app_ui_state_after_clear();
|
||||||
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;
|
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()));
|
self.overlay = Some(Overlay::new_transcript(self.transcript_cells.clone()));
|
||||||
tui.frame_requester().schedule_frame();
|
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 {
|
KeyEvent {
|
||||||
code: KeyCode::Char('g'),
|
code: KeyCode::Char('g'),
|
||||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||||
@@ -3538,8 +3565,7 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
async fn render_clear_ui_header_after_long_transcript_for_snapshot() -> String {
|
||||||
async fn clear_ui_after_long_transcript_snapshots_fresh_header_only() {
|
|
||||||
let mut app = make_test_app().await;
|
let mut app = make_test_app().await;
|
||||||
app.config.cwd = PathBuf::from("/tmp/project");
|
app.config.cwd = PathBuf::from("/tmp/project");
|
||||||
app.chat_widget.set_model("gpt-test");
|
app.chat_widget.set_model("gpt-test");
|
||||||
@@ -3644,6 +3670,18 @@ mod tests {
|
|||||||
!rendered.contains("Bracken Ferry"),
|
!rendered.contains("Bracken Ferry"),
|
||||||
"clear header should not replay prior conversation turns"
|
"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);
|
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]
|
#[tokio::test]
|
||||||
async fn session_summary_skip_zero_usage() {
|
async fn session_summary_skip_zero_usage() {
|
||||||
assert!(session_summary(TokenUsage::default(), None, None).is_none());
|
assert!(session_summary(TokenUsage::default(), None, None).is_none());
|
||||||
|
|||||||
@@ -3403,6 +3403,19 @@ impl ChatWidget {
|
|||||||
self.bottom_pane.can_launch_external_editor()
|
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) {
|
fn dispatch_command(&mut self, cmd: SlashCommand) {
|
||||||
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
|
if !cmd.available_during_task() && self.bottom_pane.is_task_running() {
|
||||||
let message = format!(
|
let message = format!(
|
||||||
|
|||||||
Reference in New Issue
Block a user