mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
fix(tui/resume): avoid duplicate assistant cells on replay
This commit is contained in:
@@ -15798,40 +15798,37 @@ fi\n\
|
||||
.is_some()
|
||||
}) {
|
||||
// Replace the tail finalized assistant cell if the new content is identical OR
|
||||
// a superset revision of the previous content (common provider behavior where
|
||||
// a later final slightly extends the earlier one). Otherwise append a new
|
||||
// assistant message so distinct messages remain separate.
|
||||
let (should_replace, _prev_len, _new_len) = self.history_cells[idx]
|
||||
// a small revision that merely adds leading/trailing context. Otherwise append a
|
||||
// new assistant message so distinct replies remain separate.
|
||||
let should_replace = self.history_cells[idx]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::AssistantMarkdownCell>()
|
||||
.map(|amc| {
|
||||
let prev = Self::normalize_text(amc.markdown());
|
||||
let newn = Self::normalize_text(&source);
|
||||
let identical = prev == newn;
|
||||
let is_superset = !identical && newn.contains(&prev);
|
||||
// Heuristic: treat as revision when previous is reasonably long to
|
||||
// avoid collapsing very short replies unintentionally.
|
||||
let long_enough = prev.len() >= 80;
|
||||
(
|
||||
identical || (is_superset && long_enough),
|
||||
prev.len(),
|
||||
newn.len(),
|
||||
)
|
||||
if identical || prev.is_empty() {
|
||||
return identical;
|
||||
}
|
||||
let is_prefix_expansion = newn.starts_with(&prev);
|
||||
let is_suffix_expansion = newn.ends_with(&prev);
|
||||
let is_large_superset = prev.len() >= 80 && newn.contains(&prev);
|
||||
identical || is_prefix_expansion || is_suffix_expansion || is_large_superset
|
||||
})
|
||||
.unwrap_or((false, 0, 0));
|
||||
.unwrap_or(false);
|
||||
if should_replace {
|
||||
tracing::debug!(
|
||||
"final-answer: replacing tail AssistantMarkdownCell via heuristic identical/superset"
|
||||
"final-answer: replacing tail AssistantMarkdownCell via heuristic identical/expansion"
|
||||
);
|
||||
let state =
|
||||
self.finalize_answer_stream_state(id.as_deref(), &final_source);
|
||||
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
|
||||
self.history_replace_at(idx, Box::new(cell));
|
||||
self.autoscroll_if_near_bottom();
|
||||
// Final assistant content revised; advance Auto Drive now.
|
||||
self.auto_on_assistant_final();
|
||||
return;
|
||||
}
|
||||
let state =
|
||||
self.finalize_answer_stream_state(id.as_deref(), &final_source);
|
||||
let cell = history_cell::AssistantMarkdownCell::from_state(state, &self.config);
|
||||
self.history_replace_at(idx, Box::new(cell));
|
||||
self.autoscroll_if_near_bottom();
|
||||
// Final assistant content revised; advance Auto Drive now.
|
||||
self.auto_on_assistant_final();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: no prior assistant cell found; insert at stable sequence position.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#![cfg(test)]
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use code_core::protocol::{Event, EventMsg, ReplayHistoryEvent};
|
||||
use code_protocol::models::{ContentItem, ResponseItem};
|
||||
use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness};
|
||||
|
||||
fn assistant_cell_count(screen: &str) -> usize {
|
||||
screen
|
||||
.lines()
|
||||
.filter(|line| line.trim_start().starts_with("• "))
|
||||
.count()
|
||||
}
|
||||
|
||||
fn message(role: &str, text: &str) -> ResponseItem {
|
||||
let content = match role {
|
||||
"assistant" => ContentItem::OutputText { text: text.to_string() },
|
||||
_ => ContentItem::InputText { text: text.to_string() },
|
||||
};
|
||||
|
||||
ResponseItem::Message {
|
||||
id: None,
|
||||
role: role.to_string(),
|
||||
content: vec![content],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_history_duplicates_short_assistant_messages() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
|
||||
let items = vec![
|
||||
message("user", "Please summarize the plan."),
|
||||
message("assistant", "Working."),
|
||||
message("assistant", "Working. Done."),
|
||||
];
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "resume-replay".to_string(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::ReplayHistory(ReplayHistoryEvent {
|
||||
items,
|
||||
history_snapshot: None,
|
||||
}),
|
||||
order: None,
|
||||
});
|
||||
|
||||
let screen = render_chat_widget_to_vt100(&mut harness, 80, 24);
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
assistant_cell_count(&screen),
|
||||
"expected a single restored assistant message but saw: {screen}"
|
||||
);
|
||||
assert!(screen.contains("Working. Done."));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replay_history_handles_prefixed_revisions() {
|
||||
let mut harness = ChatWidgetHarness::new();
|
||||
|
||||
let items = vec![
|
||||
message("user", "Please summarize the plan."),
|
||||
message("assistant", "Working."),
|
||||
message("assistant", "Update:\nWorking."),
|
||||
];
|
||||
|
||||
harness.handle_event(Event {
|
||||
id: "resume-replay".to_string(),
|
||||
event_seq: 0,
|
||||
msg: EventMsg::ReplayHistory(ReplayHistoryEvent {
|
||||
items,
|
||||
history_snapshot: None,
|
||||
}),
|
||||
order: None,
|
||||
});
|
||||
|
||||
let screen = render_chat_widget_to_vt100(&mut harness, 80, 24);
|
||||
|
||||
assert_eq!(
|
||||
1,
|
||||
assistant_cell_count(&screen),
|
||||
"expected a single restored assistant message but saw: {screen}"
|
||||
);
|
||||
assert!(screen.contains("Update:"));
|
||||
}
|
||||
Reference in New Issue
Block a user