Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions code-rs/tui/src/chatwidget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5175,20 +5175,29 @@ 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::<crate::history_cell::AssistantMarkdownCell>()
{
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()
.downcast_ref::<crate::history_cell::PlainHistoryCell>()
{
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
Expand Down Expand Up @@ -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 {
Expand Down
50 changes: 50 additions & 0 deletions code-rs/tui/tests/resume_replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}"
);
}