Merge branch 'code-branch-20251007-104544' into main

This commit is contained in:
James Peter
2025-10-07 21:51:56 +10:00
3 changed files with 248 additions and 43 deletions
+72 -2
View File
@@ -19002,7 +19002,7 @@ fi\n\
mod tests {
use super::*;
use crate::chatwidget::smoke_helpers::ChatWidgetHarness;
use crate::history_cell::HistoryCellType;
use crate::history_cell::{self, ExploreAggregationCell, HistoryCellType};
use code_core::history::state::{
AssistantStreamDelta,
AssistantStreamState,
@@ -19020,8 +19020,15 @@ mod tests {
TextEmphasis,
TextTone,
};
use code_core::parse_command::ParsedCommand;
use code_core::protocol::OrderMeta;
use code_core::protocol::{AgentMessageEvent, AgentStatusUpdateEvent, Event, EventMsg};
use code_core::protocol::{
AgentMessageEvent,
AgentStatusUpdateEvent,
Event,
EventMsg,
ExecCommandBeginEvent,
};
use code_core::protocol::AgentInfo as CoreAgentInfo;
use ratatui::backend::TestBackend;
use ratatui::text::Line;
@@ -19086,6 +19093,69 @@ mod tests {
let _ = chat.history_insert_plain_state_with_key(state, key, "test");
}
#[test]
fn finalize_explore_updates_even_with_stale_index() {
let mut harness = ChatWidgetHarness::new();
let chat = harness.chat();
reset_history(chat);
let call_id = "call-explore".to_string();
let order = OrderMeta {
request_ordinal: 1,
output_index: Some(0),
sequence_number: Some(0),
};
chat.handle_code_event(Event {
id: call_id.clone(),
event_seq: 0,
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: call_id.clone(),
command: vec!["bash".into(), "-lc".into(), "cat foo.txt".into()],
cwd: std::env::temp_dir(),
parsed_cmd: vec![ParsedCommand::Read {
cmd: "cat foo.txt".to_string(),
name: "foo.txt".to_string(),
}],
}),
order: Some(order.clone()),
});
let exec_call_id = ExecCallId(call_id.clone());
let running = chat
.exec
.running_commands
.get_mut(&exec_call_id)
.expect("explore command should be tracked");
let (agg_idx, entry_idx) = running
.explore_entry
.expect("read command should register an explore entry");
// Simulate an out-of-date index so finalize must recover by searching.
running.explore_entry = Some((usize::MAX, entry_idx));
chat.exec.running_explore_agg_index = Some(usize::MAX);
chat.finalize_all_running_due_to_answer();
let cell = chat.history_cells[agg_idx]
.as_any()
.downcast_ref::<ExploreAggregationCell>()
.expect("explore aggregation cell should remain present");
let entry = cell
.record()
.entries
.get(entry_idx)
.expect("entry index should still be valid");
assert!(
matches!(entry.status, history_cell::ExploreEntryStatus::Success),
"explore entry should be marked successful instead of remaining running"
);
assert!(
!chat.exec.running_commands.contains_key(&exec_call_id),
"finalization should clear the running command"
);
}
#[test]
fn ordering_keeps_new_answers_after_prior_backgrounds() {
let mut harness = ChatWidgetHarness::new();
+90 -41
View File
@@ -56,6 +56,74 @@ fn find_trailing_explore_agg(chat: &ChatWidget<'_>) -> Option<usize> {
None
}
fn update_explore_entry_status(
chat: &mut ChatWidget<'_>,
preferred_index: Option<usize>,
entry_idx: usize,
status: history_cell::ExploreEntryStatus,
) -> Option<usize> {
fn try_update_at(
chat: &mut ChatWidget<'_>,
idx: usize,
entry_idx: usize,
status: &history_cell::ExploreEntryStatus,
) -> Option<usize> {
if idx >= chat.history_cells.len() {
return None;
}
let cell = chat.history_cells[idx]
.as_any()
.downcast_ref::<history_cell::ExploreAggregationCell>()?;
if entry_idx >= cell.record().entries.len() {
return None;
}
let mut record = cell.record().clone();
history_cell::explore_record_update_status(&mut record, entry_idx, status.clone());
let replacement = history_cell::ExploreAggregationCell::from_record(record.clone());
chat.history_replace_with_record(
idx,
Box::new(replacement),
HistoryDomainRecord::Explore(record),
);
chat.autoscroll_if_near_bottom();
Some(idx)
}
if let Some(idx) = preferred_index.and_then(|i| try_update_at(chat, i, entry_idx, &status)) {
return Some(idx);
}
if let Some(idx) = chat
.exec
.running_explore_agg_index
.and_then(|i| try_update_at(chat, i, entry_idx, &status))
{
return Some(idx);
}
for i in (0..chat.history_cells.len()).rev() {
let Some(cell) = chat.history_cells[i]
.as_any()
.downcast_ref::<history_cell::ExploreAggregationCell>()
else {
continue;
};
if entry_idx >= cell.record().entries.len() {
continue;
}
if !matches!(
cell.record().entries[entry_idx].status,
history_cell::ExploreEntryStatus::Running
| history_cell::ExploreEntryStatus::Error { .. }
| history_cell::ExploreEntryStatus::NotFound
) {
continue;
}
if let Some(idx) = try_update_at(chat, i, entry_idx, &status) {
return Some(idx);
}
}
None
}
fn exec_record_from_begin(ev: &ExecCommandBeginEvent) -> ExecRecord {
let action = history_cell::action_enum_from_parsed(&ev.parsed_cmd);
ExecRecord {
@@ -344,27 +412,14 @@ pub(super) fn finalize_all_running_due_to_answer(chat: &mut ChatWidget<'_>) {
}
if let Some((agg_idx, entry_idx)) = explore_entry {
if *agg_idx < chat.history_cells.len() {
if let Some(existing) = chat.history_cells[*agg_idx]
.as_any()
.downcast_ref::<history_cell::ExploreAggregationCell>()
{
let mut record = existing.record().clone();
history_cell::explore_record_update_status(
&mut record,
*entry_idx,
history_cell::ExploreEntryStatus::Success,
);
let cell = history_cell::ExploreAggregationCell::from_record(record.clone());
chat.history_replace_with_record(
*agg_idx,
Box::new(cell),
HistoryDomainRecord::Explore(record),
);
chat.autoscroll_if_near_bottom();
agg_was_updated = true;
}
}
let updated = update_explore_entry_status(
chat,
Some(*agg_idx),
*entry_idx,
history_cell::ExploreEntryStatus::Success,
)
.is_some();
agg_was_updated |= updated;
}
remove_after_finalize.push(call_id.clone());
@@ -748,14 +803,19 @@ pub(super) fn handle_exec_end_now(
order: &OrderMeta,
) {
let call_id = super::ExecCallId(ev.call_id.clone());
if chat.exec.should_suppress_exec_end(&call_id) {
let suppressing = chat.exec.should_suppress_exec_end(&call_id);
if suppressing {
chat.exec.unsuppress_exec_end(&call_id);
chat.ended_call_ids.insert(call_id);
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
return;
if !chat.exec.running_commands.contains_key(&call_id) {
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
chat.maybe_hide_spinner();
chat.refresh_auto_drive_visuals();
return;
}
}
chat.ended_call_ids.insert(super::ExecCallId(ev.call_id.clone()));
chat
.ended_call_ids
.insert(super::ExecCallId(ev.call_id.clone()));
// If this call was already marked as cancelled, drop the End to avoid
// inserting a duplicate completed cell after the user interrupt.
if chat
@@ -836,20 +896,7 @@ pub(super) fn handle_exec_end_now(
exit_code: Some(exit_code),
},
};
if let Some(mut record) = chat.history_cells.get(agg_idx).and_then(|cell| {
cell.as_any()
.downcast_ref::<history_cell::ExploreAggregationCell>()
.map(|existing| existing.record().clone())
}) {
history_cell::explore_record_update_status(&mut record, entry_idx, status.clone());
let cell = history_cell::ExploreAggregationCell::from_record(record.clone());
chat.history_replace_with_record(
agg_idx,
Box::new(cell),
HistoryDomainRecord::Explore(record),
);
chat.autoscroll_if_near_bottom();
}
let updated_index = update_explore_entry_status(chat, Some(agg_idx), entry_idx, status.clone());
if !chat
.exec
.running_commands
@@ -857,6 +904,8 @@ pub(super) fn handle_exec_end_now(
.any(|rc| rc.explore_entry.is_some())
{
chat.exec.running_explore_agg_index = None;
} else if let Some(actual_idx) = updated_index {
chat.exec.running_explore_agg_index = Some(actual_idx);
}
let status_text = match status {
history_cell::ExploreEntryStatus::Success => match action {
+86
View File
@@ -0,0 +1,86 @@
use code_core::protocol::{
Event,
EventMsg,
ExecCommandBeginEvent,
ExecCommandEndEvent,
OrderMeta,
PatchApplyBeginEvent,
PatchApplyEndEvent,
};
use code_tui::test_helpers::{render_chat_widget_to_vt100, ChatWidgetHarness};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;
fn next_order_meta(request_ordinal: u64, seq: &mut u64) -> OrderMeta {
let order = OrderMeta {
request_ordinal,
output_index: Some(0),
sequence_number: Some(*seq),
};
*seq += 1;
order
}
#[test]
fn exec_cell_clears_after_patch_flow() {
let mut harness = ChatWidgetHarness::new();
let mut seq = 0_u64;
let call_id = "call_bug";
let cwd = PathBuf::from("/tmp");
harness.handle_event(Event {
id: "exec-begin".to_string(),
event_seq: 0,
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
call_id: call_id.to_string(),
command: vec!["bash".into(), "-lc".into(), "apply_patch".into()],
cwd: cwd.clone(),
parsed_cmd: Vec::new(),
}),
order: Some(next_order_meta(1, &mut seq)),
});
harness.handle_event(Event {
id: "patch-begin".to_string(),
event_seq: 0,
msg: EventMsg::PatchApplyBegin(PatchApplyBeginEvent {
call_id: call_id.to_string(),
auto_approved: true,
changes: HashMap::new(),
}),
order: Some(next_order_meta(1, &mut seq)),
});
harness.handle_event(Event {
id: "exec-end".to_string(),
event_seq: 1,
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
call_id: call_id.to_string(),
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
duration: Duration::from_millis(50),
}),
order: Some(next_order_meta(1, &mut seq)),
});
harness.handle_event(Event {
id: "patch-end".to_string(),
event_seq: 1,
msg: EventMsg::PatchApplyEnd(PatchApplyEndEvent {
call_id: call_id.to_string(),
stdout: "Success".into(),
stderr: String::new(),
success: true,
}),
order: Some(next_order_meta(1, &mut seq)),
});
let output = render_chat_widget_to_vt100(&mut harness, 80, 12);
assert!(
!output.contains("Running"),
"exec cell should not remain running after patch apply:\n{}",
output
);
}