mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 08:04:25 -10:00
Compare commits
1
Commits
main
...
boat/issue-6237
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c9fc19be5 |
No files matched your search
@@ -2038,13 +2038,16 @@ fn clear_active_selection(app: &mut App) -> bool {
|
||||
}
|
||||
|
||||
/// Branches taken by the Ctrl+C key handler. The order encodes priority and is
|
||||
/// the unit-tested contract for #1337 / #1367: a transcript selection always
|
||||
/// wins (so users learn that Ctrl+C copies when there's something to copy);
|
||||
/// otherwise an active turn is interrupted; otherwise the quit-arm flow runs.
|
||||
/// the unit-tested contract for #1337 / #1367 / #6237: a transcript selection
|
||||
/// always wins (so users learn that Ctrl+C copies when there's something to
|
||||
/// copy); otherwise an active turn is interrupted; otherwise a non-empty
|
||||
/// composer is cleared recoverably (the sibling-tool muscle-memory convention);
|
||||
/// otherwise the quit-arm flow runs.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum CtrlCDisposition {
|
||||
CopySelection,
|
||||
CancelTurn,
|
||||
ClearInput,
|
||||
ConfirmExit,
|
||||
ArmExit,
|
||||
}
|
||||
@@ -2058,6 +2061,8 @@ pub(crate) fn ctrl_c_disposition(app: &App) -> CtrlCDisposition {
|
||||
|| app.goal_continuation_waiting
|
||||
{
|
||||
CtrlCDisposition::CancelTurn
|
||||
} else if !app.input.is_empty() {
|
||||
CtrlCDisposition::ClearInput
|
||||
} else if app.quit_is_armed() {
|
||||
CtrlCDisposition::ConfirmExit
|
||||
} else {
|
||||
|
||||
@@ -6059,12 +6059,12 @@ pub(crate) async fn run_event_loop(
|
||||
// the conversation composer match below handle this key
|
||||
// exactly as they would in a live session.
|
||||
} else {
|
||||
// Ctrl+C on the launch screen follows the same two-tap
|
||||
// Ctrl+C on the launch screen follows the same
|
||||
// contract as the session shell (`CtrlCDisposition`):
|
||||
// first press arms the visible exit prompt, the second
|
||||
// inside QUIT_CONFIRMATION_WINDOW exits. Selection
|
||||
// copy and turn cancel cannot apply before a session
|
||||
// exists, so every other disposition arms.
|
||||
// a non-empty composer clears recoverably, then the first
|
||||
// press arms the visible exit prompt, the second inside
|
||||
// QUIT_CONFIRMATION_WINDOW exits. Selection copy and turn
|
||||
// cancel cannot apply before a session exists.
|
||||
if key.code == KeyCode::Char('c')
|
||||
&& key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
{
|
||||
@@ -6073,6 +6073,13 @@ pub(crate) async fn run_event_loop(
|
||||
let _ = engine_handle.send(Op::Shutdown).await;
|
||||
return Ok(());
|
||||
}
|
||||
CtrlCDisposition::ClearInput => {
|
||||
app.edit_in_progress = false;
|
||||
app.clear_input_recoverable();
|
||||
let _ = app.maybe_show_behavioral_tip(
|
||||
crate::tui::behavioral_tips::BehavioralTip::ClearedInputRestore,
|
||||
);
|
||||
}
|
||||
_ => app.arm_quit(),
|
||||
}
|
||||
app.needs_redraw = true;
|
||||
@@ -6612,11 +6619,12 @@ pub(crate) async fn run_event_loop(
|
||||
copy_active_selection(app);
|
||||
}
|
||||
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||
// Four behaviors layered on Ctrl+C in priority order — see
|
||||
// Five behaviors layered on Ctrl+C in priority order — see
|
||||
// `CtrlCDisposition` for the unit-tested decision table.
|
||||
// 1. selection active → copy + clear (Windows convention,
|
||||
// #1337); 2. turn in flight → cancel; 3. quit-armed →
|
||||
// exit; 4. otherwise → arm the 2-second exit prompt.
|
||||
// #1337); 2. turn in flight → cancel; 3. non-empty
|
||||
// composer → recoverable clear (#6237); 4. quit-armed →
|
||||
// exit; 5. otherwise → arm the 2-second exit prompt.
|
||||
match ctrl_c_disposition(app) {
|
||||
CtrlCDisposition::CopySelection => {
|
||||
copy_active_selection(app);
|
||||
@@ -6652,6 +6660,13 @@ pub(crate) async fn run_event_loop(
|
||||
app.status_message = Some(parent_stop_status(app, base));
|
||||
app.disarm_quit();
|
||||
}
|
||||
CtrlCDisposition::ClearInput => {
|
||||
app.edit_in_progress = false;
|
||||
app.clear_input_recoverable();
|
||||
let _ = app.maybe_show_behavioral_tip(
|
||||
crate::tui::behavioral_tips::BehavioralTip::ClearedInputRestore,
|
||||
);
|
||||
}
|
||||
CtrlCDisposition::ConfirmExit => {
|
||||
let _ = engine_handle.send(Op::Shutdown).await;
|
||||
return Ok(());
|
||||
|
||||
@@ -19281,6 +19281,38 @@ fn ctrl_c_disposition_goal_delay_cancels_continuation_instead_of_arming_exit() {
|
||||
assert_eq!(ctrl_c_disposition(&app), CtrlCDisposition::CancelTurn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_disposition_non_empty_composer_clears_input() {
|
||||
// #6237: with a draft present and nothing in flight, Ctrl+C clears the
|
||||
// composer recoverably instead of arming the exit prompt, matching the
|
||||
// sibling-tool muscle-memory convention.
|
||||
let mut app = create_test_app();
|
||||
app.input = "draft".to_string();
|
||||
assert!(!app.is_loading);
|
||||
assert!(!app.quit_is_armed());
|
||||
assert_eq!(ctrl_c_disposition(&app), CtrlCDisposition::ClearInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_disposition_loading_beats_non_empty_composer() {
|
||||
// In flight, Ctrl+C must stay "cancel"; a draft must not swallow the
|
||||
// only way out of a running turn.
|
||||
let mut app = create_test_app();
|
||||
app.input = "draft".to_string();
|
||||
app.is_loading = true;
|
||||
assert_eq!(ctrl_c_disposition(&app), CtrlCDisposition::CancelTurn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ctrl_c_disposition_non_empty_composer_beats_armed_quit() {
|
||||
// Once a draft is present, Ctrl+C returns to "clear", so the two-tap exit
|
||||
// path only runs on an empty composer.
|
||||
let mut app = create_test_app();
|
||||
app.arm_quit();
|
||||
app.input = "draft".to_string();
|
||||
assert_eq!(ctrl_c_disposition(&app), CtrlCDisposition::ClearInput);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escape_goal_delay_cancels_continuation_with_empty_composer() {
|
||||
let mut app = create_test_app();
|
||||
|
||||
+2
-1
@@ -14,7 +14,7 @@ Global key chords are not yet user-configurable — tracked for a future release
|
||||
| `F2` | Toggle the typed Settings editor |
|
||||
| `F3` | Open the provider/model picker (same as `/provider`) |
|
||||
| `Ctrl-K` | Open the command palette (slash-command finder) |
|
||||
| `Ctrl-C` | Cancel current turn / dismiss modal / arm-then-confirm quit |
|
||||
| `Ctrl-C` | Clear a non-empty draft (recoverable — see `Ctrl-Z`); otherwise cancel current turn / dismiss modal / arm-then-confirm quit |
|
||||
| `Ctrl-B` | Release the current shell wait so the turn can continue; foreground commands move into `/jobs`, and background tasks keep running. Use `/jobs` or `Bash` with `action: "wait"` to inspect output |
|
||||
| `Ctrl-D` | Quit (only when the composer is empty) |
|
||||
| `Tab` | When the composer is empty, cycle TUI mode: Plan → Work → Operate → Plan |
|
||||
@@ -49,6 +49,7 @@ Editing the message you're about to send.
|
||||
| `Ctrl-Enter` / `Cmd-Enter` | Send into the current turn; send normally when idle (when the terminal reports it distinctly) |
|
||||
| `Ctrl-U` | Clear the whole draft (recoverable — see `Ctrl-Z`) |
|
||||
| `Ctrl-Z` | Restore the cleared draft (only while the composer is empty) |
|
||||
| `Ctrl-C` | Clear the whole draft (recoverable — see `Ctrl-Z`) when the composer is non-empty and nothing is in flight |
|
||||
| `Ctrl-W` / `Ctrl-Backspace` / `Alt-Backspace` | Delete previous word |
|
||||
| `Ctrl-A` / `Home` | Move to start of input / start of line (readline convention) |
|
||||
| `Ctrl-E` / `End` | Move to end of input / end of line |
|
||||
|
||||
Reference in new issue
Block a user