diff --git a/agents.md b/agents.md index 30b0db0..d9bba50 100644 --- a/agents.md +++ b/agents.md @@ -90,8 +90,9 @@ Real and verified: macOS Metal toolchain so `cargo run`'s shader-build failure is fixed. - Scroll input: consecutive same-document wheel requests coalesce at the profile worker boundary while preserving total device-pixel distance; - clicks, text, and allow-once permission transfers retain strict ordering. - Verified by a blocked-worker burst regression and the live-site scroll e2e. + frames from the active render epoch remain presentable while newer input is + queued. Clicks, text, and allow-once permission transfers retain strict + ordering. Verified by generation, blocked-worker burst, and live-site tests. - Website color scheme: resolved System/Light/Dark state crosses sidecar protocol v4 and reaches `WebView::notify_theme_change`; GPUI appearance events trigger a shell repaint, and first-navigation WebView replacement diff --git a/crates/ely_app/src/shell/web_surface_runtime.rs b/crates/ely_app/src/shell/web_surface_runtime.rs index 93636d0..12992cf 100644 --- a/crates/ely_app/src/shell/web_surface_runtime.rs +++ b/crates/ely_app/src/shell/web_surface_runtime.rs @@ -104,9 +104,11 @@ impl WebSurfaceRuntime { let submitted_at = Instant::now(); let started_loading = { let session = session_for_scope(&mut self.sessions, tab.id(), scope.clone()); - let started_loading = session.started_loading(&requested_url, size, zoom_percent); + let started_loading = + session.started_loading(&requested_url, size, zoom_percent, self.color_scheme); if started_loading { session.pending_user_navigation = false; + session.frame_generation_floor = Some(generation); } if user_navigation_input { session.pending_user_navigation = true; @@ -114,6 +116,7 @@ impl WebSurfaceRuntime { session.requested_url = requested_url.clone(); session.size = size; session.zoom_percent = zoom_percent; + session.color_scheme = Some(self.color_scheme); session.scroll_offset = next_scroll_offset; session.generation = Some(generation); session.cadence.note_ensure(input_kind, started_loading, submitted_at); @@ -296,7 +299,7 @@ impl WebSurfaceRuntime { continue; }; if !self.sessions.get(&tab_id_obj).is_some_and(|session| { - &session.scope == scope && session.generation == Some(generation) + &session.scope == scope && session.accepts_frame_generation(generation) }) { continue; } diff --git a/crates/ely_app/src/shell/web_surface_runtime_generation_tests.rs b/crates/ely_app/src/shell/web_surface_runtime_generation_tests.rs index a1f1cc9..b684aee 100644 --- a/crates/ely_app/src/shell/web_surface_runtime_generation_tests.rs +++ b/crates/ely_app/src/shell/web_surface_runtime_generation_tests.rs @@ -145,6 +145,35 @@ fn late_frame_is_discarded_after_tab_session_closes() -> Result<(), String> { Ok(()) } +#[test] +fn earlier_frame_from_same_render_epoch_remains_presentable() -> Result<(), String> { + let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory); + let tab_id = TabId::new(); + let profile_id = ProfileId::new(); + let tab = web_tab(tab_id.clone(), profile_id.clone(), "https://example.com/page")?; + + runtime.ensure_tab(&tab, surface_size(), ProfileDataMode::Transient, &[], pending_input())?; + let earlier = current_generation(&runtime, &tab_id)?; + runtime.ensure_tab(&tab, surface_size(), ProfileDataMode::Transient, &[], pending_input())?; + let latest = current_generation(&runtime, &tab_id)?; + + let mut frames = Vec::new(); + runtime.collect_responses( + &scope(&profile_id), + vec![WorkerResponse::Frame { + generation: earlier, + tab_id: tab_id.as_str().to_string(), + frame: live_frame(), + }], + Instant::now(), + &mut frames, + ); + + assert!(earlier < latest); + assert!(matches!(frames.as_slice(), [WebSurfaceRuntimeFrame::Ready { .. }])); + Ok(()) +} + #[test] fn scope_change_returns_submitted_permission_grants_on_the_next_tick() -> Result<(), String> { let mut runtime = WebSurfaceRuntime::new_with_client_factory(empty_client_factory); diff --git a/crates/ely_app/src/shell/web_surface_runtime_retry_tests.rs b/crates/ely_app/src/shell/web_surface_runtime_retry_tests.rs index 590d580..4d8cf0b 100644 --- a/crates/ely_app/src/shell/web_surface_runtime_retry_tests.rs +++ b/crates/ely_app/src/shell/web_surface_runtime_retry_tests.rs @@ -57,7 +57,9 @@ fn successful_frame_resets_scope_retry_state() { let scope = WebSurfaceRuntimeScope::new(ProfileId::new(), ProfileDataMode::Transient); let tab_id = TabId::new(); let generation = RequestGeneration::new(1); - session_for_scope(&mut runtime.sessions, &tab_id, scope.clone()).generation = Some(generation); + let session = session_for_scope(&mut runtime.sessions, &tab_id, scope.clone()); + session.generation = Some(generation); + session.frame_generation_floor = Some(generation); runtime.note_scope_failure(&scope, Instant::now()); let mut frames = Vec::new(); diff --git a/crates/ely_app/src/shell/web_surface_runtime_session.rs b/crates/ely_app/src/shell/web_surface_runtime_session.rs index 71be9a6..84ba6ab 100644 --- a/crates/ely_app/src/shell/web_surface_runtime_session.rs +++ b/crates/ely_app/src/shell/web_surface_runtime_session.rs @@ -8,7 +8,7 @@ use crate::services::{ transient_profile_data_dir, }, }; -use ely_domain::{ProfileId, TabId}; +use ely_domain::{ColorScheme, ProfileId, TabId}; use super::{ web_surface_cadence::WebSurfacePollCadence, @@ -42,6 +42,8 @@ pub(super) struct WebSurfaceSession { pub(super) scroll_offset: WebSurfaceScrollOffset, pub(super) pending_user_navigation: bool, pub(super) generation: Option, + pub(super) frame_generation_floor: Option, + pub(super) color_scheme: Option, pub(super) cadence: WebSurfacePollCadence, } @@ -55,6 +57,8 @@ impl WebSurfaceSession { scroll_offset: WebSurfaceScrollOffset::default(), pending_user_navigation: false, generation: None, + frame_generation_floor: None, + color_scheme: None, cadence: WebSurfacePollCadence::default(), } } @@ -64,10 +68,17 @@ impl WebSurfaceSession { requested_url: &str, size: WebSurfaceSize, zoom_percent: u16, + color_scheme: ColorScheme, ) -> bool { self.requested_url != requested_url || self.size != size || self.zoom_percent != zoom_percent + || self.color_scheme != Some(color_scheme) + } + + pub(super) fn accepts_frame_generation(&self, generation: RequestGeneration) -> bool { + self.frame_generation_floor.is_some_and(|floor| generation >= floor) + && self.generation.is_some_and(|latest| generation <= latest) } pub(super) fn url_change_for(