From e97c2048e320d6c09fa1d556bd343c52deab0b58 Mon Sep 17 00:00:00 2001 From: eggplantiny <57978095+eggplantiny@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:36:34 +0900 Subject: [PATCH 1/3] fix: order terminal dimensions with shell output --- CHANGELOG.md | 4 + crates/core/src/session.rs | 10 +- crates/frontend/src/lib.rs | 2 +- crates/frontend/tests/surface_harness.rs | 21 ++++ docs/browser-testing.md | 7 +- docs/product-backlog.ko.md | 4 + docs/product-backlog.md | 4 + docs/ssh-input-review-results.ko.md | 65 +++++++++++ docs/ssh-input-review-results.md | 68 +++++++++++ frontends/tauri/package.json | 2 +- .../scripts/input-review-harness-test.mjs | 106 +++++++++++++++++- frontends/tauri/src/components/Term.svelte | 41 ++++--- frontends/tauri/src/lib/bridge.ts | 3 +- frontends/tauri/src/lib/terminalOutput.ts | 24 ++++ frontends/tauri/src/lib/terminalResize.ts | 35 ++++++ frontends/tauri/tests/browser/transport.ts | 2 +- frontends/tauri/tests/terminal/output.test.ts | 34 ++++++ frontends/tauri/tests/terminal/resize.test.ts | 65 +++++++++++ 18 files changed, 471 insertions(+), 26 deletions(-) create mode 100644 frontends/tauri/src/lib/terminalOutput.ts create mode 100644 frontends/tauri/src/lib/terminalResize.ts create mode 100644 frontends/tauri/tests/terminal/output.test.ts create mode 100644 frontends/tauri/tests/terminal/resize.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed4419..6f01b42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Keep terminal resizes and shell output in the same order, including when an approval dock opens and closes in a fixed window. Retain dock motion while preventing delayed sizes and asynchronous redraws from desynchronizing the visible grid and cursor. The exact installed macOS/gateway report remains open; see the [follow-up](docs/ssh-input-review-results.md#terminal-dimensions-after-v085--unreleased). + +한국어: 고정 창에서 승인 바가 열리고 닫힐 때도 터미널 크기 변경과 셸 출력을 같은 순서로 반영합니다. 모션은 유지하면서 지연된 크기 변경과 비동기 재출력으로 화면·커서가 어긋나는 결함을 수정했습니다. 사용자의 설치된 macOS·중간 접속 환경에서 나타난 정확한 문제는 아직 미해결입니다. [후속 조사](docs/ssh-input-review-results.ko.md#v085-이후-터미널-크기-후속-조사--미배포)를 참고하세요. + ## 0.8.5 — Preview · 2026-09-22 - Restore policy-based Autopilot in POSIX SSH: ordinary commands run, risky commands request approval, and deny rules remain blocked. Remote paths are never inspected as host files. diff --git a/crates/core/src/session.rs b/crates/core/src/session.rs index bf1d9cb..ce7ab29 100644 --- a/crates/core/src/session.rs +++ b/crates/core/src/session.rs @@ -402,6 +402,7 @@ pub struct OutputFrame { pub data: Vec, pub output_seq: u64, pub generation: u64, + pub size: crate::screen::Size, } pub type OutputFrameSink = Box; @@ -851,9 +852,13 @@ impl Session { fn write_output(&mut self, bytes: &[u8]) { if bytes.is_empty() { return; } + self.emit_output_frame(bytes); + } + + fn emit_output_frame(&mut self, bytes: &[u8]) { self.output_seq = self.output_seq.saturating_add(1); if let Some(sink) = &self.output_frame_sink { - sink(&OutputFrame { data: bytes.to_vec(), output_seq: self.output_seq, generation: self.surface_generation }); + sink(&OutputFrame { data: bytes.to_vec(), output_seq: self.output_seq, generation: self.surface_generation, size: self.screen.size() }); } } @@ -1025,6 +1030,9 @@ impl Session { pub fn resize(&mut self, rows: u16, cols: u16) { self.screen.resize(rows, cols); self.screen_dirty = true; + // Order renderer dimensions with PTY bytes under the same session lock. + // An IPC acknowledgement alone can overtake output event delivery. + self.emit_output_frame(&[]); if let Some(m) = &self.master { let _ = m.resize(PtySize { rows, cols, pixel_width: 0, pixel_height: 0 }); } diff --git a/crates/frontend/src/lib.rs b/crates/frontend/src/lib.rs index 69962e1..178bab7 100644 --- a/crates/frontend/src/lib.rs +++ b/crates/frontend/src/lib.rs @@ -204,7 +204,7 @@ fn terminal_output(app: AppHandle, session: String) -> conn_core::session::Outpu use base64::Engine as _; if let Some(state) = app.state.upgrade() { let data = base64::engine::general_purpose::STANDARD.encode(&frame.data); - let value = json!({"session":session,"data":data,"outputSeq":frame.output_seq,"generation":frame.generation}); + let value = json!({"session":session,"data":data,"outputSeq":frame.output_seq,"generation":frame.generation,"size":frame.size}); let mut output = state.output.lock(); match output.get_mut(&session) { Some(Some(buffer)) => { diff --git a/crates/frontend/tests/surface_harness.rs b/crates/frontend/tests/surface_harness.rs index c356ac2..aea92a1 100644 --- a/crates/frontend/tests/surface_harness.rs +++ b/crates/frontend/tests/surface_harness.rs @@ -38,6 +38,27 @@ fn native_output_is_sequenced_and_renderer_cannot_inject_observations() { assert_eq!(h.invoke("status",json!({"session":id})).unwrap()["surfaceAvailable"],true); } #[test] +fn resize_is_ordered_with_owner_output_even_when_the_shell_is_silent() { + use base64::Engine as _; + let (_dir,h,output,id)=setup(); + h.invoke("resize",json!({"session":id,"rows":27,"cols":104})).unwrap(); + let marker=output.lock().unwrap().iter().rposition(|v|v["data"]==""&&v["size"]==json!({"rows":27,"cols":104})).expect("silent resize frame"); + h.invoke("input",json!({"session":id,"data":"printf '\\nSYNTHETIC_RESIZE_OUTPUT\\n'\n"})).unwrap(); + let deadline=Instant::now()+Duration::from_secs(4); + loop { + let entries=output.lock().unwrap(); + let bytes:Vec=entries[marker+1..].iter().flat_map(|v|base64::engine::general_purpose::STANDARD.decode(v["data"].as_str().unwrap()).unwrap()).collect(); + if String::from_utf8_lossy(&bytes).contains("SYNTHETIC_RESIZE_OUTPUT") { + assert!(entries[marker..].iter().all(|v|v["size"]==json!({"rows":27,"cols":104}))); + assert!(entries.windows(2).all(|w|w[0]["outputSeq"].as_u64()