Skip to content

Commit 2865f1f

Browse files
committed
Retain completed turn height index
1 parent 1912ebf commit 2865f1f

3 files changed

Lines changed: 398 additions & 59 deletions

File tree

tools/wta/src/app/tab_state.rs

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ use super::{TabAutofixState, TurnState};
1212
pub(crate) const DEFAULT_TAB_ID: &str = "0";
1313

1414
static NEXT_COMPLETED_TURN_LAYOUT_NAMESPACE: AtomicU64 = AtomicU64::new(1);
15+
const MAX_COMPLETED_TURN_LAYOUT_CHANGES: usize = 2048;
16+
17+
pub(crate) struct CompletedTurnLayoutChanges {
18+
pub namespace: u64,
19+
pub generation: u64,
20+
pub len: usize,
21+
pub dirty_indices: Option<Vec<usize>>,
22+
}
1523

1624
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1725
pub enum NoticeKind {
@@ -407,6 +415,8 @@ pub struct TabSession {
407415
pub(crate) completed_turn_layout_ids: Vec<u64>,
408416
pub(crate) completed_turn_layout_revisions: Vec<u64>,
409417
pub(crate) next_completed_turn_layout_id: u64,
418+
pub(crate) completed_turn_layout_generation: u64,
419+
pub(crate) completed_turn_layout_changes: VecDeque<(u64, usize)>,
410420
/// Tab/Shift+Tab selects a past turn (most recent first). Enter then
411421
/// toggles `CompletedTurn.expanded`. None means no selection — Enter
412422
/// goes to the input/prompt path as before.
@@ -523,22 +533,50 @@ pub struct TabSession {
523533
}
524534

525535
impl TabSession {
526-
pub(crate) fn completed_turn_layout_metadata(&mut self) -> (u64, Vec<(u64, u64)>) {
536+
fn record_completed_turn_layout_change(&mut self, index: usize) {
537+
if self.completed_turn_layout_generation == u64::MAX {
538+
self.completed_turn_layout_namespace =
539+
NEXT_COMPLETED_TURN_LAYOUT_NAMESPACE.fetch_add(1, Ordering::Relaxed);
540+
self.completed_turn_layout_generation = 1;
541+
self.completed_turn_layout_changes.clear();
542+
} else {
543+
self.completed_turn_layout_generation += 1;
544+
}
545+
self.completed_turn_layout_changes
546+
.push_back((self.completed_turn_layout_generation, index));
547+
while self.completed_turn_layout_changes.len() > MAX_COMPLETED_TURN_LAYOUT_CHANGES {
548+
self.completed_turn_layout_changes.pop_front();
549+
}
550+
}
551+
552+
fn sync_completed_turn_layout_metadata(&mut self) {
527553
if self.completed_turn_layout_namespace == 0 {
528554
self.completed_turn_layout_namespace =
529555
NEXT_COMPLETED_TURN_LAYOUT_NAMESPACE.fetch_add(1, Ordering::Relaxed);
530556
}
531557

532558
let len = self.completed_turns.len();
559+
if self.completed_turn_layout_ids.len() > len {
560+
self.completed_turn_layout_namespace =
561+
NEXT_COMPLETED_TURN_LAYOUT_NAMESPACE.fetch_add(1, Ordering::Relaxed);
562+
self.completed_turn_layout_generation = 0;
563+
self.completed_turn_layout_changes.clear();
564+
}
533565
self.completed_turn_layout_ids.truncate(len);
534566
self.completed_turn_layout_revisions.truncate(len);
535567
while self.completed_turn_layout_ids.len() < len {
568+
let index = self.completed_turn_layout_ids.len();
536569
self.next_completed_turn_layout_id =
537570
self.next_completed_turn_layout_id.wrapping_add(1).max(1);
538571
self.completed_turn_layout_ids
539572
.push(self.next_completed_turn_layout_id);
540573
self.completed_turn_layout_revisions.push(0);
574+
self.record_completed_turn_layout_change(index);
541575
}
576+
}
577+
578+
pub(crate) fn completed_turn_layout_metadata(&mut self) -> (u64, Vec<(u64, u64)>) {
579+
self.sync_completed_turn_layout_metadata();
542580

543581
(
544582
self.completed_turn_layout_namespace,
@@ -550,17 +588,98 @@ impl TabSession {
550588
)
551589
}
552590

591+
pub(crate) fn completed_turn_layout_changes_since(
592+
&mut self,
593+
previous: Option<(u64, u64)>,
594+
) -> CompletedTurnLayoutChanges {
595+
self.sync_completed_turn_layout_metadata();
596+
let namespace = self.completed_turn_layout_namespace;
597+
let generation = self.completed_turn_layout_generation;
598+
let len = self.completed_turns.len();
599+
let Some((previous_namespace, previous_generation)) = previous else {
600+
return CompletedTurnLayoutChanges {
601+
namespace,
602+
generation,
603+
len,
604+
dirty_indices: None,
605+
};
606+
};
607+
if previous_namespace != namespace {
608+
return CompletedTurnLayoutChanges {
609+
namespace,
610+
generation,
611+
len,
612+
dirty_indices: None,
613+
};
614+
}
615+
if previous_generation == generation {
616+
return CompletedTurnLayoutChanges {
617+
namespace,
618+
generation,
619+
len,
620+
dirty_indices: Some(Vec::new()),
621+
};
622+
}
623+
624+
let Some(first_retained_generation) = self
625+
.completed_turn_layout_changes
626+
.front()
627+
.map(|(generation, _)| *generation)
628+
else {
629+
return CompletedTurnLayoutChanges {
630+
namespace,
631+
generation,
632+
len,
633+
dirty_indices: None,
634+
};
635+
};
636+
if previous_generation.saturating_add(1) < first_retained_generation {
637+
return CompletedTurnLayoutChanges {
638+
namespace,
639+
generation,
640+
len,
641+
dirty_indices: None,
642+
};
643+
}
644+
645+
let mut dirty_indices = self
646+
.completed_turn_layout_changes
647+
.iter()
648+
.filter(|(change_generation, _)| *change_generation > previous_generation)
649+
.map(|(_, index)| *index)
650+
.collect::<Vec<_>>();
651+
dirty_indices.sort_unstable();
652+
dirty_indices.dedup();
653+
CompletedTurnLayoutChanges {
654+
namespace,
655+
generation,
656+
len,
657+
dirty_indices: Some(dirty_indices),
658+
}
659+
}
660+
661+
pub(crate) fn completed_turn_layout_item(&self, index: usize) -> Option<(u64, u64)> {
662+
Some((
663+
*self.completed_turn_layout_ids.get(index)?,
664+
*self.completed_turn_layout_revisions.get(index)?,
665+
))
666+
}
667+
553668
pub(crate) fn mark_completed_turn_layout_dirty(&mut self, index: usize) {
554-
let _ = self.completed_turn_layout_metadata();
669+
self.sync_completed_turn_layout_metadata();
555670
if let Some(revision) = self.completed_turn_layout_revisions.get_mut(index) {
556671
*revision = revision.wrapping_add(1);
672+
self.record_completed_turn_layout_change(index);
557673
}
558674
}
559675

560676
pub fn clear_completed_turns(&mut self) {
561677
self.completed_turns.clear();
678+
self.completed_turn_layout_namespace = 0;
562679
self.completed_turn_layout_ids.clear();
563680
self.completed_turn_layout_revisions.clear();
681+
self.completed_turn_layout_generation = 0;
682+
self.completed_turn_layout_changes.clear();
564683
}
565684

566685
pub fn set_last_completed_turn_trailing_marker(&mut self, marker: String) -> bool {

tools/wta/src/app_tests.rs

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7966,7 +7966,7 @@ fn completed_turn_prompt_rows_expose_state_aware_action_links() {
79667966
&& link.end_column > triangle.start_column
79677967
}));
79687968

7969-
app.current_tab_mut().completed_turns[0].expanded = false;
7969+
assert!(app.current_tab_mut().set_completed_turn_expanded(0, false));
79707970
render_to_text(&mut app, 80, 16);
79717971
assert_eq!(app.completed_turn_action_links.len(), 1);
79727972
assert_eq!(
@@ -10116,34 +10116,40 @@ fn render_chat_keeps_keyboard_selected_completed_turn_visible() {
1011610116

1011710117
app.current_tab_mut().toggle_selected_completed_turn();
1011810118
crate::ui::chat::reset_completed_turn_line_build_count();
10119+
crate::ui::chat::reset_completed_turn_descriptor_lookup_count();
1011910120
let collapsed = render_to_text(&mut app, 80, 16);
1012010121
assert_eq!(
1012110122
crate::ui::chat::completed_turn_line_build_count(),
1012210123
1,
1012310124
"collapsing one completed turn must invalidate only that item",
1012410125
);
10126+
assert_eq!(crate::ui::chat::completed_turn_descriptor_lookup_count(), 1);
1012510127
assert_ne!(collapsed, retained);
1012610128

1012710129
app.current_tab_mut().toggle_selected_completed_turn();
1012810130
crate::ui::chat::reset_completed_turn_line_build_count();
10131+
crate::ui::chat::reset_completed_turn_descriptor_lookup_count();
1012910132
let expanded = render_to_text(&mut app, 80, 16);
1013010133
assert_eq!(
1013110134
crate::ui::chat::completed_turn_line_build_count(),
1013210135
1,
1013310136
"expanding one completed turn must invalidate only that item",
1013410137
);
10138+
assert_eq!(crate::ui::chat::completed_turn_descriptor_lookup_count(), 1);
1013510139
assert!(expanded.contains("ACK_SELECT_SCROLL_TURN_00"));
1013610140

1013710141
assert!(app
1013810142
.current_tab_mut()
1013910143
.set_last_completed_turn_trailing_marker("CACHED_MARKER".into()));
1014010144
crate::ui::chat::reset_completed_turn_line_build_count();
10145+
crate::ui::chat::reset_completed_turn_descriptor_lookup_count();
1014110146
let _ = render_to_text(&mut app, 80, 16);
1014210147
assert_eq!(
1014310148
crate::ui::chat::completed_turn_line_build_count(),
1014410149
1,
1014510150
"updating one trailing marker must invalidate only that item",
1014610151
);
10152+
assert_eq!(crate::ui::chat::completed_turn_descriptor_lookup_count(), 1);
1014710153

1014810154
for _ in 0..11 {
1014910155
app.handle_key(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE));
@@ -10178,10 +10184,38 @@ fn clearing_completed_turns_renews_retained_layout_identity() {
1017810184
tab.completed_turns.push(turn);
1017910185
let (same_namespace, after) = tab.completed_turn_layout_metadata();
1018010186

10181-
assert_eq!(same_namespace, namespace);
10187+
assert_ne!(same_namespace, namespace);
1018210188
assert_ne!(after[0].0, before[0].0);
1018310189
}
1018410190

10191+
#[test]
10192+
fn completed_turn_layout_change_log_wrap_and_overflow_force_safe_rebuilds() {
10193+
let mut tab = TabSession::default();
10194+
tab.completed_turns.push(CompletedTurn {
10195+
prompt: "turn".into(),
10196+
details: Vec::new(),
10197+
expanded: false,
10198+
trailing_marker: None,
10199+
});
10200+
let (namespace, _) = tab.completed_turn_layout_metadata();
10201+
10202+
tab.completed_turn_layout_generation = u64::MAX;
10203+
tab.mark_completed_turn_layout_dirty(0);
10204+
let wrapped = tab.completed_turn_layout_changes_since(Some((namespace, u64::MAX)));
10205+
assert_ne!(wrapped.namespace, namespace);
10206+
assert!(wrapped.dirty_indices.is_none());
10207+
10208+
let baseline = (wrapped.namespace, wrapped.generation);
10209+
for _ in 0..=2048 {
10210+
tab.mark_completed_turn_layout_dirty(0);
10211+
}
10212+
let overflowed = tab.completed_turn_layout_changes_since(Some(baseline));
10213+
assert!(
10214+
overflowed.dirty_indices.is_none(),
10215+
"falling behind the bounded change log must force a full rebuild",
10216+
);
10217+
}
10218+
1018510219
#[test]
1018610220
fn render_chat_materializes_only_viewport_plus_overscan_turns() {
1018710221
let mut app = test_app();
@@ -10204,6 +10238,30 @@ fn render_chat_materializes_only_viewport_plus_overscan_turns() {
1020410238
(1..=48).contains(&materialized),
1020510239
"a 16-row viewport plus 32-row overscan should materialize at most 48 turns; materialized {materialized}",
1020610240
);
10241+
10242+
crate::ui::chat::reset_completed_turn_descriptor_lookup_count();
10243+
let retained = render_to_text(&mut app, 80, 16);
10244+
assert_eq!(retained, rendered);
10245+
assert_eq!(
10246+
crate::ui::chat::completed_turn_descriptor_lookup_count(),
10247+
0,
10248+
"an unchanged bottom frame must reuse the retained height index without scanning history",
10249+
);
10250+
10251+
app.current_tab_mut().completed_turns.push(CompletedTurn {
10252+
prompt: "RETAINED_VIEWPORT_TURN_200".into(),
10253+
details: vec![ChatMessage::Agent("ACK_200".into())],
10254+
expanded: false,
10255+
trailing_marker: None,
10256+
});
10257+
crate::ui::chat::reset_completed_turn_descriptor_lookup_count();
10258+
let appended = render_to_text(&mut app, 80, 16);
10259+
assert!(appended.contains("RETAINED_VIEWPORT_TURN_200"));
10260+
assert_eq!(
10261+
crate::ui::chat::completed_turn_descriptor_lookup_count(),
10262+
1,
10263+
"appending one turn must update only the new height-index item",
10264+
);
1020710265
}
1020810266

1020910267
#[test]

0 commit comments

Comments
 (0)