From 65d23692fb9328b1666d8d707de2269ecced9aa1 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 5 Apr 2026 19:43:48 -0400 Subject: [PATCH] chat: route_entry returns Vec for multi-tool-call entries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An assistant entry can have text + multiple tool calls. route_entry now returns Vec<(PaneTarget, String, Marker)> — tool calls go to tools pane, text goes to conversation, all from the same entry. Pop phase iterates the vec in reverse to pop correct number of pane items per entry. Co-Authored-By: Kent Overstreet --- src/user/chat.rs | 56 +++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/user/chat.rs b/src/user/chat.rs index 7800be6..247a8d7 100644 --- a/src/user/chat.rs +++ b/src/user/chat.rs @@ -66,39 +66,50 @@ impl InteractScreen { } } - /// Route an agent entry to the appropriate pane. - /// Returns None for entries that shouldn't be displayed (memory, system). - fn route_entry(&mut self, entry: &crate::agent::context::ConversationEntry) -> Option<&mut PaneState> { + /// Route an agent entry to pane items. + /// Returns empty vec for entries that shouldn't be displayed. + fn route_entry(entry: &crate::agent::context::ConversationEntry) -> Vec<(PaneTarget, String, Marker)> { use crate::agent::api::types::Role; use crate::agent::context::ConversationEntry; if let ConversationEntry::Memory { .. } = entry { - return None; + return vec![]; } let msg = entry.message(); let text = msg.content_text().to_string(); - match msg.role { - if text.is_empty() { return None; } - if text.starts_with("") { return None; } + if text.starts_with("") { + return vec![]; + } - Role::User => Some(&mut self.conversation), + match msg.role { + Role::User => { + if text.is_empty() { return vec![]; } + vec![(PaneTarget::Conversation, text, Marker::User)] + } Role::Assistant => { + let mut items = Vec::new(); // 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)); + items.push((PaneTarget::Tools, line, Marker::None)); } } - Some((PaneTarget::ConversationAssistant, text, Marker::Assistant)) + // Text content → conversation + if !text.is_empty() { + items.push((PaneTarget::ConversationAssistant, text, Marker::Assistant)); + } + items } - Role::Tool => Some(&mut self.tools), - Role::System => None, + Role::Tool => { + if text.is_empty() { return vec![]; } + vec![(PaneTarget::ToolResult, text, Marker::None)] + } + Role::System => vec![], } } @@ -122,7 +133,7 @@ impl InteractScreen { break; } let popped = self.last_entries.pop().unwrap(); - if let Some((target, _, _)) = Self::route_entry(&popped) { + for (target, _, _) in Self::route_entry(&popped) { match target { PaneTarget::Conversation | PaneTarget::ConversationAssistant => self.conversation.pop_line(), @@ -136,17 +147,14 @@ impl InteractScreen { // Phase 2: push new entries let start = self.last_entries.len(); for entry in entries.iter().skip(start) { - if let Some((target, text, marker)) = Self::route_entry(entry) { + for (target, text, marker) in Self::route_entry(entry) { match target { - PaneTarget::Conversation => { - self.conversation.push_line_with_marker(text, Color::Cyan, marker); - } - PaneTarget::ConversationAssistant => { - self.conversation.push_line_with_marker(text, Color::Reset, marker); - } - PaneTarget::Tools => { - self.tools.push_line(text, Color::Yellow); - } + PaneTarget::Conversation => + self.conversation.push_line_with_marker(text, Color::Cyan, marker), + PaneTarget::ConversationAssistant => + self.conversation.push_line_with_marker(text, Color::Reset, marker), + PaneTarget::Tools => + self.tools.push_line(text, Color::Yellow), PaneTarget::ToolResult => { for line in text.lines().take(20) { self.tools.push_line(format!(" {}", line), Color::DarkGray);