From 563771e979236faac5c7b31bfaf12183fa850bcb Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 19:27:14 -0400 Subject: [PATCH] chat: route_entry helper separates routing from sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PaneTarget enum + route_entry() function: given a ConversationEntry, returns which pane it belongs to (or None to skip). The sync loop becomes: detect desync → pop, then route new entries. Routing: User→Conversation, Assistant→ConversationAssistant, tool_calls→Tools, Tool results→ToolResult, Memory/System→None. Co-Authored-By: Kent Overstreet --- src/user/chat.rs | 108 +++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 46 deletions(-) diff --git a/src/user/chat.rs b/src/user/chat.rs index 7b213b3..d6c866a 100644 --- a/src/user/chat.rs +++ b/src/user/chat.rs @@ -18,6 +18,53 @@ use super::{ }; use crate::user::ui_channel::{UiMessage, StreamTarget}; +enum PaneTarget { + Conversation, + ConversationAssistant, + Tools, + ToolResult, +} + +/// Route an agent entry to the appropriate pane. +/// Returns None for entries that shouldn't be displayed (memory, system). +fn route_entry(entry: &crate::agent::context::ConversationEntry) -> Option<(PaneTarget, String, Marker)> { + use crate::agent::api::types::Role; + use crate::agent::context::ConversationEntry; + + if let ConversationEntry::Memory { .. } = entry { + return None; + } + + let msg = entry.message(); + let text = msg.content_text().to_string(); + + match msg.role { + Role::User => { + if text.starts_with("") { return None; } + Some((PaneTarget::Conversation, text, Marker::User)) + } + Role::Assistant => { + // Tool calls → tools pane + if let Some(ref calls) = msg.tool_calls { + for call in calls { + let line = format!("[{}] {}", + call.function.name, + call.function.arguments.chars().take(80).collect::()); + // TODO: return multiple targets — for now just return first tool call + return Some((PaneTarget::Tools, line, Marker::None)); + } + } + if text.is_empty() { return None; } + Some((PaneTarget::ConversationAssistant, text, Marker::Assistant)) + } + Role::Tool => { + if text.is_empty() { return None; } + Some((PaneTarget::ToolResult, text, Marker::None)) + } + Role::System => None, + } +} + pub(crate) struct InteractScreen { pub(crate) autonomous: PaneState, pub(crate) conversation: PaneState, @@ -61,69 +108,38 @@ impl InteractScreen { /// Sync conversation display from agent entries. fn sync_from_agent(&mut self) { - use crate::agent::api::types::Role; - 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 + // Phase 1: detect desync — pop entries from panes to match + if gen != self.last_generation || count < self.last_entry_count { + // Compaction or mutation happened — full reset + // (could be smarter and pop from front, but reset is safe) self.conversation = PaneState::new(true); self.autonomous = PaneState::new(true); self.tools = PaneState::new(false); self.last_entry_count = 0; } - // Render new entries + // Phase 2: route new entries to panes for entry in agent.entries().iter().skip(self.last_entry_count) { - let msg = entry.message(); - let text = msg.content_text(); - - // Skip memory/system-reminder entries — they're context, not conversation - if let crate::agent::context::ConversationEntry::Memory { .. } = entry { - continue; - } - - match msg.role { - Role::User => { - // Skip system-reminder injections - if text.starts_with("") { - continue; - } - self.conversation.push_line_with_marker( - text.to_string(), Color::Cyan, Marker::User, - ); + match route_entry(entry) { + Some((PaneTarget::Conversation, text, marker)) => { + self.conversation.push_line_with_marker(text, Color::Cyan, marker); } - Role::Assistant => { - // Tool calls show in tools pane - if let Some(ref calls) = msg.tool_calls { - for call in calls { - let line = format!("[{}] {}", call.function.name, - call.function.arguments.chars().take(80).collect::()); - self.tools.push_line(line, Color::Yellow); - } - } - // Text content to conversation - if !text.is_empty() { - self.conversation.push_line_with_marker( - text.to_string(), Color::Reset, Marker::Assistant, - ); - } + Some((PaneTarget::ConversationAssistant, text, marker)) => { + self.conversation.push_line_with_marker(text, Color::Reset, marker); } - Role::Tool => { - // Tool results to tools pane + Some((PaneTarget::Tools, text, _)) => { + self.tools.push_line(text, Color::Yellow); + } + Some((PaneTarget::ToolResult, text, _)) => { for line in text.lines().take(20) { self.tools.push_line(format!(" {}", line), Color::DarkGray); } - if text.lines().count() > 20 { - self.tools.push_line( - format!(" ... ({} more lines)", text.lines().count() - 20), - Color::DarkGray, - ); - } } - Role::System => {} // skip + None => {} // skip (memory, system, system-reminder) } }