You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by zh...@apache.org on 2026/10/16 10:27:09 UTC

(maka) 02/03: fix(tui): bound tool reads and skip unchanged hover redraws

This is an automated email from the ASF dual-hosted git repository.

me2seeks pushed a commit to branch rust-preview
in repository https://gitbox.apache.org/repos/asf/maka.git

commit e361ad59424feb14aa69c385a7e406030ceb7e3f
Author: me2seeks <me...@gmail.com>
AuthorDate: Fri Oct 16 17:48:47 2026 +0800

    fix(tui): bound tool reads and skip unchanged hover redraws
    
    Report native hover changes before requesting another frame while retaining
    row, menu, and raw terminal pointer behavior and pending business redraws.
    
    Load tool summaries from presented viewport demand and pause automatic reads
    in cached readers. Keep completed summaries available on re-entry.
    
    Bound accepted executors and pending feedback at 64 independently of the
    1024 total pending-intent budget. Resume queued work after feedback drains,
    and reject retired queued requests without releasing actual running work.
    
    Retain opt-in MAKA_UI_TRACE lifecycle and slow-stage diagnostics, written
    through a bounded background queue without recording service payloads.
    
    Cover hover stability, source demand, composed request bursts, bounded
    queue overflow, and retirement while execution capacity is full.
---
 platform/render-terminal/src/application.rs        |  37 ++++-
 platform/render-terminal/src/application/choice.rs |  15 +-
 platform/render-terminal/src/application/paint.rs  |   1 +
 .../render-terminal/src/application/pointer.rs     |   5 +-
 .../render-terminal/src/application/reading.rs     |   9 +-
 .../render-terminal/src/application/resources.rs   |  33 ++--
 platform/render-terminal/src/application/rows.rs   |   7 +-
 platform/render-terminal/src/application/run.rs    |  53 +++++++
 platform/render-terminal/src/application/scene.rs  |   5 +-
 .../src/application/tests/layout.rs                |  74 +++++++++
 .../render-terminal/src/application/tests/rows.rs  |  27 ++++
 platform/render-terminal/src/terminal.rs           |   2 +
 platform/ui-js/tests/runtime/subscriptions.rs      | 173 ++++++++++++++++++++-
 platform/ui-runtime/src/actions.rs                 |  88 +++++++++--
 platform/ui-runtime/src/actions/mutations.rs       |  15 ++
 platform/ui-runtime/src/actions/subscriptions.rs   |   8 +
 platform/ui-runtime/src/diagnostics.rs             | 144 +++++++++++++++++
 platform/ui-runtime/src/lib.rs                     |   1 +
 platform/ui-runtime/src/tree.rs                    |   8 +-
 plugins/agent/agent-screen/conversation-view.ts    |   1 +
 plugins/agent/tool-details/index.ts                |   2 +
 plugins/shared/components.ts                       |   5 +
 plugins/shared/contracts.ts                        |  23 ++-
 products/maka/bundle/tests/timeline.test.mjs       |  87 +++++++++++
 products/maka/bundle/tests/tool-details.test.mjs   | 121 ++++++++++++++
 25 files changed, 887 insertions(+), 57 deletions(-)

diff --git a/platform/render-terminal/src/application.rs b/platform/render-terminal/src/application.rs
index 29c9d1cd4b..49beef03a6 100644
--- a/platform/render-terminal/src/application.rs
+++ b/platform/render-terminal/src/application.rs
@@ -88,7 +88,15 @@ pub trait Widget: std::any::Any {
     fn cancel_pointer(&self, _: &mut dyn std::any::Any) {}
     /// Local pointer feedback against the last presented target. Leaving clears
     /// feedback; hovering never selects, focuses, or invokes guest code.
-    fn hover(&self, _: &mut dyn std::any::Any, _: &str, _: Option<ratatui::layout::Position>) {}
+    /// Return whether native feedback changed and needs another paint.
+    fn hover(
+        &self,
+        _: &mut dyn std::any::Any,
+        _: &str,
+        _: Option<ratatui::layout::Position>,
+    ) -> bool {
+        false
+    }
     fn same_presentation(&self, _: &dyn std::any::Any) -> bool {
         false
     }
@@ -586,7 +594,6 @@ impl Presenter {
     /// scene. Crossterm supplies no screen epoch; never re-resolve a captured key.
     /// 输入立即绑定到最后成功输出的场景和真实 mount/revision。
     pub fn input(&mut self, event: Event) -> maka_ui_runtime::Result<()> {
-        self.resources.redraw = true;
         let result = self.input_inner(event, false, None);
         self.recover_result(result)
     }
@@ -594,7 +601,6 @@ impl Presenter {
         &mut self,
         event: Event,
     ) -> maka_ui_runtime::Result<Option<activation::CapturedActivation>> {
-        self.resources.redraw = true;
         let mut captured = None;
         let result = self.input_inner(event, false, Some(&mut captured));
         self.recover_result(result)?;
@@ -605,7 +611,6 @@ impl Presenter {
     pub(super) fn wheel(&mut self, event: Event) -> maka_ui_runtime::Result<()> {
         debug_assert!(matches!(event, Event::Mouse(mouse) if matches!(mouse.kind,
             MouseEventKind::ScrollUp | MouseEventKind::ScrollDown)));
-        self.resources.redraw = true;
         let result = self.input_inner(event, true, None);
         self.recover_result(result)
     }
@@ -615,6 +620,11 @@ impl Presenter {
         defer_scroll: bool,
         mut capture: Option<&mut Option<activation::CapturedActivation>>,
     ) -> maka_ui_runtime::Result<()> {
+        // Ordinary pointer motion can leave presentation entirely unchanged.
+        // Native hover reports its own changes below; other input retains the
+        // original redraw policy, including raw terminal pointer forwarding.
+        self.resources.redraw |=
+            !matches!(event, Event::Mouse(mouse) if mouse.kind == MouseEventKind::Moved);
         match event {
             Event::FocusLost => self.window_focused = false,
             Event::FocusGained => self.window_focused = true,
@@ -662,6 +672,9 @@ impl Presenter {
         } else {
             None
         };
+        if raw_pointer_target.is_some() {
+            self.resources.redraw = true;
+        }
         if raw_pointer_target.is_none() && self.open_context_menu(&event)? {
             return Ok(());
         }
@@ -1195,7 +1208,19 @@ impl Presenter {
                     && !self.resources.owners.contains_key(&mount.id())
             })
             .cloned();
+        let started = maka_ui_runtime::diagnostics::started();
         let result = self.pump_inner(input_waiting);
+        if let Some(started) = started
+            && started.elapsed() >= std::time::Duration::from_millis(16)
+        {
+            maka_ui_runtime::diagnostics::record("slow_pump", || {
+                json!({
+                    "elapsed_us": started.elapsed().as_micros(),
+                    "pending_actions": self.application.pending_action_count(),
+                    "input_waiting": input_waiting, "failed": result.is_err(),
+                })
+            });
+        }
         if let (Some(mount), Err(error @ (Error::Script(_) | Error::Invalid(_)))) =
             (replacement, &result)
             && !self.application.logical_pending()
@@ -1260,9 +1285,11 @@ impl Presenter {
         if let Err(error) = advancement {
             self.report(&error);
         }
+        let advance_span = maka_ui_runtime::diagnostics::Span::new("slow_advance_ui");
         if self.application.advance_ui()? {
             self.resources.redraw = true;
         }
+        drop(advance_span);
         self.sync_native()?;
         // Each join observer stays with the actual owner, independently of UI.
         self.finish_jobs()?;
@@ -1272,6 +1299,7 @@ impl Presenter {
             }
             if !input_waiting {
                 let resources = &mut self.resources;
+                let _span = maka_ui_runtime::diagnostics::Span::new("slow_feedback");
                 self.application.deliver_action_results(
                     &self.root,
                     |target, recipe, witness| {
@@ -1356,6 +1384,7 @@ impl Presenter {
         failure.map_or(Ok(()), Err)
     }
     fn finish_jobs(&mut self) -> maka_ui_runtime::Result<()> {
+        let _span = maka_ui_runtime::diagnostics::Span::new("slow_finish_jobs");
         let wake = self.application.waker();
         let mut cx = std::task::Context::from_waker(&wake);
         while let std::task::Poll::Ready(Some(completion)) =
diff --git a/platform/render-terminal/src/application/choice.rs b/platform/render-terminal/src/application/choice.rs
index 44f21b6a58..a6c0717667 100644
--- a/platform/render-terminal/src/application/choice.rs
+++ b/platform/render-terminal/src/application/choice.rs
@@ -183,25 +183,30 @@ impl SceneState {
             })
         })
     }
-    pub fn choice_hover(&mut self, choice: &Choice, point: Option<Position>) {
+    pub fn choice_hover(&mut self, choice: &Choice, point: Option<Position>) -> bool {
         if !self.dialogs.contains_key(&choice.key) {
-            return;
+            return false;
         }
         let Some(state) = self.choices.get_mut(&choice.key) else {
-            return;
+            return false;
         };
         let Some(geometry) = state
             .geometry
             .as_ref()
             .filter(|geometry| geometry.choice == *choice)
         else {
-            return;
+            return false;
         };
         if let Some((menu, point)) = geometry.menu.as_ref().zip(point)
             && let Some(index) = menu.rows.iter().position(|row| row.contains(point))
         {
-            state.highlighted = Some(choice.options[menu.first + index].key.clone());
+            let key = &choice.options[menu.first + index].key;
+            if state.highlighted.as_ref() != Some(key) {
+                state.highlighted = Some(key.clone());
+                return true;
+            }
         }
+        false
     }
 }
 impl Presenter {
diff --git a/platform/render-terminal/src/application/paint.rs b/platform/render-terminal/src/application/paint.rs
index 12abfd55ee..2dc8d9ed4e 100644
--- a/platform/render-terminal/src/application/paint.rs
+++ b/platform/render-terminal/src/application/paint.rs
@@ -672,6 +672,7 @@ pub(super) fn color(value: impl Into<maka_ui_runtime::scene::terminal::Color>) -
 }
 impl Presenter {
     pub fn paint(&self, area: Rect, buffer: &mut Buffer) -> maka_ui_runtime::Result<PaintedScene> {
+        let _span = maka_ui_runtime::diagnostics::Span::new("slow_paint");
         if !self.surface.is_live() {
             return Err(maka_ui_runtime::Error::Invalid(
                 "terminal surface retired".into(),
diff --git a/platform/render-terminal/src/application/pointer.rs b/platform/render-terminal/src/application/pointer.rs
index 7fe4c7d3bb..89ddf3fd89 100644
--- a/platform/render-terminal/src/application/pointer.rs
+++ b/platform/render-terminal/src/application/pointer.rs
@@ -24,6 +24,7 @@ impl Presenter {
     pub(super) fn clear_hover(&mut self) -> maka_ui_runtime::Result<()> {
         if let Some(target) = self.hover.take() {
             self.resources.hover(&target, None)?;
+            self.resources.redraw = true;
         }
         Ok(())
     }
@@ -49,7 +50,9 @@ impl Presenter {
             self.clear_hover()?;
         }
         if let Some(target) = target {
-            self.resources.hover(&target, Some(position))?;
+            let entering = self.hover.is_none();
+            let changed = self.resources.hover(&target, Some(position))?;
+            self.resources.redraw |= entering || changed;
             self.hover = Some(target);
         }
         Ok(())
diff --git a/platform/render-terminal/src/application/reading.rs b/platform/render-terminal/src/application/reading.rs
index 5a8ef42f09..b86424e2d5 100644
--- a/platform/render-terminal/src/application/reading.rs
+++ b/platform/render-terminal/src/application/reading.rs
@@ -369,8 +369,13 @@ impl Widget for Reading {
     fn handles_tab(&self, state: &dyn Any, key: &str) -> bool {
         self.node.handles_tab(state, key)
     }
-    fn hover(&self, state: &mut dyn Any, key: &str, point: Option<ratatui::layout::Position>) {
-        self.node.hover(state, key, point);
+    fn hover(
+        &self,
+        state: &mut dyn Any,
+        key: &str,
+        point: Option<ratatui::layout::Position>,
+    ) -> bool {
+        self.node.hover(state, key, point)
     }
     fn present(&self, state: &mut dyn Any, geometry: Box<dyn Any>) -> Option<NativeInput> {
         let state = state_mut(state);
diff --git a/platform/render-terminal/src/application/resources.rs b/platform/render-terminal/src/application/resources.rs
index 3593030ade..3881f731d6 100644
--- a/platform/render-terminal/src/application/resources.rs
+++ b/platform/render-terminal/src/application/resources.rs
@@ -697,6 +697,7 @@ impl Resources {
 }
 impl Presenter {
     pub(super) fn sync_native(&mut self) -> maka_ui_runtime::Result<()> {
+        let _span = maka_ui_runtime::diagnostics::Span::new("slow_sync_native");
         self.resources.deactivate(&self.application);
         // Transfer completed original work before preflight reserves the next
         // command's capacity. A newer command cannot replace an uncaptured result.
@@ -786,30 +787,30 @@ impl Resources {
         &mut self,
         target: &paint::Target,
         position: Option<ratatui::layout::Position>,
-    ) -> maka_ui_runtime::Result<()> {
+    ) -> maka_ui_runtime::Result<bool> {
         let Some(owner) = self
             .owners
             .get_mut(&target.mount.id())
             .filter(|owner| owner.live && std::rc::Rc::ptr_eq(&owner.identity, &target.owner))
         else {
-            return Ok(());
+            return Ok(false);
         };
-        if catch_unwind(AssertUnwindSafe(|| {
+        match catch_unwind(AssertUnwindSafe(|| {
             owner
                 .recipe
-                .hover(owner.state.as_mut(), &target.key, position);
-        }))
-        .is_err()
-        {
-            owner.live = false;
-            self.failed.insert(
-                target.mount.id(),
-                "terminal native hover cleanup unconfirmed".into(),
-            );
-            return Err(maka_ui_runtime::Error::Invalid(
-                "terminal native hover cleanup unconfirmed".into(),
-            ));
+                .hover(owner.state.as_mut(), &target.key, position)
+        })) {
+            Ok(changed) => Ok(changed),
+            Err(_) => {
+                owner.live = false;
+                self.failed.insert(
+                    target.mount.id(),
+                    "terminal native hover cleanup unconfirmed".into(),
+                );
+                Err(maka_ui_runtime::Error::Invalid(
+                    "terminal native hover cleanup unconfirmed".into(),
+                ))
+            }
         }
-        Ok(())
     }
 }
diff --git a/platform/render-terminal/src/application/rows.rs b/platform/render-terminal/src/application/rows.rs
index e3033acf8e..2b74fcc1e9 100644
--- a/platform/render-terminal/src/application/rows.rs
+++ b/platform/render-terminal/src/application/rows.rs
@@ -156,9 +156,9 @@ impl Widget for Rows {
         owner.offset = owner.offset.min(self.props.rows.len().saturating_sub(1));
         Ok(None)
     }
-    fn hover(&self, state: &mut dyn Any, _: &str, point: Option<Position>) {
+    fn hover(&self, state: &mut dyn Any, _: &str, point: Option<Position>) -> bool {
         let owner = state.downcast_mut::<Owner>().unwrap();
-        owner.hover = point.and_then(|point| {
+        let hover = point.and_then(|point| {
             if owner
                 .geometry
                 .as_ref()?
@@ -176,6 +176,9 @@ impl Widget for Rows {
                 .find(|hit| hit.area.contains(point))
                 .map(|hit| hit.row.clone())
         });
+        let changed = owner.hover != hover;
+        owner.hover = hover;
+        changed
     }
     fn input(
         &self,
diff --git a/platform/render-terminal/src/application/run.rs b/platform/render-terminal/src/application/run.rs
index fcf3ad214c..c4005b7e2c 100644
--- a/platform/render-terminal/src/application/run.rs
+++ b/platform/render-terminal/src/application/run.rs
@@ -50,6 +50,7 @@ async fn deadline(at: Option<Instant>) {
 }
 
 struct PendingOutput {
+    submitted: Option<Instant>,
     sequence: u64,
     generation: uuid::Uuid,
     surface: maka_plugin_core::runtime::Mount,
@@ -78,6 +79,15 @@ fn poll_output(
         return Ok(());
     };
     let output = pending.take().expect("original output receipt");
+    if let Some(start) = output.submitted
+        && start.elapsed() >= Duration::from_millis(16)
+    {
+        maka_ui_runtime::diagnostics::record("slow_output_receipt", || {
+            json!({
+                "sequence": output.sequence, "elapsed_us": start.elapsed().as_micros(),
+            })
+        });
+    }
     result
         .map_err(|error| sanitize(&error.to_string()))?
         .map_err(|error| sanitize(&error.to_string()))?;
@@ -194,6 +204,7 @@ pub fn run(
             match output.submit(bytes) {
                 Ok(receipt) => {
                     pending_output = Some(PendingOutput {
+                        submitted: maka_ui_runtime::diagnostics::started(),
                         sequence: issued_sequence,
                         generation: screen.as_ref().expect("original terminal").generation,
                         surface: presenter.surface(),
@@ -324,6 +335,7 @@ pub fn run(
                 }
                 issued_sequence += 1;
                 pending_output = Some(PendingOutput {
+                    submitted: maka_ui_runtime::diagnostics::started(),
                     sequence: issued_sequence,
                     generation: screen.generation,
                     surface: presenter.surface(),
@@ -383,6 +395,20 @@ pub fn run(
             };
             let mut next_input = input;
             while let Some(mut input) = next_input.take() {
+                let _span = maka_ui_runtime::diagnostics::Span::new("slow_input");
+                maka_ui_runtime::diagnostics::record("input", || {
+                    json!({
+                        "kind": match &input {
+                            Event::Mouse(mouse) => format!("mouse_{:?}", mouse.kind),
+                            Event::Key(_) => "key".into(),
+                            Event::Paste(_) => "paste".into(),
+                            Event::Resize(_, _) => "resize".into(),
+                            _ => "focus".into(),
+                        },
+                        "output_pending": pending_output.is_some(),
+                        "replayed": replay.is_some(),
+                    })
+                });
                 poll_output(
                     &mut pending_output,
                     &mut written_sequence,
@@ -400,6 +426,11 @@ pub fn run(
                         },
                         Admission::Discard => return Ok(false),
                         Admission::Pending => {
+                            maka_ui_runtime::diagnostics::record("input_deferred", || {
+                                json!({
+                                    "reason": "activation", "output_pending": pending_output.is_some(),
+                                })
+                            });
                             queued_input = Some(QueuedInput::Activation(activation));
                             wake.0.notify_one();
                             return Ok(false);
@@ -413,6 +444,11 @@ pub fn run(
                 if let Some(activation) = presenter.capture_activation(&input)
                     && presenter.activation_admission(&activation) == Admission::Pending
                 {
+                    maka_ui_runtime::diagnostics::record("input_deferred", || {
+                        json!({
+                            "reason": "activation", "output_pending": pending_output.is_some(),
+                        })
+                    });
                     queued_input = Some(QueuedInput::Activation(activation));
                     presenter.resources.redraw = true;
                     wake.0.notify_one();
@@ -425,6 +461,11 @@ pub fn run(
                     || input_needs_presentation(&presenter, &input))
                     && !is_exit(&input)
                 {
+                    maka_ui_runtime::diagnostics::record("input_deferred", || {
+                        json!({
+                            "reason": "presentation", "output_pending": pending_output.is_some(),
+                        })
+                    });
                     queued_input = Some(QueuedInput::Physical(
                         input,
                         presenter.opening_witnesses().map_err(|e| e.to_string())?,
@@ -457,6 +498,11 @@ pub fn run(
                         .dispatch_focused_shortcut_with_capture(&input, Some(&mut captured))
                         .map_err(|error| error.to_string())?;
                     if let Some(activation) = captured {
+                        maka_ui_runtime::diagnostics::record("input_deferred", || {
+                            json!({
+                                "reason": "activation", "output_pending": pending_output.is_some(),
+                            })
+                        });
                         queued_input = Some(QueuedInput::Activation(activation));
                         presenter.resources.redraw = true;
                         wake.0.notify_one();
@@ -490,6 +536,11 @@ pub fn run(
                         .map_err(|error| error.to_string())?
                 };
                 if let Some(activation) = captured {
+                    maka_ui_runtime::diagnostics::record("input_deferred", || {
+                        json!({
+                            "reason": "activation", "output_pending": pending_output.is_some(),
+                        })
+                    });
                     queued_input = Some(QueuedInput::Activation(activation));
                     presenter.resources.redraw = true;
                     wake.0.notify_one();
@@ -556,6 +607,8 @@ pub fn run(
             }
         }
     }
+    maka_ui_runtime::diagnostics::record("run_exit", || json!({"failed": failure.is_some()}));
+    maka_ui_runtime::diagnostics::flush();
     updates.stop();
     scheduler.block_on(updates.drain());
     // Failures remain inspectable in this owner; dropping it cannot substitute
diff --git a/platform/render-terminal/src/application/scene.rs b/platform/render-terminal/src/application/scene.rs
index a05b598fdb..59c952f441 100644
--- a/platform/render-terminal/src/application/scene.rs
+++ b/platform/render-terminal/src/application/scene.rs
@@ -131,13 +131,14 @@ impl super::Widget for Node {
         state: &mut dyn std::any::Any,
         key: &str,
         point: Option<ratatui::layout::Position>,
-    ) {
+    ) -> bool {
         if let Some(Node::Choice(choice)) = self.overlay(key) {
-            state
+            return state
                 .downcast_mut::<super::scroll::SceneState>()
                 .unwrap()
                 .choice_hover(choice, point);
         }
+        false
     }
     fn same_presentation(&self, previous: &dyn std::any::Any) -> bool {
         previous
diff --git a/platform/render-terminal/src/application/tests/layout.rs b/platform/render-terminal/src/application/tests/layout.rs
index 1f6294dc1f..0c62bd33b2 100644
--- a/platform/render-terminal/src/application/tests/layout.rs
+++ b/platform/render-terminal/src/application/tests/layout.rs
@@ -1489,6 +1489,80 @@ fn anchored_model_dialog_follows_its_trigger_across_viewport_sizes() {
     presenter.shutdown_settled().unwrap();
 }
 
+#[test]
+fn unchanged_button_hover_does_not_redraw_or_clear_an_existing_redraw() {
+    let source = json!({"kind":"stack","key":"root","direction":"column","gap":0,"padding":0,"background":0,"children":[
+        {"kind":"button","key":"tool","label":"Read file","hint":"Show details"}
+    ]});
+    let (_runtime, mut presenter) = native(View::Scene(source), Rc::new(Cell::new(true)));
+    flushed(&mut presenter, 32, 6);
+    let mouse = |column, row| {
+        Event::Mouse(MouseEvent {
+            kind: MouseEventKind::Moved,
+            column,
+            row,
+            modifiers: KeyModifiers::NONE,
+        })
+    };
+    presenter.resources.redraw = false;
+    presenter.input(mouse(30, 5)).unwrap();
+    assert!(
+        !presenter.resources.redraw,
+        "empty space has no hover feedback"
+    );
+    presenter.input(mouse(2, 0)).unwrap();
+    assert!(presenter.resources.redraw, "entering shows the hint");
+    let buffer = flushed(&mut presenter, 32, 6);
+    assert!(
+        buffer
+            .content
+            .iter()
+            .map(|cell| cell.symbol())
+            .collect::<String>()
+            .contains("Show details")
+    );
+    let revision = presenter
+        .application
+        .frame(&presenter.root)
+        .unwrap()
+        .revision;
+    presenter.resources.redraw = false;
+    for column in [3, 4, 5, 6, 7, 6, 5, 4] {
+        presenter.input_with_capture(mouse(column, 0)).unwrap();
+        assert!(!presenter.resources.redraw, "same button at {column}");
+    }
+    assert_eq!(
+        presenter
+            .application
+            .frame(&presenter.root)
+            .unwrap()
+            .revision,
+        revision
+    );
+    presenter.resources.redraw = true;
+    presenter.input(mouse(3, 0)).unwrap();
+    assert!(
+        presenter.resources.redraw,
+        "pending business redraw survives"
+    );
+    presenter.resources.redraw = false;
+    presenter.input(mouse(30, 5)).unwrap();
+    assert!(presenter.resources.redraw, "leaving removes the hint");
+    let buffer = flushed(&mut presenter, 32, 6);
+    assert!(
+        !buffer
+            .content
+            .iter()
+            .map(|cell| cell.symbol())
+            .collect::<String>()
+            .contains("Show details")
+    );
+    presenter
+        .surface_closed(&presenter.surface(), Ok(()))
+        .unwrap();
+    presenter.shutdown_settled().unwrap();
+}
+
 #[test]
 fn passive_text_hint_hover_never_steals_focus_or_activation() {
     let source = json!({"kind":"stack","key":"root","direction":"column","gap":0,"padding":0,"background":0,"children":[
diff --git a/platform/render-terminal/src/application/tests/rows.rs b/platform/render-terminal/src/application/tests/rows.rs
index 7797dc06ea..588e4f304b 100644
--- a/platform/render-terminal/src/application/tests/rows.rs
+++ b/platform/render-terminal/src/application/tests/rows.rs
@@ -90,6 +90,33 @@ fn native_rows_hover_is_local_and_only_flushed_controls_can_activate() {
     assert!(tui.focus.as_ref().unwrap().same(&focus));
     close(tui);
 }
+#[test]
+fn native_rows_hover_redraws_only_when_the_hovered_row_changes() {
+    let (_runtime, mut tui) = layout::native(
+        layout::View::Rows(Rc::new(RefCell::new(props())), Rc::new(Cell::new(false))),
+        Rc::new(Cell::new(true)),
+    );
+    draw(&mut tui, 40, 10);
+    tui.input(mouse(MouseEventKind::Moved, 2, 2)).unwrap();
+    assert!(tui.resources.redraw);
+    draw(&mut tui, 40, 10);
+    tui.resources.redraw = false;
+    tui.input_with_capture(mouse(MouseEventKind::Moved, 3, 2))
+        .unwrap();
+    assert!(!tui.resources.redraw, "same row in the same native widget");
+    tui.input_with_capture(mouse(MouseEventKind::Moved, 3, 3))
+        .unwrap();
+    assert!(
+        tui.resources.redraw,
+        "another row in the same native widget"
+    );
+    draw(&mut tui, 40, 10);
+    tui.resources.redraw = false;
+    tui.input(mouse(MouseEventKind::Moved, 4, 3)).unwrap();
+    assert!(!tui.resources.redraw);
+    close(tui);
+}
+
 #[test]
 fn native_rows_selected_removal_exits_exact_focus_and_does_not_retarget_repeat() {
     let props = Rc::new(RefCell::new(props()));
diff --git a/platform/render-terminal/src/terminal.rs b/platform/render-terminal/src/terminal.rs
index 29a606067b..68cc73e88b 100644
--- a/platform/render-terminal/src/terminal.rs
+++ b/platform/render-terminal/src/terminal.rs
@@ -89,6 +89,7 @@ pub(crate) fn render(
     screen: &mut Screen,
     render: impl FnOnce(&mut ratatui::Frame<'_>),
 ) -> io::Result<Vec<u8>> {
+    let _span = maka_ui_runtime::diagnostics::Span::new("slow_render");
     queue!(
         screen.terminal.backend_mut().writer_mut(),
         BeginSynchronizedUpdate
@@ -129,6 +130,7 @@ impl Output {
             .name("maka-terminal-output".into())
             .spawn(move || {
                 while let Ok(request) = receive.recv() {
+                    let _span = maka_ui_runtime::diagnostics::Span::new("slow_stdout_write");
                     let result = writer
                         .write_all(&request.bytes)
                         .and_then(|_| writer.flush());
diff --git a/platform/ui-js/tests/runtime/subscriptions.rs b/platform/ui-js/tests/runtime/subscriptions.rs
index d0cb3d646c..afb69d58f3 100644
--- a/platform/ui-js/tests/runtime/subscriptions.rs
+++ b/platform/ui-js/tests/runtime/subscriptions.rs
@@ -129,6 +129,7 @@ fn sequence(index: usize, stop: CancellationToken, mode: Mode) -> StreamFuture {
 enum Mode {
     Causal,
     Many,
+    Burst,
     Panic,
     BadItem,
     WideItem,
@@ -175,6 +176,15 @@ impl NativeComponent<()> for Parent {
                 on: BTreeMap::from([("change".into(), "changed".into())]),
             });
         }
+        if matches!(self.0, Mode::Burst) {
+            slots.extend((0..8).map(|index| ComponentSlot {
+                key: format!("tool-{index}"),
+                binding: "child".into(),
+                member: 0,
+                props: json!({"cycle":0}),
+                on: BTreeMap::new(),
+            }));
+        }
         let mut output = Frame {
             revision: state["revision"].as_u64().unwrap(),
             state: state.clone(),
@@ -191,7 +201,11 @@ impl NativeComponent<()> for Parent {
             )?,
         }
         .output(vec![]);
-        let count = if matches!(self.0, Mode::Many) { 16 } else { 1 };
+        let count = match self.0 {
+            Mode::Many => 16,
+            Mode::Burst => 7,
+            _ => 1,
+        };
         output.subscriptions = (0..count)
             .map(|i| Action {
                 recovery: Value::Null,
@@ -249,13 +263,14 @@ impl NativeComponent<()> for Parent {
         Ok(output)
     }
 }
-struct Child;
+struct Child(Mode);
 impl NativeComponent<()> for Child {
     fn call(
         &self,
         call: ComponentCall<'_>,
         _: &maka_ui_runtime::abi::ValueBindings,
     ) -> maka_ui_runtime::Result<ComponentOutput<NativeView<()>>> {
+        let initial = matches!(&call, ComponentCall::Mount { .. });
         let (revision, cycle, emit) = match call {
             ComponentCall::Mount { props } => (0, props["cycle"].clone(), false),
             ComponentCall::Update { state, props } => (
@@ -263,6 +278,11 @@ impl NativeComponent<()> for Child {
                 props["cycle"].clone(),
                 props["cycle"] != state["cycle"],
             ),
+            ComponentCall::Event { state, .. } if matches!(self.0, Mode::Burst) => (
+                state["revision"].as_u64().unwrap() + 1,
+                state["cycle"].clone(),
+                false,
+            ),
             _ => panic!("no child raw inputs"),
         };
         let events = if emit {
@@ -273,13 +293,28 @@ impl NativeComponent<()> for Child {
         } else {
             vec![]
         };
-        Ok(Frame {
+        let mut output = Frame {
             revision,
             state: json!({"revision":revision,"cycle":cycle}),
             surface: None,
             view: NativeView::new(Rc::new(()), BTreeMap::new(), vec![])?,
         }
-        .output(events))
+        .output(events);
+        if initial && matches!(self.0, Mode::Burst) {
+            output.actions = (0..12)
+                .map(|index| Action {
+                    recovery: Value::Null,
+                    continuations: Vec::new(),
+                    key: format!("source-{index}"),
+                    binding: "stream".into(),
+                    member: 0,
+                    method: "query".into(),
+                    input: Value::Null,
+                    resources: BTreeMap::new(),
+                })
+                .collect();
+        }
+        Ok(output)
     }
 }
 fn component_contract(name: &str, events: BTreeMap<String, Value>) -> Contract {
@@ -359,6 +394,24 @@ fn fixture_data(
     packages.push(package);
     let service = native_package("native.stream", "test.stream", service_contract());
     let child = native_package("native.child", "test.child", child_contract.clone());
+    let child = if matches!(mode, Mode::Burst) {
+        let mut declaration = child.manifest().application.clone().unwrap();
+        declaration
+            .entries
+            .get_mut(&EntryTarget::Desktop)
+            .unwrap()
+            .imports
+            .insert(
+                "stream".into(),
+                Import {
+                    contract: service_contract(),
+                    cardinality: Cardinality::One,
+                },
+            );
+        support::package("native.child", declaration, None)
+    } else {
+        child
+    };
     for (name, key, package) in [("stream", 3, &service), ("child", 4, &child)] {
         input
             .instances
@@ -371,6 +424,15 @@ fn fixture_data(
             }],
         );
     }
+    if matches!(mode, Mode::Burst) {
+        input.instances.get_mut("child").unwrap().bindings.insert(
+            "stream".into(),
+            vec![Target::Instance {
+                path: vec!["stream".into()],
+                export: "Export".into(),
+            }],
+        );
+    }
     packages.extend([service, child]);
     let starts = Arc::new(AtomicUsize::new(0));
     let observed = starts.clone();
@@ -392,7 +454,7 @@ fn fixture_data(
                 "Export".into(),
                 NativeExport {
                     contract: child_contract.clone(),
-                    value: NativeCapability::Component(Rc::new(Child)),
+                    value: NativeCapability::Component(Rc::new(Child(mode))),
                 },
             )]))
         })
@@ -503,6 +565,107 @@ fn temporary_causal_demand_changes_do_not_restart_original_committed_execution()
     assert_eq!(starts.load(Ordering::SeqCst), 1);
     close(&mut app, &[root]);
 }
+#[test]
+fn composed_reads_wait_for_executor_and_feedback_capacity_without_rejecting_the_frame() {
+    let timer = timer();
+    let (mut app, starts) = fixture(Mode::Burst, &timer);
+    let root = app.mount_launch(json!({})).unwrap();
+    assert_eq!(app.pending_action_count(), 103);
+    app.capture_actions(&root, |_| panic!("no resources"))
+        .unwrap();
+    let mut first = Vec::new();
+    while let Some(task) = app.start_task(&root).unwrap() {
+        first.push(task);
+    }
+    assert_eq!(first.len(), 64);
+    assert_eq!(app.accepted_action_count(), 64);
+    let mut subscriptions = Vec::new();
+    for task in first {
+        let (id, completion) = run(&timer, task);
+        if let Some(next) = app.finish_task(id, completion).unwrap() {
+            subscriptions.push(next);
+        }
+    }
+    assert_eq!(app.accepted_action_count(), 7);
+    assert_eq!(app.pending_action_count(), 103);
+    assert!(
+        app.start_task(&root).unwrap().is_none(),
+        "undelivered feedback retains capacity"
+    );
+    assert_eq!(
+        app.deliver_action_results(&root, |_, _, _| Ok(false))
+            .unwrap(),
+        64
+    );
+    let mut remaining = Vec::new();
+    while let Some(task) = app.start_task(&root).unwrap() {
+        remaining.push(task);
+    }
+    assert_eq!(remaining.len(), 39);
+    for task in remaining {
+        let (id, completion) = run(&timer, task);
+        if let Some(next) = app.finish_task(id, completion).unwrap() {
+            subscriptions.push(next);
+        }
+    }
+    assert_eq!(starts.load(Ordering::SeqCst), 7);
+    app.deliver_action_results(&root, |_, _, _| Ok(false))
+        .unwrap();
+    assert_eq!(app.accepted_action_count(), 7);
+    app.retire(&root).unwrap();
+    for task in subscriptions {
+        let (id, completion) = run(&timer, task);
+        assert!(app.finish_task(id, completion).unwrap().is_none());
+    }
+    assert_eq!(app.accepted_action_count(), 0);
+    close(&mut app, &[root]);
+}
+
+#[test]
+fn queued_reads_remain_bounded_and_an_overflow_rolls_back_only_its_own_frame() {
+    let timer = timer();
+    let (mut app, _) = fixture(Mode::Burst, &timer);
+    let roots = (0..9)
+        .map(|_| app.mount_launch(json!({})).unwrap())
+        .collect::<Vec<_>>();
+    assert_eq!(app.pending_action_count(), 927);
+    assert!(
+        matches!(app.mount_launch(json!({})), Err(maka_ui_runtime::Error::Invalid(reason)) if reason == "pending action limit")
+    );
+    assert_eq!(app.pending_action_count(), 927);
+    assert_eq!(app.accepted_action_count(), 0);
+    close(&mut app, &roots);
+}
+
+#[test]
+fn retiring_a_full_executor_queue_rejects_unstarted_reads_without_releasing_running_work() {
+    let timer = timer();
+    let (mut app, _) = fixture(Mode::Burst, &timer);
+    let root = app.mount_launch(json!({})).unwrap();
+    app.capture_actions(&root, |_| panic!("no resources"))
+        .unwrap();
+    let mut tasks = Vec::new();
+    while let Some(task) = app.start_task(&root).unwrap() {
+        tasks.push(task);
+    }
+    assert_eq!(tasks.len(), 64);
+    app.retire(&root).unwrap();
+    assert!(app.start_task(&root).unwrap().is_none());
+    assert_eq!(
+        app.accepted_action_count(),
+        64,
+        "retirement is not executor cleanup"
+    );
+    for task in tasks {
+        let (id, completion) = run(&timer, task);
+        assert!(app.finish_task(id, completion).unwrap().is_none());
+    }
+    app.deliver_action_results(&root, |_, _, _| Ok(false))
+        .unwrap();
+    assert_eq!(app.pending_action_count(), 0);
+    close(&mut app, &[root]);
+}
+
 #[test]
 fn one_pending_feedback_does_not_double_charge_each_of_48_live_requests() {
     let timer = timer();
diff --git a/platform/ui-runtime/src/actions.rs b/platform/ui-runtime/src/actions.rs
index 144ced3faa..21253efb20 100644
--- a/platform/ui-runtime/src/actions.rs
+++ b/platform/ui-runtime/src/actions.rs
@@ -34,6 +34,11 @@ use serde::{Deserialize, Serialize};
 use std::{collections::VecDeque, future::Future, pin::Pin, sync::Arc};
 use tokio_util::sync::CancellationToken;
 
+// Queued intents are bounded separately from accepted executors and feedback.
+// A composed frame can declare more reads than can execute at once.
+pub(super) const MAX_PENDING_ACTIONS: usize = 1024;
+const MAX_ACTIVE_ACTIONS: usize = 64;
+
 #[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
 #[serde(deny_unknown_fields)]
 pub struct CaptureSource {
@@ -377,6 +382,46 @@ impl<V: ?Sized> Actions<V> {
                 .filter(|o| o.request.is_none_or(|id| !self.running.contains_key(&id)))
                 .count()
     }
+    fn can_start(&self) -> bool {
+        // Pending feedback retains a slot until it is consumed. Otherwise a
+        // slow UI callback could let completed query outcomes grow unbounded.
+        self.len() - self.queue.len() < MAX_ACTIVE_ACTIONS
+    }
+    pub(super) fn trace_limit(&self, consumer: &Mount, action: &Action, staged: &[Prepared<V>]) {
+        diagnostics::record("pending_action_limit", || {
+            json!({
+                "incoming": {
+                    "mount": consumer.id(), "key": action.key,
+                    "binding": action.binding, "method": action.method,
+                },
+                "pending": self.len(), "staged_count": staged.len(),
+                "staged": staged.iter().map(|a| json!({
+                    "id": a.id, "mount": a.consumer.id(), "key": a.key,
+                    "method": a.method, "subscription": a.subscription,
+                })).collect::<Vec<_>>(),
+                "queue": self.queue.iter().map(|a| json!({
+                    "id": a.id, "mount": a.consumer.id(), "key": a.key,
+                    "method": a.method, "subscription": a.subscription,
+                    "invalidated": a.invalidated,
+                })).collect::<Vec<_>>(),
+                "running": self.running.iter().map(|(id, a)| json!({
+                    "id": id, "mount": a.consumer.id(), "key": a.key,
+                    "method": a.method, "completed": a.completed,
+                    "subscription": a.stream.is_some(), "mutation": a.mutation.is_some(),
+                    "stop_requested": a.stream.as_ref().map(|flow| flow.stop.is_cancelled()),
+                    "awaiting_feedback": a.stream.as_ref().map(|flow| flow.pending_feedback),
+                })).collect::<Vec<_>>(),
+                "preparing": self.preparing.iter().map(|(id, a)| json!({
+                    "id": id, "mount": a.action.consumer.id(),
+                    "key": a.action.key, "method": a.action.method, "failed": a.failed(),
+                })).collect::<Vec<_>>(),
+                "feedback": self.outcomes.iter().map(|a| json!({
+                    "id": a.request, "mount": a.consumer.id(), "key": a.key,
+                })).collect::<Vec<_>>(),
+            })
+        });
+        diagnostics::flush();
+    }
     fn bytes(&self) -> usize {
         self.queue
             .iter()
@@ -918,11 +963,29 @@ impl<V: ?Sized + 'static> Application<V> {
         }
         Ok(captured)
     }
+    fn action_valid(&self, action: &Prepared<V>) -> bool {
+        !action.invalidated
+            && (!action.subscription
+                || self.subscription_wanted(&action.consumer, &action.key, action.id))
+            && !self.is_retiring(&action.consumer)
+            && action
+                .captures
+                .values()
+                .chain(action.witnesses.iter().map(|c| &c.target))
+                .all(|target| self.target_valid(target))
+    }
+    pub(super) fn wake_queued_actions(&self) {
+        if self.actions.can_start() && self.actions.queue.iter().any(|a| a.resources.is_some()) {
+            self.waker().wake_by_ref();
+        }
+    }
     /// Final Mount and captured registration check. A queue is not accepted Work.
     pub fn start_task(&mut self, root: &Mount) -> Result<Option<ActionTask>> {
         loop {
+            let capacity = self.actions.can_start();
             let Some(index) = self.actions.queue.iter().position(|a| {
                 a.resources.is_some()
+                    && (capacity || !self.action_valid(a))
                     && (self.is_retiring(&a.consumer)
                         || self
                             .mount_root(&a.consumer)
@@ -932,16 +995,14 @@ impl<V: ?Sized + 'static> Application<V> {
                 return Ok(None);
             };
             let action = self.actions.queue.remove(index).expect("queue position");
-            if action.invalidated
-                || action.subscription
-                    && !self.subscription_wanted(&action.consumer, &action.key, action.id)
-                || self.is_retiring(&action.consumer)
-                || action
-                    .captures
-                    .values()
-                    .chain(action.witnesses.iter().map(|c| &c.target))
-                    .any(|t| !self.target_valid(t))
-            {
+            diagnostics::record("action_dequeued", || {
+                json!({
+                    "id": action.id, "mount": action.consumer.id(), "key": action.key,
+                    "method": action.method, "subscription": action.subscription,
+                    "pending": self.actions.len() + 1,
+                })
+            });
+            if !self.action_valid(&action) {
                 self.reject_action(action, "consumer retired before admission".into());
                 continue;
             }
@@ -1153,6 +1214,12 @@ impl<V: ?Sized + 'static> Application<V> {
         let mut first_error = None;
         for _ in 0..self.actions.outcomes.len() {
             let outcome = self.actions.outcomes.pop_front().expect("bounded outcomes");
+            diagnostics::record("feedback_polled", || {
+                json!({
+                    "id": outcome.request, "mount": outcome.consumer.id(),
+                    "key": outcome.key, "pending": self.actions.len(),
+                })
+            });
             let item = matches!(&outcome.outcome, ActionOutcome::Updated { .. });
             if self.is_retiring(&outcome.consumer)
                 || outcome.request.is_some_and(|id| {
@@ -1230,6 +1297,7 @@ impl<V: ?Sized + 'static> Application<V> {
                 }
             }
         }
+        self.wake_queued_actions();
         if let Some(error) = first_error {
             return Err(error);
         }
diff --git a/platform/ui-runtime/src/actions/mutations.rs b/platform/ui-runtime/src/actions/mutations.rs
index 6e30538ff3..2cdb0ffba5 100644
--- a/platform/ui-runtime/src/actions/mutations.rs
+++ b/platform/ui-runtime/src/actions/mutations.rs
@@ -217,6 +217,20 @@ impl<V: ?Sized + 'static> Application<V> {
         id: Uuid,
         completion: TaskCompletion,
     ) -> Result<Option<ActionTask>> {
+        diagnostics::record("action_completion", || {
+            json!({
+                "id": id, "pending": self.actions.len(),
+                "kind": match &completion {
+                    TaskCompletion::Query(_) => "query",
+                    TaskCompletion::Subscription(_) => "subscription",
+                    TaskCompletion::Prepared(_) => "prepared",
+                    TaskCompletion::Mutation(_) => "mutation",
+                    TaskCompletion::MutationProgress { .. } => "mutation_progress",
+                    TaskCompletion::Closed(_) => "closed",
+                    TaskCompletion::Interrupted(_) => "interrupted",
+                },
+            })
+        });
         let result = (|| match completion {
             TaskCompletion::Subscription(completion) => self.finish_subscription(id, completion),
             TaskCompletion::Query(completion) => {
@@ -251,6 +265,7 @@ impl<V: ?Sized + 'static> Application<V> {
             }
         })();
         self.sync_subscription_demand();
+        self.wake_queued_actions();
         result
     }
 
diff --git a/platform/ui-runtime/src/actions/subscriptions.rs b/platform/ui-runtime/src/actions/subscriptions.rs
index f974b6e20c..6265d5ec1b 100644
--- a/platform/ui-runtime/src/actions/subscriptions.rs
+++ b/platform/ui-runtime/src/actions/subscriptions.rs
@@ -69,6 +69,14 @@ impl<V: ?Sized + 'static> Application<V> {
             if let Some(flow) = &running.stream
                 && !self.subscription_wanted(&running.consumer, &running.key, *id)
             {
+                if !flow.stop.is_cancelled() {
+                    diagnostics::record("subscription_stop", || {
+                        json!({
+                            "id": id, "mount": running.consumer.id(),
+                            "key": running.key, "method": running.method,
+                        })
+                    });
+                }
                 flow.stop.cancel();
             }
             if let Some(session) = &running.mutation
diff --git a/platform/ui-runtime/src/diagnostics.rs b/platform/ui-runtime/src/diagnostics.rs
new file mode 100644
index 0000000000..580cd6a8cf
--- /dev/null
+++ b/platform/ui-runtime/src/diagnostics.rs
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+//! Opt-in UI diagnostics, enabled with `MAKA_UI_TRACE=/absolute/path.jsonl`.
+//! Payloads contain scheduling metadata only, never service inputs or outputs.
+
+use serde_json::{Value, json};
+use std::{
+    fs::OpenOptions,
+    io::Write,
+    sync::{
+        OnceLock,
+        atomic::{AtomicU64, Ordering},
+        mpsc,
+    },
+    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
+};
+
+struct Trace {
+    sender: mpsc::SyncSender<Message>,
+    dropped: AtomicU64,
+}
+enum Message {
+    Event(Value),
+    Flush(mpsc::SyncSender<()>),
+}
+fn trace() -> Option<&'static Trace> {
+    static TRACE: OnceLock<Option<Trace>> = OnceLock::new();
+    TRACE
+        .get_or_init(|| {
+            let path = std::env::var_os("MAKA_UI_TRACE")?;
+            let mut file = OpenOptions::new()
+                .create(true)
+                .append(true)
+                .open(path)
+                .ok()?;
+            let (sender, receiver) = mpsc::sync_channel(4096);
+            std::thread::Builder::new()
+                .name("maka-ui-trace".into())
+                .spawn(move || {
+                    let header = json!({
+                        "event": "trace_started",
+                        "pid": std::process::id(),
+                        "time_us": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_micros(),
+                        "fields": {
+                            "debug_build": cfg!(debug_assertions),
+                            "version": env!("CARGO_PKG_VERSION"),
+                        },
+                    });
+                    if serde_json::to_writer(&mut file, &header).is_err()
+                        || file.write_all(b"\n").is_err()
+                    {
+                        return;
+                    }
+                    while let Ok(message) = receiver.recv() {
+                        match message {
+                            Message::Event(value) => {
+                                if serde_json::to_writer(&mut file, &value).is_err()
+                                    || file.write_all(b"\n").is_err()
+                                {
+                                    break;
+                                }
+                            }
+                            Message::Flush(done) => {
+                                let _ = file.flush();
+                                let _ = done.try_send(());
+                            }
+                        }
+                    }
+                })
+                .ok()?;
+            Some(Trace {
+                sender,
+                dropped: AtomicU64::new(0),
+            })
+        })
+        .as_ref()
+}
+/// Record lazily without blocking the foreground on filesystem writes.
+/// A saturated diagnostic queue drops records and reports the count next time.
+pub fn record(event: &'static str, fields: impl FnOnce() -> Value) {
+    let Some(trace) = trace() else { return };
+    let dropped = trace.dropped.swap(0, Ordering::Relaxed);
+    let value = json!({
+        "time_us": SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_micros(),
+        "pid": std::process::id(), "event": event, "dropped": dropped, "fields": fields(),
+    });
+    if trace.sender.try_send(Message::Event(value)).is_err() {
+        trace.dropped.fetch_add(dropped + 1, Ordering::Relaxed);
+    }
+}
+/// Give fatal diagnostics a bounded opportunity to reach disk before exit.
+pub fn flush() {
+    if let Some(trace) = trace() {
+        let (send, receive) = mpsc::sync_channel(1);
+        if trace.sender.try_send(Message::Flush(send)).is_ok() {
+            let _ = receive.recv_timeout(Duration::from_millis(250));
+        }
+    }
+}
+/// Start a latency measurement only when diagnostics are enabled.
+pub fn started() -> Option<Instant> {
+    trace().map(|_| Instant::now())
+}
+/// Log synchronous work exceeding one 60 Hz frame, including error returns.
+/// Do not wrap waits for input: idle time is not UI work.
+pub struct Span {
+    event: &'static str,
+    start: Option<Instant>,
+}
+impl Span {
+    pub fn new(event: &'static str) -> Self {
+        Self {
+            event,
+            start: trace().map(|_| Instant::now()),
+        }
+    }
+}
+impl Drop for Span {
+    fn drop(&mut self) {
+        if let Some(start) = self.start {
+            let elapsed = start.elapsed();
+            if elapsed >= Duration::from_millis(16) {
+                record(self.event, || json!({"elapsed_us": elapsed.as_micros()}));
+            }
+        }
+    }
+}
diff --git a/platform/ui-runtime/src/lib.rs b/platform/ui-runtime/src/lib.rs
index 33029b3442..ba3a82c5fb 100644
--- a/platform/ui-runtime/src/lib.rs
+++ b/platform/ui-runtime/src/lib.rs
@@ -26,6 +26,7 @@ pub mod backend;
 pub mod budget;
 pub mod checkpoint;
 mod deployment;
+pub mod diagnostics;
 pub mod diff;
 pub mod dismiss;
 pub mod editor;
diff --git a/platform/ui-runtime/src/tree.rs b/platform/ui-runtime/src/tree.rs
index 52e4a1e10b..3c37b4e1d1 100644
--- a/platform/ui-runtime/src/tree.rs
+++ b/platform/ui-runtime/src/tree.rs
@@ -1077,7 +1077,9 @@ impl<V: ?Sized + 'static> Application<V> {
                 continue;
             }
             if let Some(action) = finish.actions.pop_front() {
-                if self.actions.len() + update.actions.len() >= 64 {
+                if self.actions.len() + update.actions.len() >= actions::MAX_PENDING_ACTIONS {
+                    self.actions
+                        .trace_limit(&finish.mount, &action, &update.actions);
                     return Err(Error::Invalid("pending action limit".into()));
                 }
                 finish.preparing =
@@ -1102,7 +1104,9 @@ impl<V: ?Sized + 'static> Application<V> {
                 if let Some(old) = old {
                     finish.demands.insert(action.key.clone(), old.clone());
                 } else {
-                    if self.actions.len() + update.actions.len() >= 64 {
+                    if self.actions.len() + update.actions.len() >= actions::MAX_PENDING_ACTIONS {
+                        self.actions
+                            .trace_limit(&finish.mount, &action, &update.actions);
                         return Err(Error::Invalid("pending action limit".into()));
                     }
                     finish.preparing = Some(self.begin_prepare_action(
diff --git a/plugins/agent/agent-screen/conversation-view.ts b/plugins/agent/agent-screen/conversation-view.ts
index 31f09af589..0f20dc27fd 100644
--- a/plugins/agent/agent-screen/conversation-view.ts
+++ b/plugins/agent/agent-screen/conversation-view.ts
@@ -213,6 +213,7 @@ export function conversationView<N extends Node>(
                 locale,
                 target,
               );
+        props.active = entry === visit;
         if (entry !== visit) props.markReadEnabled = false;
         return ui.box(
           'task-reader-' + entry.key,
diff --git a/plugins/agent/tool-details/index.ts b/plugins/agent/tool-details/index.ts
index ac3bbe5fed..d84b19703a 100644
--- a/plugins/agent/tool-details/index.ts
+++ b/plugins/agent/tool-details/index.ts
@@ -348,6 +348,7 @@ export function toolDetails<N extends Node>(
     }
     for (const [id, part] of Object.entries(state.parts))
       if (
+        props.load !== false &&
         (props.expanded || compactPart(id)) &&
         (!part.page || part.preparing) &&
         !part.pending &&
@@ -787,6 +788,7 @@ export function toolDetails<N extends Node>(
               };
             if (
               value.kind === 'preparing' &&
+              props.load !== false &&
               !part.failed &&
               (props.expanded || compactPart(entry![0]))
             )
diff --git a/plugins/shared/components.ts b/plugins/shared/components.ts
index cdbf92ea31..b9b15260d8 100644
--- a/plugins/shared/components.ts
+++ b/plugins/shared/components.ts
@@ -659,6 +659,11 @@ export function timeline<N extends Node>(
                 locale: props.locale,
                 context: props.toolContext,
                 tool: message.tool,
+                load:
+                  props.active !== false &&
+                  (expanded ||
+                    visible.includes('message-' + message.id) ||
+                    browsing?.id === message.id),
                 expanded,
                 focusOnPointer: !search,
               },
diff --git a/plugins/shared/contracts.ts b/plugins/shared/contracts.ts
index 29862bde50..a65955263c 100644
--- a/plugins/shared/contracts.ts
+++ b/plugins/shared/contracts.ts
@@ -262,6 +262,7 @@ export interface SourcePage {
   landing: 'start' | 'end';
 }
 export interface ToolDetailsProps {
+  load?: boolean;
   theme: Theme;
   locale: string;
   context: { sessionId: string; through: number };
@@ -270,6 +271,7 @@ export interface ToolDetailsProps {
   focusOnPointer: boolean;
 }
 export interface TimelineProps {
+  active?: boolean;
   leadingPrompt?: { source: number; text: string } | null;
   activeOutput?: Message | null;
   copyRequest: number | null;
@@ -824,14 +826,18 @@ export const imageViewerContract = component(
 const filePath = { anyOf: [{ type: 'string', minLength: 1, maxLength: 4096 }, { type: 'null' }] };
 export const toolDetailsContract = component(
   'maka.agent.ToolDetails',
-  object({
-    theme: { $ref: '#/$defs/Theme' },
-    locale,
-    context: object({ sessionId: string, through: { type: 'integer', minimum: 1 } }),
-    tool: { $ref: '#/$defs/HistoryTool' },
-    expanded: { type: 'boolean' },
-    focusOnPointer: { type: 'boolean' },
-  }),
+  object(
+    {
+      load: { type: 'boolean' },
+      theme: { $ref: '#/$defs/Theme' },
+      locale,
+      context: object({ sessionId: string, through: { type: 'integer', minimum: 1 } }),
+      tool: { $ref: '#/$defs/HistoryTool' },
+      expanded: { type: 'boolean' },
+      focusOnPointer: { type: 'boolean' },
+    },
+    ['theme', 'locale', 'context', 'tool', 'expanded', 'focusOnPointer'],
+  ),
   {
     fileContext: object({
       sessionId: string,
@@ -848,6 +854,7 @@ export const timelineContract = component(
   'maka.agent.Timeline',
   object(
     {
+      active: { type: 'boolean' },
       leadingPrompt: {
         anyOf: [
           object({ source: { type: 'integer', minimum: 1 }, text: string }),
diff --git a/products/maka/bundle/tests/timeline.test.mjs b/products/maka/bundle/tests/timeline.test.mjs
index 8172dc2e0b..b256ea6819 100644
--- a/products/maka/bundle/tests/timeline.test.mjs
+++ b/products/maka/bundle/tests/timeline.test.mjs
@@ -480,3 +480,90 @@ for (const target of ['tui', 'desktop']) {
     assert.equal(source().props.terminal.foreground, theme.terminal.foreground);
   });
 }
+
+test('tool details follow the visible viewport and suspend in a cached reader', () => {
+  const props = {
+    theme,
+    locale: 'en',
+    active: true,
+    copyRequest: null,
+    browse: null,
+    timings: [],
+    markReadEnabled: false,
+    toolContext: { sessionId: 'session', through: 8 },
+    waitingTools: [],
+    liveTools: [],
+    source: null,
+    search: null,
+    loading: false,
+    older: false,
+    newer: false,
+    latest: false,
+    landing: { sequence: 0, edge: 'end' },
+    live: false,
+    messages: Array.from({ length: 40 }, (_, i) => ({
+      id: 'tool-' + i,
+      contentKey: 'tool-' + i,
+      role: 'tool_call',
+      text: 'Read',
+      timestamp: null,
+      source: 7,
+      image: false,
+      continued: false,
+      tool: {
+        toolUseId: 'read-' + i,
+        call: {
+          sequence: 7,
+          name: 'Read',
+          title: null,
+          presentation: 'maka.workspace.Files@1/readText',
+          pending: false,
+          dispatched: true,
+        },
+        result: { sequence: 8, isError: false, settlement: 'returned', image: false },
+      },
+    })),
+  };
+  const find = (node, key) => {
+    if (!node || typeof node !== 'object') return undefined;
+    if (node.key === key) return node;
+    for (const value of Object.values(node)) {
+      for (const child of Array.isArray(value) ? value : [value]) {
+        const found = find(child, key);
+        if (found) return found;
+      }
+    }
+  };
+  let out = component.mount(props, {});
+  const detail = (index) => find(out.frame.view, 'tool-details-tool-' + index).props;
+  assert.equal(detail(0).load, false, 'initial placeholders can present without fetching details');
+  out = component.event(
+    out.frame.state,
+    {
+      kind: 'component',
+      child: 'reading',
+      name: 'observed',
+      event: {
+        readingRevision: 1,
+        selectedChildKey: null,
+        pendingCommand: null,
+        receipt: null,
+        viewport: {
+          readingRevision: 1,
+          atStart: false,
+          atEnd: false,
+          visibleChildKeys: ['message-tool-4', 'message-tool-5'],
+        },
+      },
+    },
+    props,
+    {},
+  );
+  for (let i = 0; i < 40; i++) assert.equal(detail(i).load, i === 4 || i === 5);
+  props.active = false;
+  out = component.update(out.frame.state, props, {});
+  assert.equal(detail(4).load, false);
+  props.active = true;
+  out = component.update(out.frame.state, props, {});
+  assert.equal(detail(4).load, true);
+});
diff --git a/products/maka/bundle/tests/tool-details.test.mjs b/products/maka/bundle/tests/tool-details.test.mjs
new file mode 100644
index 0000000000..f04dbfe74f
--- /dev/null
+++ b/products/maka/bundle/tests/tool-details.test.mjs
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { test } from 'node:test';
+import Ajv from 'ajv/dist/2020.js';
+const { activate } = await import('../dist/packages/appearance/tui.js');
+const theme = activate({ scheme: 'dark' }).Appearance.value;
+const { component, activate: detailsExports } = await import(
+  '../dist/packages/tool-details/tui.js'
+);
+
+function props() {
+  return {
+    theme,
+    locale: 'en',
+    load: false,
+    context: { sessionId: 'session', through: 8 },
+    tool: {
+      toolUseId: 'read-file',
+      call: {
+        sequence: 7,
+        name: 'Read',
+        title: null,
+        presentation: 'maka.workspace.Files@1/readText',
+        pending: false,
+        dispatched: true,
+      },
+      result: { sequence: 8, isError: false, settlement: 'returned', image: false },
+    },
+    expanded: false,
+    focusOnPointer: true,
+  };
+}
+function reply(out, action, value, current) {
+  return component.event(
+    out.frame.state,
+    { kind: 'action', key: action.key, event: { status: 'succeeded', value } },
+    current,
+    {},
+  );
+}
+function page(action, current, text) {
+  return {
+    kind: 'ready',
+    through: 8,
+    filePath: null,
+    source: {
+      sequence: action.input.source.sequence,
+      field: action.input.field,
+      path: action.input.path,
+      format: 'text',
+      text,
+      offset: 0,
+      totalBytes: Buffer.byteLength(text),
+      tool: current.tool,
+    },
+  };
+}
+test('tool summaries wait for demand and reuse completed reads after leaving the viewport', () => {
+  const current = props();
+  let out = component.mount(current, {});
+  assert.equal(out.actions.length, 0);
+  current.load = true;
+  out = component.update(out.frame.state, current, {});
+  const actions = out.actions;
+  assert.equal(actions.length, 2);
+  current.load = false;
+  out = component.update(out.frame.state, current, {});
+  assert.equal(out.actions.length, 0);
+  for (const action of actions) {
+    const text = action.input.field === 'arguments' ? '/workspace/source.rs' : 'completed';
+    out = reply(out, action, page(action, current, text), current);
+    assert.equal(out.actions.length, 0);
+  }
+  current.load = true;
+  out = component.update(out.frame.state, current, {});
+  assert.equal(out.actions.length, 0, 'completed summary does not reload on re-entry');
+  current.expanded = true;
+  out = component.update(out.frame.state, current, {});
+  assert.equal(out.actions.length, 4, 'expanded Read fetches content and line metadata');
+});
+
+test('a preparing source pauses polling when its demand is suspended', () => {
+  const current = props();
+  current.load = true;
+  let out = component.mount(current, {});
+  const action = out.actions[0];
+  current.load = false;
+  out = reply(out, action, { kind: 'preparing', through: 8 }, current);
+  assert.equal(out.actions.length, 0);
+  current.load = true;
+  out = component.update(out.frame.state, current, {});
+  assert.equal(out.actions.length, 1);
+  assert.deepEqual(out.actions[0].input, action.input);
+});
+
+test('demand is optional for existing tool detail callers', () => {
+  const current = props();
+  delete current.load;
+  const schema = detailsExports().ToolDetails.contract.interface.props;
+  const valid = new Ajv({ strict: false }).compile(schema);
+  assert.equal(valid(current), true, JSON.stringify(valid.errors));
+  assert.equal(component.mount(current, {}).actions.length, 2);
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org