chat: state-driven sync from agent entries

InteractScreen holds agent ref, syncs conversation display from
agent.entries() on each tick via blocking_lock(). Tracks generation
counter and entry count to detect compactions and new entries.

Agent gets a generation counter, incremented on compaction and
non-last-entry mutations (age_out_images).

sync_from_agent() is the single path for pane updates. UiMessage
handle_ui_message still exists but will be removed once sync
handles all entry types (streaming, tool calls, DMN).

Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-05 19:17:13 -04:00
parent 6f000bd0f6
commit 350c447ebc
3 changed files with 57 additions and 2 deletions

View file

@ -31,10 +31,15 @@ pub(crate) struct InteractScreen {
pub(crate) turn_started: Option<std::time::Instant>,
pub(crate) call_started: Option<std::time::Instant>,
pub(crate) call_timeout_secs: u64,
// State sync with agent
last_generation: u64,
last_entry_count: usize,
/// Reference to agent for state sync
agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>,
}
impl InteractScreen {
pub fn new() -> Self {
pub fn new(agent: std::sync::Arc<tokio::sync::Mutex<crate::agent::Agent>>) -> Self {
Self {
autonomous: PaneState::new(true),
conversation: PaneState::new(true),
@ -48,9 +53,51 @@ impl InteractScreen {
turn_started: None,
call_started: None,
call_timeout_secs: 60,
last_generation: 0,
last_entry_count: 0,
agent,
}
}
/// Sync conversation display from agent entries.
fn sync_from_agent(&mut self) {
let agent = self.agent.blocking_lock();
let gen = agent.generation;
let count = agent.entries().len();
if gen != self.last_generation {
// Generation changed — full re-render
self.conversation = PaneState::new(true);
self.autonomous = PaneState::new(true);
self.tools = PaneState::new(false);
self.last_entry_count = 0;
}
// Render new entries
if count > self.last_entry_count {
for entry in &agent.entries()[self.last_entry_count..] {
let msg = entry.message();
let text = msg.content_text();
match msg.role {
crate::agent::api::types::Role::User => {
self.conversation.push_line_with_marker(
text.to_string(), Color::Cyan, Marker::User,
);
}
crate::agent::api::types::Role::Assistant => {
self.conversation.push_line_with_marker(
text.to_string(), Color::Reset, Marker::Assistant,
);
}
_ => {}
}
}
}
self.last_generation = gen;
self.last_entry_count = count;
}
/// Process a UiMessage — update pane state.
pub fn handle_ui_message(&mut self, msg: &UiMessage, app: &mut App) {
match msg {
@ -399,6 +446,9 @@ impl ScreenView for InteractScreen {
}
}
// Sync state from agent
self.sync_from_agent();
// Draw
self.draw_main(frame, area, app);
None

View file

@ -382,7 +382,7 @@ pub async fn run(
let notify_rx = crate::thalamus::channels::subscribe_all();
// InteractScreen held separately for UiMessage routing
let mut interact = crate::user::chat::InteractScreen::new();
let mut interact = crate::user::chat::InteractScreen::new(mind.agent.clone());
// Overlay screens: F2=conscious, F3=subconscious, F4=unconscious, F5=thalamus
let mut screens: Vec<Box<dyn tui::ScreenView>> = vec![
Box::new(crate::user::context::ConsciousScreen::new()),