@@ -14,8 +14,8 @@ use warp::tui_export::{BlockHeight, BlockHeightItem, BlockHeightSummary, BlockId
1414use warpui:: { EntityId , ViewHandle } ;
1515use warpui_core:: AppContext ;
1616use warpui_core:: elements:: tui:: {
17- TuiChildView , TuiElement , TuiLayoutContext , TuiRowResize , TuiSelectionSpan , TuiViewportContent ,
18- TuiViewportWindow , TuiViewportedElement , TuiVisibleViewportItem ,
17+ TuiChildView , TuiConstraint , TuiElement , TuiLayoutContext , TuiRowResize , TuiSelectionSpan ,
18+ TuiSize , TuiViewportContent , TuiViewportWindow , TuiViewportedElement , TuiVisibleViewportItem ,
1919} ;
2020
2121use super :: agent_block:: TuiAIBlock ;
@@ -64,6 +64,7 @@ pub(super) struct TuiBlockListViewportSource {
6464 cli_subagent_blocks : CLISubagentBlockRegistry ,
6565 handoff_blocks : HandoffBlockRegistry ,
6666 height_changes : RefCell < Vec < TuiRowResize > > ,
67+ deferred_streaming_heights : RefCell < HashSet < EntityId > > ,
6768}
6869
6970impl TuiBlockListViewportSource {
@@ -79,6 +80,7 @@ impl TuiBlockListViewportSource {
7980 cli_subagent_blocks : Rc :: new ( RefCell :: new ( HashMap :: new ( ) ) ) ,
8081 handoff_blocks : Rc :: new ( RefCell :: new ( HashMap :: new ( ) ) ) ,
8182 height_changes : RefCell :: new ( Vec :: new ( ) ) ,
83+ deferred_streaming_heights : RefCell :: new ( HashSet :: new ( ) ) ,
8284 }
8385 }
8486 pub ( super ) fn new_with_rich_content (
@@ -93,6 +95,7 @@ impl TuiBlockListViewportSource {
9395 cli_subagent_blocks,
9496 handoff_blocks,
9597 height_changes : RefCell :: new ( Vec :: new ( ) ) ,
98+ deferred_streaming_heights : RefCell :: new ( HashSet :: new ( ) ) ,
9699 }
97100 }
98101
@@ -103,10 +106,11 @@ impl TuiBlockListViewportSource {
103106 ///
104107 /// A non-dirty band block is re-measured only when its cached height cannot
105108 /// be trusted: its last measurement was at a different width (reflow), it
106- /// has never been measured (no recorded width), or it is still streaming
107- /// (its height can grow without a per-update invalidation — e.g. an
108- /// expanded, still-running shell command). At a stable width with no
109- /// dynamic height, nothing extra is measured and the cached
109+ /// has never been measured (no recorded width), or it contains dynamic
110+ /// child content such as an expanded, still-running shell command. Agent
111+ /// output updates explicitly dirty their block, so animation-only repaints
112+ /// do not need to measure the entire streaming response again. At a stable
113+ /// width with no dynamic height, the cached
110114 /// `last_laid_out_height` is reused. Off-band blocks keep their cached
111115 /// height until they scroll into the band.
112116 fn agent_heights_to_measure (
@@ -117,16 +121,41 @@ impl TuiBlockListViewportSource {
117121 ) -> HashSet < EntityId > {
118122 let mut model = self . model . lock ( ) ;
119123 let mut view_ids = model. block_list_mut ( ) . take_dirty_rich_content_items ( ) ;
124+ view_ids. extend ( self . deferred_streaming_heights . borrow_mut ( ) . drain ( ) ) ;
120125
121126 let agent_blocks = self . agent_blocks . borrow ( ) ;
122127 let cli_subagent_blocks = self . cli_subagent_blocks . borrow ( ) ;
123128 let handoff_blocks = self . handoff_blocks . borrow ( ) ;
124- let block_list = model. block_list ( ) ;
125129 let band_top = window. scroll_top . saturating_sub ( OVERHANG_ROWS ) ;
126130 let band_bottom = window
127131 . scroll_top
128132 . saturating_add ( usize:: from ( window. viewport_height ) )
129133 . saturating_add ( OVERHANG_ROWS ) ;
134+ // A streaming tail below a fixed viewport cannot affect the visible
135+ // rows or their absolute top anchor. Consume this update's dirty bit
136+ // without measuring it, but retain the signal in this source so an
137+ // end-clamped or later approaching query can measure it. The final
138+ // non-streaming update is never deferred, so completed height remains
139+ // exact.
140+ let deferred_streaming = view_ids
141+ . iter ( )
142+ . filter ( |view_id| {
143+ model
144+ . block_list ( )
145+ . rich_content_row_range ( * * view_id)
146+ . is_some_and ( |rows| rows. start >= band_bottom)
147+ && agent_blocks
148+ . get ( view_id)
149+ . is_some_and ( |view| view. as_ref ( app) . is_streaming ( app) )
150+ } )
151+ . copied ( )
152+ . collect :: < HashSet < _ > > ( ) ;
153+ for view_id in & deferred_streaming {
154+ view_ids. remove ( view_id) ;
155+ }
156+ * self . deferred_streaming_heights . borrow_mut ( ) = deferred_streaming;
157+
158+ let block_list = model. block_list ( ) ;
130159 let mut cursor = block_list
131160 . block_heights ( )
132161 . cursor :: < BlockHeight , BlockHeightSummary > ( ) ;
@@ -180,19 +209,56 @@ impl TuiBlockListViewportSource {
180209 view_ids
181210 . into_iter ( )
182211 . filter_map ( |view_id| {
183- let height = if let Some ( view) = agent_blocks. get ( & view_id) {
184- let view = view. as_ref ( app) ;
185- let height = view. desired_height ( width, ctx, app) ;
212+ let height = if let Some ( view_handle) = agent_blocks. get ( & view_id) {
213+ let view = view_handle. as_ref ( app) ;
214+ let height = if ctx. rendered_views . contains_key ( & view_id) {
215+ usize:: from (
216+ TuiChildView :: new ( view_handle)
217+ . layout (
218+ TuiConstraint :: loose ( TuiSize :: new ( width, u16:: MAX ) ) ,
219+ ctx,
220+ app,
221+ )
222+ . height ,
223+ )
224+ } else {
225+ view. desired_height ( width, ctx, app)
226+ } ;
186227 view. record_height_measurement ( width) ;
187228 height
188- } else if let Some ( view) = cli_subagent_blocks. get ( & view_id) {
189- let view = view. as_ref ( app) ;
190- let height = view. desired_height ( width, ctx, app) ;
229+ } else if let Some ( view_handle) = cli_subagent_blocks. get ( & view_id) {
230+ let view = view_handle. as_ref ( app) ;
231+ let height = if ctx. rendered_views . contains_key ( & view_id) {
232+ usize:: from (
233+ TuiChildView :: new ( view_handle)
234+ . layout (
235+ TuiConstraint :: loose ( TuiSize :: new ( width, u16:: MAX ) ) ,
236+ ctx,
237+ app,
238+ )
239+ . height ,
240+ )
241+ } else {
242+ view. desired_height ( width, ctx, app)
243+ } ;
191244 view. record_height_measurement ( width) ;
192245 height
193246 } else {
194- let view = handoff_blocks. get ( & view_id) ?. as_ref ( app) ;
195- let height = view. desired_height ( width, ctx, app) ;
247+ let view_handle = handoff_blocks. get ( & view_id) ?;
248+ let view = view_handle. as_ref ( app) ;
249+ let height = if ctx. rendered_views . contains_key ( & view_id) {
250+ usize:: from (
251+ TuiChildView :: new ( view_handle)
252+ . layout (
253+ TuiConstraint :: loose ( TuiSize :: new ( width, u16:: MAX ) ) ,
254+ ctx,
255+ app,
256+ )
257+ . height ,
258+ )
259+ } else {
260+ view. desired_height ( width, ctx, app)
261+ } ;
196262 view. record_height_measurement ( width) ;
197263 height
198264 } ;
0 commit comments