mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
fix(daemon): keep the newest mark's reading when suppression collapses a batch
`suppress_relayed_prompt_marks` compares everything but `mark_at_prompt`, so that two marks the local editor can no longer tell apart still collapse into one `Prompt` message. But `dedup_by` keeps the *earlier* of the pair, and a far shell's whole turn can arrive in a single read: the previous command's `D`, the prompt's `A`/`B`, then the next command's `C`. With `at_prompt` cleared across the batch those two entries differ only in the mark's own reading, and the `C` was the one being dropped — leaving `mark_at_prompt` standing at `true` while a remote command was starting, which `PaneContext::at_prompt` reports and `pane_freeness` answers `free` from. The reverse order loses the prompt instead: a command that starts and finishes inside one read collapses onto its own `C`, and the pane reads busy for as long as it takes some unrelated output to arrive — which on an idle prompt is never. Both need the same thing, so carry the reading onto the survivor before the later entry goes. `command` only tells the pair apart when the far shell names it; nushell's integration and several third-party ones emit a bare `133;C`. Claude-Session: https://claude.ai/code/session_01JRqYZ9E153WpSHGS2AW3BM
This commit is contained in:
@@ -2880,16 +2880,26 @@ fn same_dir(a: &Path, b: &Path) -> bool {
|
||||
/// only thing this machine knows about the far shell (#840). Because of it,
|
||||
/// dedup has to compare what is actually emitted rather than the whole struct
|
||||
/// — two marks that used to collapse into one `Prompt` message must still
|
||||
/// collapse when only their unsuppressed twin tells them apart.
|
||||
/// collapse when only their unsuppressed twin tells them apart, and the
|
||||
/// survivor has to carry the *newest* twin: a whole turn can arrive in one read
|
||||
/// (`D`, the prompt's `A`/`B`, then the next command's `C`), and dropping the
|
||||
/// last entry's reading would hand the pane back a prompt it has already left.
|
||||
fn suppress_relayed_prompt_marks(shell: &mut Vec<ShellState>) {
|
||||
for s in shell.iter_mut() {
|
||||
s.at_prompt = false;
|
||||
}
|
||||
shell.dedup_by(|a, b| {
|
||||
a.active == b.active
|
||||
let same = a.active == b.active
|
||||
&& a.at_prompt == b.at_prompt
|
||||
&& a.last_exit_code == b.last_exit_code
|
||||
&& a.command == b.command
|
||||
&& a.command == b.command;
|
||||
// `dedup_by` keeps `b`, the earlier of the pair, and drops `a`. Move
|
||||
// the reading over first so the collapse costs a `Prompt` message and
|
||||
// nothing else.
|
||||
if same {
|
||||
b.mark_at_prompt = a.mark_at_prompt;
|
||||
}
|
||||
same
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4315,6 +4325,42 @@ mod tests {
|
||||
assert!(local.shell.last().unwrap().at_prompt);
|
||||
}
|
||||
|
||||
/// The collapse that suppression performs must not hand the pane back a
|
||||
/// prompt it has already left. A far shell's whole turn can arrive in one
|
||||
/// read — the previous command's `D`, the prompt's `A`/`B`, then the next
|
||||
/// command's `C` — and once `at_prompt` is cleared across the batch those
|
||||
/// two entries differ only in the mark's own reading. The collapse keeps
|
||||
/// the earlier entry, so that reading has to travel with it or freeness
|
||||
/// answers `free` while a remote command is running (#840).
|
||||
#[test]
|
||||
fn suppression_collapses_a_batch_onto_its_newest_marks_reading() {
|
||||
let mut s = OscSniffer::new();
|
||||
// Bare `133;C`: nushell's integration emits it with no command name, as
|
||||
// do several third-party ones, so `command` cannot tell the two entries
|
||||
// apart either.
|
||||
let mut turn = s.feed(b"\x1b]133;D;0\x07\x1b]133;A\x07\x1b]133;B\x07\x1b]133;C\x07");
|
||||
suppress_relayed_prompt_marks(&mut turn.shell);
|
||||
assert_eq!(
|
||||
turn.shell.len(),
|
||||
1,
|
||||
"still one `Prompt` message, exactly as before the mark was added"
|
||||
);
|
||||
assert!(
|
||||
!turn.shell.last().unwrap().mark_at_prompt,
|
||||
"the newest mark started a command, so the pane is not at a prompt"
|
||||
);
|
||||
|
||||
// And the other order: a command that starts and finishes inside one
|
||||
// read has to end at the prompt, not at the `C` that opened it.
|
||||
let mut turn = s.feed(b"\x1b]133;C\x07\x1b]133;D;0\x07\x1b]133;A\x07");
|
||||
suppress_relayed_prompt_marks(&mut turn.shell);
|
||||
assert_eq!(turn.shell.len(), 1);
|
||||
assert!(
|
||||
turn.shell.last().unwrap().mark_at_prompt,
|
||||
"the newest mark is the prompt the far shell just drew"
|
||||
);
|
||||
}
|
||||
|
||||
/// A prompt mark that arrives while the pane is pointed at a remote host
|
||||
/// can only be the far shell's — the near one cannot be at a prompt while
|
||||
/// the connection owns its pty. That is what proves the far side runs the
|
||||
|
||||
Reference in New Issue
Block a user