diff --git a/crates/op-editor-core/src/agent_indicators.rs b/crates/op-editor-core/src/agent_indicators.rs index 9fcc98ff0..823b9b948 100644 --- a/crates/op-editor-core/src/agent_indicators.rs +++ b/crates/op-editor-core/src/agent_indicators.rs @@ -17,13 +17,13 @@ use std::collections::{HashMap, HashSet}; use std::sync::{LazyLock, Mutex}; /// Duration of the short generated-node entrance animation. -pub const REVEAL_DURATION_MS: u64 = 1_680; +pub const REVEAL_DURATION_MS: u64 = 1_920; /// Delay between the first generated nodes in one applied batch. -pub const REVEAL_STAGGER_MS: u64 = 48; +pub const REVEAL_STAGGER_MS: u64 = 40; /// Minimum delay before descendants of a newly revealed container begin /// their own entrances. This leaves the parent opening beat readable /// without making nested content feel stalled. -pub const REVEAL_CHILD_RUNWAY_MS: u64 = 80; +pub const REVEAL_CHILD_RUNWAY_MS: u64 = 72; /// Extra delay for nested generated nodes. Visual traversal order /// already places children after parents; keeping this at zero avoids /// depth changes compressing adjacent stream slots into the same frame. @@ -32,12 +32,12 @@ pub const REVEAL_DEPTH_STAGGER_MS: u64 = 0; /// beat. Once the parent has begun settling, delayed children animate /// independently so streamed content does not pop in abruptly. pub const REVEAL_CHILD_SUPPRESS_FRACTION: f32 = 0.04; -const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 18; +const REVEAL_FULL_STAGGER_SIBLINGS: u64 = 24; const REVEAL_MID_STAGGER_SIBLINGS: u64 = 72; const REVEAL_COMPRESSED_STAGGER_MS: u64 = 32; const REVEAL_TAIL_STAGGER_MS: u64 = 24; const REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT: usize = 1; -const REVEAL_BURST_RECOVERY_STAGGER_MS: u64 = REVEAL_STAGGER_MS; +const REVEAL_BURST_RECOVERY_STAGGER_MS: u64 = 32; const CLOCK_REBASE_THRESHOLD_MS: u64 = 60_000; const REVEAL_FRAME_MS: u64 = 16; @@ -334,11 +334,18 @@ fn smooth_overdue_reveal_burst(r: &mut AgentIndicators, now_ms: u64) { } let mut newly_due_seen = 0; let mut reschedule_tail = false; - let mut next_slot = now_ms.saturating_add(REVEAL_BURST_RECOVERY_STAGGER_MS); + let mut next_slot = now_ms; for (id, original_start) in ordered { if original_start > prev_ms && original_start <= now_ms { newly_due_seen += 1; - if newly_due_seen > REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT { + if newly_due_seen <= REVEAL_MAX_NEW_STARTS_PER_SNAPSHOT { + if let Some(started_at) = r.reveals.get_mut(&id) { + *started_at = next_slot; + } + next_slot = next_slot.saturating_add(REVEAL_BURST_RECOVERY_STAGGER_MS); + reschedule_tail = true; + continue; + } else { reschedule_tail = true; } } @@ -452,7 +459,7 @@ mod tests { "reveal should stay active long enough to read as a smooth entrance" ); assert!( - snapshot_at(2_800).reveals.is_empty(), + snapshot_at(3_000).reveals.is_empty(), "expired reveal should be pruned" ); assert!(!is_active(), "expired reveal should not keep animating"); @@ -570,6 +577,34 @@ mod tests { end_if_epoch(epoch); } + #[test] + fn snapshot_replays_first_overdue_reveal_instead_of_jumping_mid_animation() { + let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let epoch = begin(); + for i in 0..6 { + add_reveal( + epoch, + &format!("n{i}"), + 1_000 + i as u64 * REVEAL_STAGGER_MS, + ); + } + + assert_eq!(snapshot_at(1_000).reveals.get("n0"), Some(&1_000)); + let snap = snapshot_at(1_220); + + assert_eq!( + snap.reveals.get("n1"), + Some(&1_220), + "the first overdue node should restart at the current frame instead of appearing partway through" + ); + assert_eq!( + snap.reveals.get("n2"), + Some(&(1_220 + REVEAL_BURST_RECOVERY_STAGGER_MS)), + "overdue siblings should recover one-by-one on a tighter cadence" + ); + end_if_epoch(epoch); + } + #[test] fn snapshot_recovery_preserves_stream_order_after_frame_gap() { let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()); diff --git a/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs b/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs index 9c9d240a5..2eb5b80e4 100644 --- a/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs +++ b/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs @@ -205,11 +205,11 @@ fn paint_reveal_sweep( let tail = (1.0 - t * t).clamp(0.0, 1.0); let radius = 8.0_f32.min(animated.size.y / 2.0); cx.backend - .fill_round_rect(animated, radius, REVEAL_ACCENT.with_alpha(0.075 * tail)); + .fill_round_rect(animated, radius, REVEAL_ACCENT.with_alpha(0.060 * tail)); cx.backend.stroke_round_rect( animated, radius, - REVEAL_ACCENT.with_alpha(0.42 * tail), + REVEAL_ACCENT.with_alpha(0.34 * tail), 1.25, ); let sweep_w = (animated.size.x * 0.18).clamp(16.0, 64.0); @@ -223,14 +223,14 @@ fn paint_reveal_sweep( cx.backend.save(); cx.backend.clip_rect(animated); cx.backend - .fill_round_rect(sweep, sweep_w / 2.0, Color::WHITE.with_alpha(0.24 * tail)); + .fill_round_rect(sweep, sweep_w / 2.0, Color::WHITE.with_alpha(0.18 * tail)); cx.backend.restore(); } fn lifted_scaled_rect(rect: Rect, ease: f32) -> Rect { let settle = 1.0 - ease; - let scale = 0.970 + ease * 0.030; - let lift = settle * 8.0; + let scale = 0.960 + ease * 0.040; + let lift = settle * 11.0; let w = rect.size.x * scale; let h = rect.size.y * scale; Rect { @@ -457,7 +457,7 @@ mod tests { &roots, Point2D::new(100.0, 50.0), 2.0, - 2_800, + 3_000, ); assert!(expired_backend.round_fills.is_empty()); assert!(expired_backend.round_strokes.is_empty()); diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs index adf066ea0..42fdbbe64 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_paint.rs @@ -669,8 +669,8 @@ fn push_reveal_transform(cx: &mut PaintCx<'_>, rect: Rect, phase: RevealPhase) - return false; } let settle = 1.0 - phase.ease; - let lift = 9.0 * settle * (1.0 - phase.t * 0.12); - let scale = 0.968 + 0.032 * phase.ease; + let lift = 11.0 * settle * (1.0 - phase.t * 0.10); + let scale = 0.960 + 0.040 * phase.ease; let pivot = Point2D::new( rect.origin.x + rect.size.x / 2.0, rect.origin.y + rect.size.y / 2.0, diff --git a/crates/op-orchestrator/src/subagent_reveal_tests.rs b/crates/op-orchestrator/src/subagent_reveal_tests.rs index 338f8b2ed..9521f6e25 100644 --- a/crates/op-orchestrator/src/subagent_reveal_tests.rs +++ b/crates/op-orchestrator/src/subagent_reveal_tests.rs @@ -158,7 +158,7 @@ fn reveal_schedule_gives_new_container_children_an_entrance_runway() { }); assert!( - starts[0] - status >= 80, + starts[0] - status >= op_editor_core::agent_indicators::REVEAL_CHILD_RUNWAY_MS, "children of a new container need their own runway instead of starting inside the parent's first beat" ); assert!(