diff --git a/AGENTS.md b/AGENTS.md index 66614ede650..740a5e505a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,7 +89,10 @@ Examples: - PR readiness is not merge intent. Use the shared `babysit-pr`/GitHub skills and `.github/github.json` PR workflow metadata when watching fix trains, auto-review lag, CI, review comments, and merge readiness. -- Use `just local-code-rebuild` to rebuild the current branch into the PATH-resolved binary. +- Before restarting or dogfooding the Every Code harness, run `just local-code-rebuild` + so the PATH-resolved `code` command uses the current branch's binary. +- Use `just local-code-rebuild` whenever you need to rebuild the current branch into + the PATH-resolved binary. - After `./build-fast.sh`, run `just local-code-rebuild` again before release smoke checks; the fast build validates dev-fast artifacts, while the rebuild recipe owns the PATH-resolved release binary and embeds the `VERSION` file value. - During active local work, run `just local-cleanup-space --apply --keep-current-fast-cache` when repo-local diff --git a/code-rs/tui/src/chatwidget.rs b/code-rs/tui/src/chatwidget.rs index 07fa26cf523..462c9bd0463 100644 --- a/code-rs/tui/src/chatwidget.rs +++ b/code-rs/tui/src/chatwidget.rs @@ -5175,12 +5175,20 @@ impl ChatWidget<'_> { fn history_has_assistant_text(&self, text: &str) -> bool { let normalized = Self::normalize_text(text); + let normalized_preview = Self::normalize_text(&Self::markdown_to_plain_preview(text)); self.history_cells.iter().any(|cell| { if let Some(existing) = cell .as_any() .downcast_ref::() { - return Self::normalize_text(existing.markdown()) == normalized; + let existing_normalized = Self::normalize_text(existing.markdown()); + if existing_normalized == normalized { + return true; + } + let existing_preview = Self::normalize_text(&Self::markdown_to_plain_preview( + existing.markdown(), + )); + return existing_preview == normalized_preview; } if let Some(existing) = cell .as_any() @@ -5188,7 +5196,8 @@ impl ChatWidget<'_> { { if existing.state().kind == PlainMessageKind::Assistant { let existing_text = Self::message_lines_to_plain_preview(&existing.state().lines); - return Self::normalize_text(&existing_text) == normalized; + let existing_normalized = Self::normalize_text(&existing_text); + return existing_normalized == normalized || existing_normalized == normalized_preview; } } false @@ -13548,6 +13557,9 @@ impl ChatWidget<'_> { if trimmed.is_empty() { continue; } + if trimmed.starts_with("```") { + continue; + } if trimmed.starts_with('#') { segments.push(trimmed.trim_start_matches('#').trim().to_string()); } else { diff --git a/code-rs/tui/tests/resume_replay.rs b/code-rs/tui/tests/resume_replay.rs index 35b10cb525d..ae4beaa8923 100644 --- a/code-rs/tui/tests/resume_replay.rs +++ b/code-rs/tui/tests/resume_replay.rs @@ -2,6 +2,7 @@ #![allow(clippy::unwrap_used, clippy::expect_used)] use code_core::history::{ + AssistantMessageState, ExploreEntry, ExploreEntryStatus, ExploreRecord, ExploreSummary, HistoryId, HistoryRecord, HistorySnapshot, InlineSpan, OrderKeySnapshot, PlanIcon, PlanProgress, PlanStep, PlanUpdateState, ReasoningBlock, ReasoningSection, ReasoningState, TextEmphasis, TextTone, @@ -13,6 +14,7 @@ use code_protocol::models::{ }; use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness}; use serde_json::to_value; +use std::time::SystemTime; fn assistant_cell_count(screen: &str) -> usize { screen @@ -149,6 +151,27 @@ fn tool_only_snapshot() -> HistorySnapshot { } } +fn assistant_snapshot(id: u64, markdown: &str) -> HistorySnapshot { + HistorySnapshot { + records: vec![HistoryRecord::AssistantMessage(AssistantMessageState { + id: HistoryId(id), + stream_id: None, + markdown: markdown.to_string(), + citations: Vec::new(), + metadata: None, + token_usage: None, + mid_turn: false, + created_at: SystemTime::UNIX_EPOCH, + })], + next_id: id.saturating_add(1), + exec_call_lookup: Default::default(), + tool_call_lookup: Default::default(), + stream_lookup: Default::default(), + order: vec![OrderKeySnapshot { req: 1, out: 0, seq: 1 }], + order_debug: Vec::new(), + } +} + #[test] fn replay_history_duplicates_short_assistant_messages() { let mut harness = ChatWidgetHarness::new(); @@ -310,3 +333,30 @@ fn replay_history_restores_final_assistant_after_snapshot_tail() { "screen: {screen}" ); } + +#[test] +fn replay_history_deduplicates_snapshot_assistant_with_fenced_replay_markdown() { + let mut harness = ChatWidgetHarness::new(); + + let answer = "Yes, I'd restart now.\n\nBefore restart/dogfood, the important step is:\n\n```sh\njust local-code-rebuild\n```\n\nThat updates the PATH-resolved code binary."; + let snapshot_answer = "Yes, I'd restart now.\n\nBefore restart/dogfood, the important step is:\n\njust local-code-rebuild\n\nThat updates the PATH-resolved code binary."; + let snapshot_json = to_value(&assistant_snapshot(1, snapshot_answer)).expect("snapshot to json"); + + harness.handle_event(Event { + id: "resume-replay".to_string(), + event_seq: 0, + msg: EventMsg::ReplayHistory(ReplayHistoryEvent { + items: vec![message("assistant", answer)], + history_snapshot: Some(snapshot_json), + }), + order: None, + }); + + let screen = render_chat_widget_to_vt100(&mut harness, 80, 20); + + assert_eq!( + 1, + screen.matches("Yes, I'd restart now.").count(), + "expected one restored assistant answer. screen: {screen}" + ); +}