mirror of
https://github.com/openai/codex.git
synced 2026-10-18 17:18:52 +00:00
Remove the done prefix from TUI completion timestamps (#46067)
## What changed Render completion footers as `2:32 PM` or `Worked for 2m 5s · 2:32 PM`, with the existing date formatting for older completions. Update test helpers to identify completion footer cells by type when normalizing or excluding timestamps, preserving message text that resembles a completion footer. ## Testing Update completion label and transcript snapshots, and add assertions that timestamp normalization preserves ordinary message text while normalizing completion footer cells. GitOrigin-RevId: 35f1658db02f8527212c006fd7492b574a105304
This commit is contained in:
@@ -923,7 +923,8 @@ goals = true
|
||||
.as_any()
|
||||
.is::<crate::history_cell::FinalMessageSeparator>()
|
||||
{
|
||||
replayed_history.push_str(&normalize_completion_timestamps(rendered));
|
||||
replayed_history
|
||||
.push_str(&normalize_completion_timestamps(cell.as_ref(), rendered));
|
||||
} else {
|
||||
replayed_history.push_str(&rendered);
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ expression: "snapshots.join(\"\\n\\n--- next overlapping page ---\\n\\n\")"
|
||||
Earlier messages are available — press ctrl + t to view the full transcript
|
||||
• Middle answer
|
||||
|
||||
Worked for 2m 5s · done Sep 6, 2000 at 2:33 PM
|
||||
Worked for 2m 5s · Sep 6, 2000 at 2:33 PM
|
||||
|
||||
--- next overlapping page ---
|
||||
|
||||
@@ -15,7 +15,7 @@ Earlier messages are available — press ctrl + t to view the full transcript
|
||||
|
||||
• Oldest answer
|
||||
|
||||
Worked for 2m 5s · done Sep 6, 2000 at 2:32 PM
|
||||
Worked for 2m 5s · Sep 6, 2000 at 2:32 PM
|
||||
|
||||
|
||||
› Middle prompt
|
||||
@@ -23,4 +23,4 @@ Earlier messages are available — press ctrl + t to view the full transcript
|
||||
|
||||
• Middle answer
|
||||
|
||||
Worked for 2m 5s · done Sep 6, 2000 at 2:33 PM
|
||||
Worked for 2m 5s · Sep 6, 2000 at 2:33 PM
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: rendered_retry
|
||||
---
|
||||
› Handle the safety-buffered request
|
||||
Keep the accepted steer
|
||||
⚠ Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`.• done done [completion time]
|
||||
⚠ Full-history hydration is deprecated for paginated threads; omit `includeTurns` or set it to `false`, then page with `thread/turns/list` and `thread/items/list`.• done [completion time]
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::chatwidget::ChatWidget;
|
||||
use crate::chatwidget::ReplayKind;
|
||||
use crate::chatwidget::tests::make_chatwidget_manual_with_sender;
|
||||
use crate::chatwidget::tests::render_bottom_popup;
|
||||
use crate::history_cell::FinalMessageSeparator;
|
||||
use codex_app_server_protocol::AgentMessageDeltaNotification;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
@@ -138,19 +139,6 @@ fn finish_turn(
|
||||
);
|
||||
}
|
||||
|
||||
fn without_completion_metadata(rendered: &str) -> String {
|
||||
rendered
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let stable = line
|
||||
.split_once(" done ")
|
||||
.map_or(line, |(before, _)| before);
|
||||
(!stable.is_empty()).then_some(stable)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
#[path = "realtime_tests/caption_replay.rs"]
|
||||
mod caption_replay;
|
||||
#[path = "realtime_tests/handoff_privacy.rs"]
|
||||
|
||||
@@ -107,7 +107,9 @@ async fn typed_steering_restores_normal_output_for_an_existing_voice_item() {
|
||||
|
||||
let mut rendered = String::new();
|
||||
while let Ok(event) = events.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event
|
||||
&& !cell.as_any().is::<FinalMessageSeparator>()
|
||||
{
|
||||
for line in cell.display_lines(/*width*/ 80) {
|
||||
rendered.push_str(&line.to_string());
|
||||
}
|
||||
@@ -115,10 +117,7 @@ async fn typed_steering_restores_normal_output_for_an_existing_voice_item() {
|
||||
}
|
||||
assert!(rendered.contains("typed answer"), "{rendered}");
|
||||
assert!(!rendered.contains("private voice commentary"), "{rendered}");
|
||||
insta::assert_snapshot!(
|
||||
"typed_steering_keeps_voice_commentary_private",
|
||||
without_completion_metadata(&rendered)
|
||||
);
|
||||
insta::assert_snapshot!("typed_steering_keeps_voice_commentary_private", rendered);
|
||||
assert!(ops.try_recv().is_err());
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,9 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
|
||||
let mut rendered_history = Vec::new();
|
||||
commit_realtime_history_events(&mut chat, &mut events);
|
||||
while let Ok(event) = events.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event
|
||||
&& !cell.as_any().is::<FinalMessageSeparator>()
|
||||
{
|
||||
let rendered = cell
|
||||
.transcript_lines(/*width*/ 80)
|
||||
.iter()
|
||||
@@ -169,7 +171,7 @@ async fn delegation_started_before_peer_connection_keeps_its_voice_origin() {
|
||||
}
|
||||
insta::assert_snapshot!(
|
||||
"voice_delegation_during_connection",
|
||||
without_completion_metadata(&rendered_history.join("\n"))
|
||||
rendered_history.join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -322,21 +324,20 @@ async fn explicit_final_answer_can_explain_private_channel_markers() {
|
||||
commit_realtime_history_events(&mut chat, &mut events);
|
||||
let rendered = std::iter::from_fn(|| events.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => Some(
|
||||
cell.transcript_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
),
|
||||
AppEvent::InsertHistoryCell(cell) if !cell.as_any().is::<FinalMessageSeparator>() => {
|
||||
Some(
|
||||
cell.transcript_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
insta::assert_snapshot!(
|
||||
"explicit_final_answer_with_channel_marker",
|
||||
without_completion_metadata(&rendered)
|
||||
);
|
||||
insta::assert_snapshot!("explicit_final_answer_with_channel_marker", rendered);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -401,22 +401,21 @@ async fn answer_exceeding_speech_budget_is_shown_in_full_instead() {
|
||||
assert!(chat.realtime_conversation.pending_speech.is_empty());
|
||||
let rendered = std::iter::from_fn(|| events.try_recv().ok())
|
||||
.filter_map(|event| match event {
|
||||
AppEvent::InsertHistoryCell(cell) => Some(
|
||||
cell.transcript_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
),
|
||||
AppEvent::InsertHistoryCell(cell) if !cell.as_any().is::<FinalMessageSeparator>() => {
|
||||
Some(
|
||||
cell.transcript_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert_eq!(
|
||||
without_completion_metadata(&rendered)
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
rendered.split_whitespace().collect::<Vec<_>>().join(" "),
|
||||
format!("• {text}")
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
+1
-1
@@ -50,4 +50,4 @@ expression: "normalize_completion_timestamps(&\n(normalize_snapshot_paths(term.b
|
||||
"path": "C:\\Program Files\\App",
|
||||
"regex": "^foo.*(bar)?$"
|
||||
}
|
||||
done [completion time]
|
||||
[completion time]
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: normalize_completion_timestamps(& (text))
|
||||
---
|
||||
• The change is ready.
|
||||
|
||||
Worked for [duration] · done [completion time]
|
||||
Worked for [duration] · [completion time]
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalize_completion_timestamps(& (text))
|
||||
|
||||
• The change is ready.
|
||||
|
||||
Worked for [duration] · done [completion time]
|
||||
Worked for [duration] · [completion time]
|
||||
|
||||
+7
-7
@@ -2,12 +2,12 @@
|
||||
source: tui/src/chatwidget/tests/completion_styling_tests.rs
|
||||
expression: "&transcript"
|
||||
---
|
||||
› done Sep 6, 2000 at 2:32 PM
|
||||
• done Sep 6, 2000 at 2:32 PM
|
||||
└ done Sep 6, 2000 at 2:32 PM
|
||||
› Sep 6, 2000 at 2:32 PM
|
||||
• Sep 6, 2000 at 2:32 PM
|
||||
└ Sep 6, 2000 at 2:32 PM
|
||||
The job was done Sep 6, 2000 at 2:32 PM
|
||||
done Sep 6, 2000 at 2:32 PM is the expected text
|
||||
Worked for 2m 5s · done Sep 6, 2000 at 2:32 PM is quoted
|
||||
Sep 6, 2000 at 2:32 PM is the expected text
|
||||
Worked for 2m 5s · Sep 6, 2000 at 2:32 PM is quoted
|
||||
|
||||
done 3:24 PM
|
||||
Worked for 1h 2m 3s · done Sep 6, 2000 at 2:32 PM
|
||||
3:24 PM
|
||||
Worked for 1h 2m 3s · Sep 6, 2000 at 2:32 PM
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalize_completion_timestamps(& (combined))
|
||||
|
||||
• Final response.
|
||||
|
||||
Worked for [duration] · done [completion time]
|
||||
Worked for [duration] · [completion time]
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: "normalize_completion_timestamps(&\n(cells.iter().map(|cell|\nlines_to_single_string(cell).trim().to_string()).collect::<Vec<_>>().join(\"\\n\")))"
|
||||
---
|
||||
done [completion time]
|
||||
[completion time]
|
||||
• Saved conversation to conversation.md
|
||||
• Copied conversation to clipboard
|
||||
■ Copy failed: clipboard unavailable
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ expression: normalize_completion_timestamps(& (combined))
|
||||
|
||||
• Waited for background terminal · just fix
|
||||
|
||||
done [completion time]
|
||||
[completion time]
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ expression: normalize_completion_timestamps(& (combined))
|
||||
|
||||
• Final response.
|
||||
|
||||
done [completion time]
|
||||
[completion time]
|
||||
|
||||
+1
-1
@@ -6,4 +6,4 @@ expression: normalize_completion_timestamps(& (combined))
|
||||
|
||||
• Streaming response.
|
||||
|
||||
done [completion time]
|
||||
[completion time]
|
||||
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: normalize_completion_timestamps(& (combined))
|
||||
---
|
||||
• Waited for background terminal · just fix
|
||||
|
||||
done [completion time]
|
||||
[completion time]
|
||||
|
||||
@@ -826,11 +826,11 @@ async fn live_app_server_turn_completed_clears_working_status_after_answer_item(
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
|
||||
let completion_cells = drain_insert_history(&mut rx)
|
||||
let completion_cells = drain_insert_history_normalized(&mut rx)
|
||||
.iter()
|
||||
.map(|lines| normalize_completion_timestamps(lines_to_single_string(lines).trim()))
|
||||
.map(|lines| lines_to_single_string(lines).trim().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(completion_cells, vec!["done [completion time]"]);
|
||||
assert_eq!(completion_cells, vec!["[completion time]"]);
|
||||
assert!(!chat.bottom_pane.is_task_running());
|
||||
assert!(chat.bottom_pane.status_widget().is_none());
|
||||
assert_eq!(
|
||||
|
||||
@@ -56,7 +56,7 @@ fn saved_completion_label() -> String {
|
||||
Local
|
||||
.timestamp_opt(COMPLETED_AT, /*nsecs*/ 0)
|
||||
.unwrap()
|
||||
.format("done %b %-d, %Y at %-I:%M %p")
|
||||
.format("%b %-d, %Y at %-I:%M %p")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn completion_follows_plain_and_streamed_tool_answers() {
|
||||
}
|
||||
complete_turn(&mut chat, completed_turn(Some(125_000), Some(COMPLETED_AT)));
|
||||
|
||||
let text = drain_insert_history(&mut rx)
|
||||
let text = drain_insert_history_normalized(&mut rx)
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
@@ -83,7 +83,7 @@ async fn completion_follows_plain_and_streamed_tool_answers() {
|
||||
} else {
|
||||
"completion_after_plain_answer"
|
||||
},
|
||||
normalize_completion_timestamps(text),
|
||||
text,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -109,7 +109,7 @@ async fn completion_live_applies_duration_threshold_and_preserves_timestamp_fall
|
||||
let time = if completed_at.is_some() {
|
||||
saved_completion_label()
|
||||
} else {
|
||||
time.format("done %-I:%M %p").to_string()
|
||||
time.format("%-I:%M %p").to_string()
|
||||
};
|
||||
format!("{prefix}{time}")
|
||||
});
|
||||
@@ -192,7 +192,7 @@ async fn completion_replay_waits_for_older_turn_items_to_load() {
|
||||
let older_time = Local
|
||||
.timestamp_opt(older_completed_at, /*nsecs*/ 0)
|
||||
.unwrap()
|
||||
.format("done %b %-d, %Y at %-I:%M %p");
|
||||
.format("%b %-d, %Y at %-I:%M %p");
|
||||
assert_eq!(
|
||||
completion_labels(&mut rx),
|
||||
format!("Worked for 2m 5s · {older_time}"),
|
||||
@@ -228,14 +228,31 @@ async fn completion_failed_and_interrupted_turns_do_not_report_success() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_snapshot_normalization_is_explicit_and_line_scoped() {
|
||||
fn completion_snapshot_normalization_preserves_clock_only_message_lines() {
|
||||
let timestamp = "Sep 6, 2000 at 2:32 PM";
|
||||
let transcript = format!(
|
||||
"› done {timestamp}\n• done {timestamp}\n └ done {timestamp}\n The job was done {timestamp}\n done {timestamp} is the expected text\n Worked for 2m 5s · done {timestamp} is quoted\n\n done 3:24 PM\n Worked for 1h 2m 3s · done {timestamp}\n"
|
||||
"› {timestamp}\n• {timestamp}\n └ {timestamp}\n The job was done {timestamp}\n {timestamp} is the expected text\n Worked for 2m 5s · {timestamp} is quoted\n\n 3:24 PM\n Worked for 1h 2m 3s · {timestamp}\n"
|
||||
);
|
||||
let message = history_cell::PlainHistoryCell::new(
|
||||
transcript
|
||||
.lines()
|
||||
.map(|line| Line::from(line.to_owned()))
|
||||
.collect(),
|
||||
);
|
||||
assert_chatwidget_snapshot!("completion_like_transcript_text", &transcript);
|
||||
assert_eq!(
|
||||
normalize_completion_timestamps(&transcript),
|
||||
"› done Sep 6, 2000 at 2:32 PM\n• done Sep 6, 2000 at 2:32 PM\n └ done Sep 6, 2000 at 2:32 PM\n The job was done Sep 6, 2000 at 2:32 PM\n done Sep 6, 2000 at 2:32 PM is the expected text\n Worked for 2m 5s · done Sep 6, 2000 at 2:32 PM is quoted\n\n done [completion time]\n Worked for [duration] · done [completion time]\n"
|
||||
normalize_completion_timestamps(&message, &transcript),
|
||||
transcript
|
||||
);
|
||||
|
||||
let footer =
|
||||
history_cell::FinalMessageSeparator::new(Some(3_723), /*runtime_metrics*/ None)
|
||||
.with_completed_at(Local.timestamp_opt(COMPLETED_AT, /*nsecs*/ 0).unwrap());
|
||||
assert_eq!(
|
||||
normalize_completion_timestamps(
|
||||
&footer,
|
||||
lines_to_single_string(&footer.display_lines(/*width*/ 80))
|
||||
),
|
||||
" Worked for [duration] · [completion time]\n"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -888,15 +888,12 @@ async fn unified_exec_wait_after_final_agent_message_snapshot() {
|
||||
complete_assistant_message(&mut chat, "msg-1", "Final response.", /*phase*/ None);
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let cells = drain_insert_history_normalized(&mut rx);
|
||||
let combined = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!(
|
||||
"unified_exec_wait_after_final_agent_message",
|
||||
normalize_completion_timestamps(combined)
|
||||
);
|
||||
assert_chatwidget_snapshot!("unified_exec_wait_after_final_agent_message", combined);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -915,15 +912,12 @@ async fn unified_exec_wait_before_streamed_agent_message_snapshot() {
|
||||
handle_agent_message_delta(&mut chat, "Streaming response.");
|
||||
handle_turn_completed(&mut chat, "turn-wait-1", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let cells = drain_insert_history_normalized(&mut rx);
|
||||
let combined = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!(
|
||||
"unified_exec_wait_before_streamed_agent_message",
|
||||
normalize_completion_timestamps(combined)
|
||||
);
|
||||
assert_chatwidget_snapshot!("unified_exec_wait_before_streamed_agent_message", combined);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -954,19 +948,21 @@ async fn final_worked_for_uses_cumulative_turn_duration_snapshot() {
|
||||
);
|
||||
handle_turn_completed(&mut chat, "turn-1", duration_ms);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let cells = drain_insert_history_with(&mut rx, |cell| {
|
||||
let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80));
|
||||
if cell.as_any().is::<history_cell::FinalMessageSeparator>() {
|
||||
assert!(rendered.contains("Worked for 2m 5s"), "{rendered}");
|
||||
}
|
||||
normalize_completion_timestamps(cell, rendered)
|
||||
.lines()
|
||||
.map(|line| Line::from(line.to_owned()))
|
||||
.collect()
|
||||
});
|
||||
let combined = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert!(
|
||||
combined.contains("Worked for 2m 5s"),
|
||||
"expected final separator to use cumulative turn duration, got:\n{combined}"
|
||||
);
|
||||
assert_chatwidget_snapshot!(
|
||||
"final_worked_for_uses_cumulative_turn_duration",
|
||||
normalize_completion_timestamps(combined)
|
||||
);
|
||||
assert_chatwidget_snapshot!("final_worked_for_uses_cumulative_turn_duration", combined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1033,15 +1029,12 @@ async fn unified_exec_waiting_multiple_empty_snapshots() {
|
||||
|
||||
handle_turn_completed(&mut chat, "turn-wait-3", /*duration_ms*/ None);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let cells = drain_insert_history_normalized(&mut rx);
|
||||
let combined = cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
.collect::<String>();
|
||||
assert_chatwidget_snapshot!(
|
||||
"unified_exec_waiting_multiple_empty_after",
|
||||
normalize_completion_timestamps(combined)
|
||||
);
|
||||
assert_chatwidget_snapshot!("unified_exec_waiting_multiple_empty_after", combined);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1108,7 +1101,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() {
|
||||
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
|
||||
let post_cells = drain_insert_history(&mut rx);
|
||||
let post_cells = drain_insert_history_normalized(&mut rx);
|
||||
let mut combined = pre_cells
|
||||
.iter()
|
||||
.map(|lines| lines_to_single_string(lines))
|
||||
@@ -1121,10 +1114,7 @@ async fn unified_exec_non_empty_then_empty_snapshots() {
|
||||
combined.push('\n');
|
||||
}
|
||||
combined.push_str(&post);
|
||||
assert_chatwidget_snapshot!(
|
||||
"unified_exec_non_empty_then_empty_after",
|
||||
normalize_completion_timestamps(combined)
|
||||
);
|
||||
assert_chatwidget_snapshot!("unified_exec_non_empty_then_empty_after", combined);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -345,13 +345,29 @@ pub(super) fn drain_insert_history(
|
||||
drain_insert_history_with(rx, |cell| cell.display_lines(/*width*/ 80))
|
||||
}
|
||||
|
||||
pub(super) fn drain_insert_history_normalized(
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
) -> Vec<Vec<ratatui::text::Line<'static>>> {
|
||||
drain_insert_history_with(rx, |cell| {
|
||||
cell.display_lines(/*width*/ 80)
|
||||
.into_iter()
|
||||
.map(|mut line| {
|
||||
if cell.as_any().is::<history_cell::FinalMessageSeparator>() {
|
||||
line.spans = vec![normalize_completion_timestamps(cell, &line).into()];
|
||||
}
|
||||
line
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn drain_insert_history_transcript(
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
) -> Vec<Vec<ratatui::text::Line<'static>>> {
|
||||
drain_insert_history_with(rx, |cell| cell.transcript_lines(/*width*/ 80))
|
||||
}
|
||||
|
||||
fn drain_insert_history_with(
|
||||
pub(super) fn drain_insert_history_with(
|
||||
rx: &mut tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
render: impl Fn(&dyn HistoryCell) -> Vec<ratatui::text::Line<'static>>,
|
||||
) -> Vec<Vec<ratatui::text::Line<'static>>> {
|
||||
@@ -1768,11 +1784,17 @@ pub(super) async fn assert_hook_events_snapshot(
|
||||
assert_chatwidget_snapshot!(snapshot_name, combined);
|
||||
}
|
||||
|
||||
/// Normalize complete footer lines only, in snapshots that opt into clock normalization.
|
||||
pub(crate) fn normalize_completion_timestamps(value: impl std::fmt::Display) -> String {
|
||||
/// Normalize timestamps only in structurally identified completion footer cells.
|
||||
pub(crate) fn normalize_completion_timestamps(
|
||||
cell: &dyn HistoryCell,
|
||||
value: impl std::fmt::Display,
|
||||
) -> String {
|
||||
if !cell.as_any().is::<history_cell::FinalMessageSeparator>() {
|
||||
return value.to_string();
|
||||
}
|
||||
static COMPLETION_FOOTER: std::sync::LazyLock<regex_lite::Regex> = std::sync::LazyLock::new(
|
||||
|| {
|
||||
regex_lite::Regex::new(r"(?m)^(?P<indent>[ \t]*)(?P<duration>Worked for (?:[0-9]+h )?(?:[0-9]+m )?[0-9]+s · )?done (?:[A-Z][a-z]{2} [0-9]{1,2}(?:, [0-9]{4})? at )?[0-9]{1,2}:[0-9]{2} (?:AM|PM)(?P<padding>[ \t]*)$")
|
||||
regex_lite::Regex::new(r"(?m)^(?P<indent>[ \t]*)(?P<duration>Worked for (?:[0-9]+h )?(?:[0-9]+m )?[0-9]+s · )?(?:[A-Z][a-z]{2} [0-9]{1,2}(?:, [0-9]{4})? at )?[0-9]{1,2}:[0-9]{2} (?:AM|PM)(?P<padding>[ \t]*)$")
|
||||
.expect("valid completion footer pattern")
|
||||
},
|
||||
);
|
||||
@@ -1785,7 +1807,7 @@ pub(crate) fn normalize_completion_timestamps(value: impl std::fmt::Display) ->
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{indent}{duration}done [completion time]{padding}")
|
||||
format!("{indent}{duration}[completion time]{padding}")
|
||||
})
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
@@ -2180,16 +2180,14 @@ async fn slash_export_opens_destination_picker() {
|
||||
}
|
||||
chat.add_error_message("Copy failed: clipboard unavailable".to_string());
|
||||
chat.add_error_message("Export failed: missing parent".to_string());
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
let cells = drain_insert_history_normalized(&mut rx);
|
||||
assert_chatwidget_snapshot!(
|
||||
"slash_export_completion_message",
|
||||
normalize_completion_timestamps(
|
||||
cells
|
||||
.iter()
|
||||
.map(|cell| lines_to_single_string(cell).trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
),
|
||||
cells
|
||||
.iter()
|
||||
.map(|cell| lines_to_single_string(cell).trim().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -5806,16 +5806,14 @@ printf 'fenced within fenced\n'
|
||||
|
||||
// Finalize the stream without sending a final AgentMessage, to flush any tail.
|
||||
handle_turn_completed(&mut chat, "turn-1", /*duration_ms*/ None);
|
||||
for lines in drain_insert_history(&mut rx) {
|
||||
for lines in drain_insert_history_normalized(&mut rx) {
|
||||
crate::insert_history::insert_history_lines(&mut term, lines)
|
||||
.expect("Failed to insert history lines in test");
|
||||
}
|
||||
|
||||
assert_chatwidget_snapshot!(
|
||||
"chatwidget_markdown_code_blocks_vt100_snapshot",
|
||||
normalize_completion_timestamps(normalize_snapshot_paths(
|
||||
term.backend().vt100().screen().contents()
|
||||
))
|
||||
normalize_snapshot_paths(term.backend().vt100().screen().contents())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,11 +65,11 @@ impl FinalMessageSeparator {
|
||||
}
|
||||
if let Some(completed_at) = self.completed_at {
|
||||
let format = if completed_at.date_naive() == today {
|
||||
"done %-I:%M %p"
|
||||
"%-I:%M %p"
|
||||
} else if completed_at.year() == today.year() {
|
||||
"done %b %-d at %-I:%M %p"
|
||||
"%b %-d at %-I:%M %p"
|
||||
} else {
|
||||
"done %b %-d, %Y at %-I:%M %p"
|
||||
"%b %-d, %Y at %-I:%M %p"
|
||||
};
|
||||
label_parts.push(completed_at.format(format).to_string());
|
||||
}
|
||||
|
||||
@@ -40,13 +40,13 @@ fn completion_label_shows_duration_only_above_sixty_seconds() {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
insta::assert_snapshot!(labels.join("\n"), @"
|
||||
done 2:32 PM
|
||||
done 2:32 PM
|
||||
done 2:32 PM
|
||||
done 2:32 PM
|
||||
Worked for 1m 1s · done 2:32 PM
|
||||
Worked for 2m 5s · done 2:32 PM
|
||||
Worked for 1h 0m 5s · done 2:32 PM
|
||||
2:32 PM
|
||||
2:32 PM
|
||||
2:32 PM
|
||||
2:32 PM
|
||||
Worked for 1m 1s · 2:32 PM
|
||||
Worked for 2m 5s · 2:32 PM
|
||||
Worked for 1h 0m 5s · 2:32 PM
|
||||
");
|
||||
}
|
||||
|
||||
@@ -60,9 +60,9 @@ fn completion_label_includes_date_when_viewed_on_another_day() {
|
||||
.with_completed_at(completed_at);
|
||||
let tomorrow = completed_at.date_naive().succ_opt().expect("next day");
|
||||
|
||||
insta::assert_snapshot!(cell.label(tomorrow).expect("completion label"), @"Worked for 2m 5s · done Sep 6 at 2:32 PM");
|
||||
insta::assert_snapshot!(cell.label(tomorrow).expect("completion label"), @"Worked for 2m 5s · Sep 6 at 2:32 PM");
|
||||
let next_year = tomorrow.with_year(/*year*/ 2001).expect("valid next year");
|
||||
insta::assert_snapshot!(cell.label(next_year).expect("completion label"), @"Worked for 2m 5s · done Sep 6, 2000 at 2:32 PM");
|
||||
insta::assert_snapshot!(cell.label(next_year).expect("completion label"), @"Worked for 2m 5s · Sep 6, 2000 at 2:32 PM");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -75,9 +75,9 @@ fn completion_uses_twelve_hour_time_at_midnight_noon_and_afternoon() {
|
||||
.expect("completion label")
|
||||
});
|
||||
insta::assert_snapshot!(labels.join("\n"), @"
|
||||
done 12:32 AM
|
||||
done 12:32 PM
|
||||
done 3:32 PM
|
||||
12:32 AM
|
||||
12:32 PM
|
||||
3:32 PM
|
||||
");
|
||||
}
|
||||
|
||||
@@ -115,13 +115,13 @@ fn completion_wraps_metadata_and_preserves_unwrapped_raw_text() {
|
||||
let lines = cell.display_lines(/*width*/ 24);
|
||||
let rendered = lines.iter().map(ToString::to_string).collect::<Vec<_>>();
|
||||
|
||||
insta::assert_snapshot!(rendered.join("\n"), @"
|
||||
Worked for 2m 5s ·
|
||||
done Sep 6, 2000 at
|
||||
2:32 PM · Local tools:
|
||||
3 calls (2.5s)
|
||||
insta::assert_snapshot!(rendered.join("\n"), @r"
|
||||
Worked for 2m 5s · Sep
|
||||
6, 2000 at 2:32 PM ·
|
||||
Local tools: 3 calls
|
||||
(2.5s)
|
||||
");
|
||||
insta::assert_snapshot!(cell.raw_lines()[0].to_string(), @"Worked for 2m 5s · done Sep 6, 2000 at 2:32 PM · Local tools: 3 calls (2.5s)");
|
||||
insta::assert_snapshot!(cell.raw_lines()[0].to_string(), @"Worked for 2m 5s · Sep 6, 2000 at 2:32 PM · Local tools: 3 calls (2.5s)");
|
||||
for width in [0, 1, 5, 24] {
|
||||
assert!(
|
||||
cell.display_lines(width)
|
||||
@@ -163,6 +163,6 @@ fn completion_rendering_uses_the_captured_display_date() {
|
||||
.with_completed_at(completed_at);
|
||||
cell.display_date = completed_at.date_naive();
|
||||
|
||||
insta::assert_snapshot!(cell.display_lines(/*width*/ 80)[0].to_string(), @" done 2:32 PM");
|
||||
assert_eq!(cell.raw_lines(), vec![Line::from("done 2:32 PM")]);
|
||||
insta::assert_snapshot!(cell.display_lines(/*width*/ 80)[0].to_string(), @" 2:32 PM");
|
||||
assert_eq!(cell.raw_lines(), vec![Line::from("2:32 PM")]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user