Skip to content

Commit 1912ebf

Browse files
committed
Materialize retained chat turns on demand
1 parent f78aa93 commit 1912ebf

2 files changed

Lines changed: 70 additions & 28 deletions

File tree

tools/wta/src/app_tests.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10182,6 +10182,30 @@ fn clearing_completed_turns_renews_retained_layout_identity() {
1018210182
assert_ne!(after[0].0, before[0].0);
1018310183
}
1018410184

10185+
#[test]
10186+
fn render_chat_materializes_only_viewport_plus_overscan_turns() {
10187+
let mut app = test_app();
10188+
app.state = ConnectionState::Connected;
10189+
for index in 0..200 {
10190+
app.current_tab_mut().completed_turns.push(CompletedTurn {
10191+
prompt: format!("RETAINED_VIEWPORT_TURN_{index:03}"),
10192+
details: vec![ChatMessage::Agent(format!("ACK_{index:03}"))],
10193+
expanded: false,
10194+
trailing_marker: None,
10195+
});
10196+
}
10197+
10198+
crate::ui::chat::reset_completed_turn_line_materialization_count();
10199+
let rendered = render_to_text(&mut app, 80, 16);
10200+
let materialized = crate::ui::chat::completed_turn_line_materialization_count();
10201+
10202+
assert!(rendered.contains("RETAINED_VIEWPORT_TURN_199"));
10203+
assert!(
10204+
(1..=48).contains(&materialized),
10205+
"a 16-row viewport plus 32-row overscan should materialize at most 48 turns; materialized {materialized}",
10206+
);
10207+
}
10208+
1018510209
#[test]
1018610210
fn input_history_deduplicates_and_caps_at_fifty() {
1018710211
let mut tab = TabSession::default();

tools/wta/src/ui/chat.rs

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::borrow::Cow;
33
use std::cell::Cell;
44
use std::cell::RefCell;
55
use std::collections::VecDeque;
6+
use std::rc::Rc;
67

78
use ratatui::prelude::*;
89
use ratatui::widgets::{Block, Borders, Paragraph, Wrap};
@@ -51,10 +52,7 @@ impl PreparedChatLayout {
5152

5253
struct PreparedCompletedTurn {
5354
index: usize,
54-
lines: Vec<Line<'static>>,
55-
height: usize,
56-
expanded: bool,
57-
prompt_rows: Vec<PromptRowGeometry>,
55+
layout: Rc<CachedCompletedTurn>,
5856
}
5957

6058
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -79,7 +77,7 @@ struct CachedCompletedTurn {
7977

8078
struct CompletedTurnCacheEntry {
8179
key: CompletedTurnCacheKey,
82-
value: CachedCompletedTurn,
80+
value: Rc<CachedCompletedTurn>,
8381
bytes: usize,
8482
}
8583

@@ -90,17 +88,23 @@ struct CompletedTurnLayoutCache {
9088
}
9189

9290
impl CompletedTurnLayoutCache {
93-
fn get(&mut self, key: CompletedTurnCacheKey) -> Option<CachedCompletedTurn> {
91+
fn get(&mut self, key: CompletedTurnCacheKey) -> Option<Rc<CachedCompletedTurn>> {
9492
let index = self.entries.iter().position(|entry| entry.key == key)?;
9593
let entry = self.entries.remove(index)?;
96-
let value = entry.value.clone();
94+
let value = Rc::clone(&entry.value);
9795
self.entries.push_back(entry);
9896
Some(value)
9997
}
10098

101-
fn insert(&mut self, key: CompletedTurnCacheKey, value: CachedCompletedTurn, bytes: usize) {
99+
fn insert(
100+
&mut self,
101+
key: CompletedTurnCacheKey,
102+
value: CachedCompletedTurn,
103+
bytes: usize,
104+
) -> Rc<CachedCompletedTurn> {
105+
let value = Rc::new(value);
102106
if bytes > MAX_COMPLETED_TURN_CACHE_ENTRY_BYTES {
103-
return;
107+
return value;
104108
}
105109
if let Some(index) = self.entries.iter().position(|entry| entry.key == key) {
106110
if let Some(previous) = self.entries.remove(index) {
@@ -116,8 +120,12 @@ impl CompletedTurnLayoutCache {
116120
self.bytes = self.bytes.saturating_sub(evicted.bytes);
117121
}
118122
self.bytes = self.bytes.saturating_add(bytes);
119-
self.entries
120-
.push_back(CompletedTurnCacheEntry { key, value, bytes });
123+
self.entries.push_back(CompletedTurnCacheEntry {
124+
key,
125+
value: Rc::clone(&value),
126+
bytes,
127+
});
128+
value
121129
}
122130
}
123131

@@ -129,6 +137,7 @@ thread_local! {
129137
#[cfg(test)]
130138
thread_local! {
131139
static COMPLETED_TURN_LINE_BUILD_COUNT: Cell<usize> = const { Cell::new(0) };
140+
static COMPLETED_TURN_LINE_MATERIALIZATION_COUNT: Cell<usize> = const { Cell::new(0) };
132141
}
133142

134143
#[cfg(test)]
@@ -146,6 +155,21 @@ fn record_completed_turn_line_build() {
146155
COMPLETED_TURN_LINE_BUILD_COUNT.with(|count| count.set(count.get() + 1));
147156
}
148157

158+
#[cfg(test)]
159+
pub(crate) fn reset_completed_turn_line_materialization_count() {
160+
COMPLETED_TURN_LINE_MATERIALIZATION_COUNT.with(|count| count.set(0));
161+
}
162+
163+
#[cfg(test)]
164+
pub(crate) fn completed_turn_line_materialization_count() -> usize {
165+
COMPLETED_TURN_LINE_MATERIALIZATION_COUNT.with(Cell::get)
166+
}
167+
168+
#[cfg(test)]
169+
fn record_completed_turn_line_materialization() {
170+
COMPLETED_TURN_LINE_MATERIALIZATION_COUNT.with(|count| count.set(count.get() + 1));
171+
}
172+
149173
fn tool_output_lines(output: &ToolCallOutput) -> Vec<String> {
150174
let mut lines = output.text.lines().rev();
151175
let mut tail: Vec<String> = lines
@@ -349,17 +373,12 @@ pub fn prepare(app: &mut App, area_width: u16) -> PreparedChatLayout {
349373
lines,
350374
};
351375
let bytes = completed_turn_cache_bytes(&value);
352-
COMPLETED_TURN_LAYOUT_CACHE.with(|cache| {
353-
cache.borrow_mut().insert(key, value.clone(), bytes);
354-
});
355-
value
376+
COMPLETED_TURN_LAYOUT_CACHE
377+
.with(|cache| cache.borrow_mut().insert(key, value, bytes))
356378
});
357379
PreparedCompletedTurn {
358380
index,
359-
height: cached.height,
360-
lines: cached.lines,
361-
expanded: cached.expanded,
362-
prompt_rows: cached.prompt_rows,
381+
layout: cached,
363382
}
364383
})
365384
.collect();
@@ -389,7 +408,7 @@ pub fn prepare(app: &mut App, area_width: u16) -> PreparedChatLayout {
389408
.sum::<usize>()
390409
+ completed_turns
391410
.iter()
392-
.map(|turn| turn.height)
411+
.map(|turn| turn.layout.height)
393412
.sum::<usize>()
394413
+ rendered_lines_height(&pending_lines, wrap_width)
395414
+ rendered_lines_height(&welcome_lines, wrap_width))
@@ -769,13 +788,12 @@ pub fn render(frame: &mut Frame, app: &mut App, area: Rect, prepared: PreparedCh
769788
let mut selection_reached = selection_target_idx.is_none();
770789
let mut rendered_rows_below = rendered_lines_height(&reversed_lines, wrap_width);
771790
for prepared_turn in completed_turns.into_iter().rev() {
772-
let PreparedCompletedTurn {
773-
index: idx,
774-
mut lines,
775-
height: turn_height,
776-
expanded,
777-
prompt_rows,
778-
} = prepared_turn;
791+
let PreparedCompletedTurn { index: idx, layout } = prepared_turn;
792+
#[cfg(test)]
793+
record_completed_turn_line_materialization();
794+
let turn_height = layout.height;
795+
let expanded = layout.expanded;
796+
let prompt_rows = layout.prompt_rows.clone();
779797
turn_hit_offsets.push((idx, rendered_rows_below, turn_height, expanded, prompt_rows));
780798
if selection_target_idx == Some(idx) {
781799
let selected_end = rendered_rows_below.saturating_add(turn_height);
@@ -792,7 +810,7 @@ pub fn render(frame: &mut Frame, app: &mut App, area: Rect, prepared: PreparedCh
792810
.saturating_add(32);
793811
selection_reached = true;
794812
}
795-
reversed_lines.extend(lines.drain(..).rev());
813+
reversed_lines.extend(layout.lines.iter().rev().cloned());
796814
rendered_rows_below = rendered_rows_below.saturating_add(turn_height);
797815
if reversed_lines.len() >= requested_lines && selection_reached {
798816
truncated = true;

0 commit comments

Comments
 (0)