fix(input): restore editor after interrupting tab handoff (#290)

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
This commit is contained in:
l0ng-ai
2026-08-02 12:12:14 +08:00
committed by GitHub
co-authored by l0ng-ai
parent 75dbbf8daf
commit 48c13684a8
+82 -1
View File
@@ -159,6 +159,7 @@ pub struct TerminalView {
remote_completion_inflight: bool,
completion_generation: u64,
editor_handoff: Option<u64>,
editor_handoff_interrupt_seq: Option<u64>,
reverse_search: Option<ReverseSearch>,
integration_notice: Option<String>,
integration_notice_shown: bool,
@@ -760,6 +761,7 @@ impl TerminalView {
completion: None,
completion_generation: 0,
editor_handoff: None,
editor_handoff_interrupt_seq: None,
remote_completion_inflight: false,
reverse_search: None,
integration_notice: None,
@@ -1129,6 +1131,15 @@ impl TerminalView {
self.note_integration_gap(cx);
}
if m.control && !m.platform && !m.alt && ks.key == "c" && self.handoff_active() {
// A Tab/unknown-chord handoff leaves the daemon at_prompt: the shell
// never saw Enter, so there is no C mark. Ctrl-C makes readline draw
// a fresh prompt whose A/B report is consequently true -> true and
// does not advance prompt_cycle. Remember this report boundary so
// that fresh prompt can still return ownership to the local editor.
self.editor_handoff_interrupt_seq = Some(self.terminal.prompt_seq());
}
let kitty = self.kitty_flags();
if let Some(bytes) = super::input::keystroke_to_bytes(ks, kitty) {
let plain = !m.control && !m.alt && !m.platform;
@@ -2485,7 +2496,7 @@ impl TerminalView {
if self.shell_vi_prompt() {
return Some("the shell prompt is in vi mode");
}
if self.editor_handoff == Some(self.terminal.prompt_cycle()) {
if self.handoff_active() {
return Some("this prompt's line was already handed to the shell");
}
if !self.at_shell_prompt() {
@@ -2504,6 +2515,9 @@ impl TerminalView {
fn handoff_active(&self) -> bool {
self.editor_handoff == Some(self.terminal.prompt_cycle())
&& self
.editor_handoff_interrupt_seq
.is_none_or(|seq| self.terminal.prompt_seq() <= seq)
&& self.terminal.at_prompt()
&& !self.on_alt_screen()
}
@@ -2903,6 +2917,7 @@ impl TerminalView {
}
self.cmd.clear();
self.editor_handoff = Some(self.terminal.prompt_cycle());
self.editor_handoff_interrupt_seq = None;
self.send_to_pty(chord, cx);
}
@@ -5980,6 +5995,72 @@ mod gpui_tests {
wait_for_input_active(&window, cx);
}
#[gpui::test]
fn ctrl_c_after_tab_handoff_returns_the_fresh_prompt_to_the_editor(cx: &mut TestAppContext) {
crate::core::config::pin_test_config_dir();
let (window, mut daemon) = harness(cx);
DaemonMsg::Prompt {
active: true,
at_prompt: true,
last_exit: None,
}
.encode(&mut daemon)
.unwrap();
wait_for_input_active(&window, cx);
window
.update(cx, |view, window, cx| {
for ch in ["z", "z", "q", "q", "x"] {
type_char(view, ch, window, cx);
}
view.complete_tab(true, cx);
assert!(!view.input_active(), "Tab handed this line to the shell");
view.on_key_down(
&KeyDownEvent {
keystroke: key("ctrl-c"),
is_held: false,
prefer_character_input: false,
},
window,
cx,
);
})
.unwrap();
assert_eq!(
next_input_until_timeout(&mut daemon),
Some(b"zzqqx".to_vec())
);
assert_eq!(next_input_until_timeout(&mut daemon), Some(b"\t".to_vec()));
assert_eq!(next_input_until_timeout(&mut daemon), Some(vec![0x03]));
// Tab handoff never emitted C, so the shell integration reports the
// interrupted prompt as another A/B while the daemon is still at_prompt.
DaemonMsg::Prompt {
active: true,
at_prompt: true,
last_exit: Some(130),
}
.encode(&mut daemon)
.unwrap();
wait_for_input_active(&window, cx);
window
.update(cx, |view, window, cx| {
type_char(view, "n", window, cx);
assert_eq!(
view.cmd.text(),
"n",
"the first character on the fresh line belongs to tty7's editor"
);
})
.unwrap();
assert_eq!(
next_input_until_timeout(&mut daemon),
None,
"the fresh line must not keep going raw to the shell"
);
}
#[gpui::test]
fn a_late_remote_listing_leaves_a_line_the_editor_no_longer_owns_alone(
cx: &mut TestAppContext,