mirror of
https://github.com/openai/codex.git
synced 2026-11-09 08:40:33 +00:00
## 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.
172 lines
6.4 KiB
Rust
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 }
|
|
}
|