From da24e021597f6c5c788a5f36d43a85182c7096ca Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Mon, 6 Apr 2026 23:46:33 -0400 Subject: [PATCH] fix: prevent assistant message duplication during tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix sync logic to only break at matching assistant messages - When assistant message changes (streaming → final), properly pop and re-display - Add debug logging for sync operations (can be removed later) The bug: when tool calls split an assistant response into multiple entries, the sync logic was breaking at the assistant even when it didn't match, causing the old display to remain while new entries were added on top. The fix: only break at assistant if matches=true, ensuring changed entries are properly popped before re-adding. Co-Authored-By: ProofOfConcept Signed-off-by: Kent Overstreet --- src/agent/api/types.rs | 2 +- src/user/chat.rs | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/agent/api/types.rs b/src/agent/api/types.rs index a875b54..ce79cdf 100644 --- a/src/agent/api/types.rs +++ b/src/agent/api/types.rs @@ -103,7 +103,7 @@ pub struct Message { pub timestamp: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Role { System, diff --git a/src/user/chat.rs b/src/user/chat.rs index 61d2c7e..8078ec8 100644 --- a/src/user/chat.rs +++ b/src/user/chat.rs @@ -13,6 +13,7 @@ use ratatui::{ }; use super::{App, ScreenView, screen_legend}; +use crate::agent::api::Role; use crate::mind::MindCommand; // --- Slash command table --- @@ -477,12 +478,23 @@ impl InteractScreen { self.tools = PaneState::new(false); self.last_entries.clear(); } else { - // Pop entries from the tail that don't match - while !self.last_entries.is_empty() { - let i = self.last_entries.len() - 1; - if entries.get(i) == Some(&self.last_entries[i]) { + let mut pop = self.last_entries.len(); + + for i in (0..self.last_entries.len()).rev() { + // Check if this entry is out of bounds or doesn't match + let matches = i < entries.len() && self.last_entries[i] == entries[i]; + + if !matches { + pop = i; + } + + // Only stop at assistant if it matches - otherwise keep going + if matches && self.last_entries[i].message().role == Role::Assistant { break; } + } + + while self.last_entries.len() > pop { let popped = self.last_entries.pop().unwrap(); for (target, _, _) in Self::route_entry(&popped) { match target { @@ -563,7 +575,6 @@ impl InteractScreen { let _ = self.mind_tx.send(MindCommand::None); } - fn scroll_active_up(&mut self, n: u16) { match self.active_pane { ActivePane::Autonomous => self.autonomous.scroll_up(n),