fix(terminal): preserve Pi reading position across synchronized redraws (#234)
Reanchor only uniquely matched content after complete synchronized Pi redraws. Preserve standard clearing and cancel stale anchors on user actions or interrupted updates. Related to #159.
This commit is contained in:
1 parent
d30d1c9bbc
commit
bdee8d22c5
9 files changed
+551
-72
No files matched your search
@@ -14,6 +14,46 @@ fn hook(session: &str, name: &str, sequence: u64) -> AiHookEvent {
|
||||
crate::ai_hook::parse_remote_envelope(wire.as_bytes(), Some(42)).unwrap()
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn pi_redraw_anchor_follows_accepted_hook_and_command_lifecycle(cx: &mut TestAppContext) {
|
||||
use nebula_terminal::term::test::TermSize;
|
||||
use nebula_terminal::vte::ansi::Processor;
|
||||
|
||||
let (view, window, _) = open(cx);
|
||||
view.update(window, |view, cx| {
|
||||
let event = |kind: &str, seq: u64| {
|
||||
crate::ai_hook::parse_remote_envelope(
|
||||
format!("nebula-hook/1 source=pi pane=42\n{{\"kind\":\"{kind}\",\"session_id\":\"redraw-policy\",\"bridge_sequence\":{seq}}}").as_bytes(),
|
||||
Some(42),
|
||||
).unwrap()
|
||||
};
|
||||
let check = |view: &mut TerminalView, expected: usize| {
|
||||
let mut term = view.session.as_ref().unwrap().term.lock();
|
||||
term.resize(TermSize::new(60, 5));
|
||||
let mut stream = Processor::<nebula_terminal::vte::ansi::StdSyncHandler>::default();
|
||||
let text = (0..12).map(|n| format!("ROW_{n:03}")).collect::<Vec<_>>().join("\r\n");
|
||||
let frame = format!("\x1b[?2026h\x1b[2J\x1b[H\x1b[3J{text}\x1b[?2026l");
|
||||
term.scroll_display(Scroll::Bottom);
|
||||
stream.advance(&mut *term, frame.as_bytes());
|
||||
term.scroll_display(Scroll::Top);
|
||||
term.scroll_display(Scroll::Delta(-2));
|
||||
stream.advance(&mut *term, frame.as_bytes());
|
||||
assert_eq!(term.grid().display_offset(), expected);
|
||||
};
|
||||
check(view, 0);
|
||||
assert!(view.handle_ai_hook(&event("session-start", 1), cx));
|
||||
check(view, 5);
|
||||
assert!(view.handle_ai_hook(&event("done", 2), cx));
|
||||
check(view, 5); // 回合结束不是 Pi 进程退出。
|
||||
assert!(view.handle_ai_hook(&event("session-end", 3), cx));
|
||||
check(view, 0);
|
||||
assert!(view.handle_ai_hook(&event("session-start", 4), cx));
|
||||
check(view, 5);
|
||||
view.finish_foreground_command(Some(0), cx);
|
||||
check(view, 0);
|
||||
});
|
||||
}
|
||||
|
||||
fn screen(view: &mut TerminalView, text: &str) {
|
||||
feed(view, format!("\x1b[2J\x1b[H{}", text.replace('\n', "\r\n")).as_bytes());
|
||||
}
|
||||
|
||||
@@ -67,6 +67,11 @@ impl TerminalView {
|
||||
| crate::ai_hook::AiTurnOutcome::Incomplete
|
||||
);
|
||||
}
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().set_redraw_anchor_enabled(
|
||||
event.source == "pi" && event.kind != AiHookKind::SessionEnd,
|
||||
);
|
||||
}
|
||||
if let Some(target) = target {
|
||||
let previous = self.recovery.target.clone();
|
||||
self.recovery.confirm(target);
|
||||
|
||||
@@ -853,6 +853,9 @@ impl TerminalView {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().cancel_redraw_anchor();
|
||||
}
|
||||
let delta_y = event.delta.pixel_delta(self.line_height).y.as_f32();
|
||||
// 旧壳 `mouse_wheel_input`:Ctrl+滚轮先于一切滚动消费者,一步 1
|
||||
// 逻辑像素,钳在 4–64。设置页步进会写盘;这里同样写 `font_size=`,
|
||||
|
||||
@@ -127,6 +127,9 @@ impl TerminalView {
|
||||
/// the primary edge; the cached-prompt path calls the same reset so the two
|
||||
/// lifecycle routes cannot drift apart.
|
||||
pub(super) fn clear_foreground_agent_state(&mut self, cx: &mut Context<Self>) -> bool {
|
||||
if let Some(session) = &self.session {
|
||||
session.term.lock().set_redraw_anchor_enabled(false);
|
||||
}
|
||||
self.confirmation.observe_waiting(false);
|
||||
self.recovery.command_ended();
|
||||
self.answers.close();
|
||||
|
||||
Reference in new issue
Block a user