Files
codex/codex-rs/tui/src/history_cell/separators.rs
T
Eric Traut e6a7368810 TUI: split history cells into focused modules (#22704)
## Why

`codex-rs/tui/src/history_cell.rs` had become the dumping ground for
transcript rendering: the shared trait, common helpers, and the concrete
cells for messages, plans, MCP/search, notices, patches, approvals,
session chrome, and separators all lived together. That made small
transcript changes require reopening a very large file and made
ownership less obvious.

## What changed

- Replaced the monolithic `history_cell.rs` with a `history_cell/`
module tree organized by concern.
- Kept the existing `crate::history_cell::*` surface stable through
re-exports in `history_cell/mod.rs`.
- Moved the existing render coverage into `history_cell/tests.rs`.

## Reviewer notes

- This PR is intentionally mechanical in mature — existing code and
tests moving into files that match their concern.
- The snapshot files under `codex-rs/tui/src/history_cell/snapshots/`
moved with the extracted test module. `insta` resolves these unnamed
snapshots relative to the source file that declares them, so this is
path churn only; snapshot contents were not updated.
- The small non-mechanical seam edits are limited to split fallout:
sibling-module visibility for shared cell containers, moving
approval-specific exec-snippet helpers beside approvals, fixing the
separator module path, and keeping a couple of existing test helpers
reachable after extraction.
2026-05-14 21:19:06 -07:00

172 lines
6.4 KiB
Rust

//! Turn separators and runtime-metrics labels for transcript history.
use super::*;
#[derive(Debug)]
/// A visual divider between turns, optionally showing how long the assistant "worked for".
///
/// This separator is only emitted for turns that performed concrete work (e.g., running commands,
/// applying patches, making MCP tool calls), so purely conversational turns do not show an empty
/// divider.
pub struct FinalMessageSeparator {
elapsed_seconds: Option<u64>,
runtime_metrics: Option<RuntimeMetricsSummary>,
}
impl FinalMessageSeparator {
/// Creates a separator; completed turns should pass protocol turn duration when available.
pub(crate) fn new(
elapsed_seconds: Option<u64>,
runtime_metrics: Option<RuntimeMetricsSummary>,
) -> Self {
Self {
elapsed_seconds,
runtime_metrics,
}
}
}
impl HistoryCell for FinalMessageSeparator {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let mut label_parts = Vec::new();
if let Some(elapsed_seconds) = self
.elapsed_seconds
.filter(|seconds| *seconds > 60)
.map(crate::status_indicator_widget::fmt_elapsed_compact)
{
label_parts.push(format!("Worked for {elapsed_seconds}"));
}
if let Some(metrics_label) = self.runtime_metrics.and_then(runtime_metrics_label) {
label_parts.push(metrics_label);
}
if label_parts.is_empty() {
return vec![Line::from_iter(["─".repeat(width as usize).dim()])];
}
let label = format!("─ {} ─", label_parts.join(" • "));
let (label, _suffix, label_width) = take_prefix_by_width(&label, width as usize);
vec![
Line::from_iter([
label,
"─".repeat((width as usize).saturating_sub(label_width)),
])
.dim(),
]
}
fn raw_lines(&self) -> Vec<Line<'static>> {
let mut label_parts = Vec::new();
if let Some(elapsed_seconds) = self
.elapsed_seconds
.filter(|seconds| *seconds > 60)
.map(crate::status_indicator_widget::fmt_elapsed_compact)
{
label_parts.push(format!("Worked for {elapsed_seconds}"));
}
if let Some(metrics_label) = self.runtime_metrics.and_then(runtime_metrics_label) {
label_parts.push(metrics_label);
}
if label_parts.is_empty() {
Vec::new()
} else {
vec![Line::from(label_parts.join(" • "))]
}
}
}
pub(crate) fn runtime_metrics_label(summary: RuntimeMetricsSummary) -> Option<String> {
let mut parts = Vec::new();
if summary.tool_calls.count > 0 {
let duration = format_duration_ms(summary.tool_calls.duration_ms);
let calls = pluralize(summary.tool_calls.count, "call", "calls");
parts.push(format!(
"Local tools: {} {calls} ({duration})",
summary.tool_calls.count
));
}
if summary.api_calls.count > 0 {
let duration = format_duration_ms(summary.api_calls.duration_ms);
let calls = pluralize(summary.api_calls.count, "call", "calls");
parts.push(format!(
"Inference: {} {calls} ({duration})",
summary.api_calls.count
));
}
if summary.websocket_calls.count > 0 {
let duration = format_duration_ms(summary.websocket_calls.duration_ms);
parts.push(format!(
"WebSocket: {} events send ({duration})",
summary.websocket_calls.count
));
}
if summary.streaming_events.count > 0 {
let duration = format_duration_ms(summary.streaming_events.duration_ms);
let stream_label = pluralize(summary.streaming_events.count, "Stream", "Streams");
let events = pluralize(summary.streaming_events.count, "event", "events");
parts.push(format!(
"{stream_label}: {} {events} ({duration})",
summary.streaming_events.count
));
}
if summary.websocket_events.count > 0 {
let duration = format_duration_ms(summary.websocket_events.duration_ms);
parts.push(format!(
"{} events received ({duration})",
summary.websocket_events.count
));
}
if summary.responses_api_overhead_ms > 0 {
let duration = format_duration_ms(summary.responses_api_overhead_ms);
parts.push(format!("Responses API overhead: {duration}"));
}
if summary.responses_api_inference_time_ms > 0 {
let duration = format_duration_ms(summary.responses_api_inference_time_ms);
parts.push(format!("Responses API inference: {duration}"));
}
if summary.responses_api_engine_iapi_ttft_ms > 0
|| summary.responses_api_engine_service_ttft_ms > 0
{
let mut ttft_parts = Vec::new();
if summary.responses_api_engine_iapi_ttft_ms > 0 {
let duration = format_duration_ms(summary.responses_api_engine_iapi_ttft_ms);
ttft_parts.push(format!("{duration} (iapi)"));
}
if summary.responses_api_engine_service_ttft_ms > 0 {
let duration = format_duration_ms(summary.responses_api_engine_service_ttft_ms);
ttft_parts.push(format!("{duration} (service)"));
}
parts.push(format!("TTFT: {}", ttft_parts.join(" ")));
}
if summary.responses_api_engine_iapi_tbt_ms > 0
|| summary.responses_api_engine_service_tbt_ms > 0
{
let mut tbt_parts = Vec::new();
if summary.responses_api_engine_iapi_tbt_ms > 0 {
let duration = format_duration_ms(summary.responses_api_engine_iapi_tbt_ms);
tbt_parts.push(format!("{duration} (iapi)"));
}
if summary.responses_api_engine_service_tbt_ms > 0 {
let duration = format_duration_ms(summary.responses_api_engine_service_tbt_ms);
tbt_parts.push(format!("{duration} (service)"));
}
parts.push(format!("TBT: {}", tbt_parts.join(" ")));
}
if parts.is_empty() {
None
} else {
Some(parts.join(" • "))
}
}
fn format_duration_ms(duration_ms: u64) -> String {
if duration_ms >= 1_000 {
let seconds = duration_ms as f64 / 1_000.0;
format!("{seconds:.1}s")
} else {
format!("{duration_ms}ms")
}
}
fn pluralize(count: u64, singular: &'static str, plural: &'static str) -> &'static str {
if count == 1 { singular } else { plural }
}