fix: prevent assistant message duplication during tool calls

- 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 <poc@bcachefs.org>
Signed-off-by: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
Kent Overstreet 2026-04-06 23:46:33 -04:00
parent 98a1ae74d7
commit da24e02159
2 changed files with 17 additions and 6 deletions

View file

@ -103,7 +103,7 @@ pub struct Message {
pub timestamp: Option<String>, pub timestamp: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Role { pub enum Role {
System, System,

View file

@ -13,6 +13,7 @@ use ratatui::{
}; };
use super::{App, ScreenView, screen_legend}; use super::{App, ScreenView, screen_legend};
use crate::agent::api::Role;
use crate::mind::MindCommand; use crate::mind::MindCommand;
// --- Slash command table --- // --- Slash command table ---
@ -477,12 +478,23 @@ impl InteractScreen {
self.tools = PaneState::new(false); self.tools = PaneState::new(false);
self.last_entries.clear(); self.last_entries.clear();
} else { } else {
// Pop entries from the tail that don't match let mut pop = self.last_entries.len();
while !self.last_entries.is_empty() {
let i = self.last_entries.len() - 1; for i in (0..self.last_entries.len()).rev() {
if entries.get(i) == Some(&self.last_entries[i]) { // 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; break;
} }
}
while self.last_entries.len() > pop {
let popped = self.last_entries.pop().unwrap(); let popped = self.last_entries.pop().unwrap();
for (target, _, _) in Self::route_entry(&popped) { for (target, _, _) in Self::route_entry(&popped) {
match target { match target {
@ -563,7 +575,6 @@ impl InteractScreen {
let _ = self.mind_tx.send(MindCommand::None); let _ = self.mind_tx.send(MindCommand::None);
} }
fn scroll_active_up(&mut self, n: u16) { fn scroll_active_up(&mut self, n: u16) {
match self.active_pane { match self.active_pane {
ActivePane::Autonomous => self.autonomous.scroll_up(n), ActivePane::Autonomous => self.autonomous.scroll_up(n),