chat: double-buffered sync with content-length diffing
Store content lengths of rendered entries. On each tick: - Generation changed → full pane reset - Entries removed → pop from tail - Last entry content length changed → pop and re-render (streaming) - New entries → route and push PaneState gains pop_line() for removing the last rendered entry. This handles streaming (last entry growing), compaction (generation bump), and normal appends. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
563771e979
commit
ca9f2b2b9a
2 changed files with 57 additions and 29 deletions
|
|
@ -78,9 +78,10 @@ pub(crate) struct InteractScreen {
|
||||||
pub(crate) turn_started: Option<std::time::Instant>,
|
pub(crate) turn_started: Option<std::time::Instant>,
|
||||||
pub(crate) call_started: Option<std::time::Instant>,
|
pub(crate) call_started: Option<std::time::Instant>,
|
||||||
pub(crate) call_timeout_secs: u64,
|
pub(crate) call_timeout_secs: u64,
|
||||||
// State sync with agent
|
// State sync with agent — double buffer
|
||||||
last_generation: u64,
|
last_generation: u64,
|
||||||
last_entry_count: usize,
|
/// Content lengths of rendered entries — for detecting changes
|
||||||
|
last_entry_lengths: Vec<usize>,
|
||||||
/// Reference to agent for state sync
|
/// Reference to agent for state sync
|
||||||
agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
|
agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +102,7 @@ impl InteractScreen {
|
||||||
call_started: None,
|
call_started: None,
|
||||||
call_timeout_secs: 60,
|
call_timeout_secs: 60,
|
||||||
last_generation: 0,
|
last_generation: 0,
|
||||||
last_entry_count: 0,
|
last_entry_lengths: Vec::new(),
|
||||||
agent,
|
agent,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -110,41 +111,63 @@ impl InteractScreen {
|
||||||
fn sync_from_agent(&mut self) {
|
fn sync_from_agent(&mut self) {
|
||||||
let agent = self.agent.blocking_lock();
|
let agent = self.agent.blocking_lock();
|
||||||
let gen = agent.generation;
|
let gen = agent.generation;
|
||||||
let count = agent.entries().len();
|
let entries = agent.entries();
|
||||||
|
|
||||||
// Phase 1: detect desync — pop entries from panes to match
|
// Phase 1: detect desync and pop
|
||||||
if gen != self.last_generation || count < self.last_entry_count {
|
if gen != self.last_generation {
|
||||||
// Compaction or mutation happened — full reset
|
|
||||||
// (could be smarter and pop from front, but reset is safe)
|
|
||||||
self.conversation = PaneState::new(true);
|
self.conversation = PaneState::new(true);
|
||||||
self.autonomous = PaneState::new(true);
|
self.autonomous = PaneState::new(true);
|
||||||
self.tools = PaneState::new(false);
|
self.tools = PaneState::new(false);
|
||||||
self.last_entry_count = 0;
|
self.last_entry_lengths.clear();
|
||||||
|
} else {
|
||||||
|
// Pop entries from the tail that were removed or changed
|
||||||
|
while self.last_entry_lengths.len() > entries.len() {
|
||||||
|
self.last_entry_lengths.pop();
|
||||||
|
// TODO: pop from correct pane
|
||||||
|
}
|
||||||
|
// Check if last entry changed (streaming)
|
||||||
|
if let (Some(&last_len), Some(entry)) = (
|
||||||
|
self.last_entry_lengths.last(),
|
||||||
|
entries.get(self.last_entry_lengths.len() - 1),
|
||||||
|
) {
|
||||||
|
let cur_len = entry.message().content_text().len();
|
||||||
|
if cur_len != last_len {
|
||||||
|
// Last entry changed — pop and re-render
|
||||||
|
self.last_entry_lengths.pop();
|
||||||
|
self.conversation.pop_line();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 2: route new entries to panes
|
// Phase 2: push new/changed entries
|
||||||
for entry in agent.entries().iter().skip(self.last_entry_count) {
|
let start = self.last_entry_lengths.len();
|
||||||
match route_entry(entry) {
|
for entry in entries.iter().skip(start) {
|
||||||
Some((PaneTarget::Conversation, text, marker)) => {
|
let msg = entry.message();
|
||||||
|
let text_len = msg.content_text().len();
|
||||||
|
|
||||||
|
if let Some((target, text, marker)) = route_entry(entry) {
|
||||||
|
match target {
|
||||||
|
PaneTarget::Conversation => {
|
||||||
self.conversation.push_line_with_marker(text, Color::Cyan, marker);
|
self.conversation.push_line_with_marker(text, Color::Cyan, marker);
|
||||||
}
|
}
|
||||||
Some((PaneTarget::ConversationAssistant, text, marker)) => {
|
PaneTarget::ConversationAssistant => {
|
||||||
self.conversation.push_line_with_marker(text, Color::Reset, marker);
|
self.conversation.push_line_with_marker(text, Color::Reset, marker);
|
||||||
}
|
}
|
||||||
Some((PaneTarget::Tools, text, _)) => {
|
PaneTarget::Tools => {
|
||||||
self.tools.push_line(text, Color::Yellow);
|
self.tools.push_line(text, Color::Yellow);
|
||||||
}
|
}
|
||||||
Some((PaneTarget::ToolResult, text, _)) => {
|
PaneTarget::ToolResult => {
|
||||||
for line in text.lines().take(20) {
|
for line in text.lines().take(20) {
|
||||||
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
self.tools.push_line(format!(" {}", line), Color::DarkGray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {} // skip (memory, system, system-reminder)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.last_entry_lengths.push(text_len);
|
||||||
|
}
|
||||||
|
|
||||||
self.last_generation = gen;
|
self.last_generation = gen;
|
||||||
self.last_entry_count = count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process a UiMessage — update pane state.
|
/// Process a UiMessage — update pane state.
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,11 @@ impl PaneState {
|
||||||
self.evict();
|
self.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pop_line(&mut self) {
|
||||||
|
self.lines.pop();
|
||||||
|
self.markers.pop();
|
||||||
|
}
|
||||||
|
|
||||||
fn scroll_up(&mut self, n: u16) {
|
fn scroll_up(&mut self, n: u16) {
|
||||||
self.scroll = self.scroll.saturating_sub(n);
|
self.scroll = self.scroll.saturating_sub(n);
|
||||||
self.pinned = true;
|
self.pinned = true;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue