use crate::{
output::ContextUsageSource,
tui::{PADDED_INLINE_SEPARATOR, normalize_inline_separators},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ContextUsageState {
pub(crate) current_tokens: usize,
pub(crate) max_tokens: usize,
pub(crate) reasoning_tokens: Option<usize>,
pub(crate) source: ContextUsageSource,
pub(crate) request_sequence: u64,
}
impl Default for ContextUsageState {
fn default() -> Self {
Self {
current_tokens: 0,
max_tokens: 0,
reasoning_tokens: None,
source: ContextUsageSource::FallbackEstimate,
request_sequence: 0,
}
}
}
pub(super) fn merge_context_usage(
previous: Option<ContextUsageState>,
incoming: ContextUsageState,
) -> ContextUsageState {
let Some(previous) = previous else {
return incoming;
};
let mut merged = previous;
if incoming.request_sequence > previous.request_sequence {
let accepts_new_sequence = incoming.current_tokens > 0
|| matches!(
incoming.source,
ContextUsageSource::ProviderExact | ContextUsageSource::ProviderPartial
);
if accepts_new_sequence {
merged = incoming;
}
} else if incoming.request_sequence == previous.request_sequence {
if incoming.source == ContextUsageSource::ProviderPartial {
merged.max_tokens = incoming.max_tokens;
merged.reasoning_tokens = incoming.reasoning_tokens;
} else if context_usage_source_strength(incoming.source)
>= context_usage_source_strength(previous.source)
&& !(incoming.current_tokens == 0
&& incoming.source != ContextUsageSource::ProviderExact
&& previous.current_tokens > 0)
{
merged = incoming;
}
}
if !matches!(
incoming.source,
ContextUsageSource::ProviderExact | ContextUsageSource::ProviderPartial
) {
merged.reasoning_tokens = previous.reasoning_tokens;
}
merged
}
fn context_usage_source_strength(source: ContextUsageSource) -> u8 {
match source {
ContextUsageSource::ProviderExact => 4,
ContextUsageSource::LastProviderUsage
| ContextUsageSource::TokenizerEstimate
| ContextUsageSource::TokenizerProjection => 3,
ContextUsageSource::FallbackEstimate | ContextUsageSource::FallbackProjection => 2,
ContextUsageSource::ProviderPartial => 1,
}
}
pub(super) fn transcript_tool_row(
call: &crate::providers::ToolCall,
summary: &crate::output::ToolDisplaySummary,
) -> String {
let mut parts = vec![format!(
"{} {}",
summary.unicode_mark,
transcript_tool_label(call, summary)
)];
parts.extend(transcript_tool_metadata(summary).into_iter().take(4));
parts.join(PADDED_INLINE_SEPARATOR)
}
fn transcript_tool_label(
call: &crate::providers::ToolCall,
summary: &crate::output::ToolDisplaySummary,
) -> String {
let tool_name = if call.name.trim().is_empty() {
summary.tool_name.as_str()
} else {
call.name.as_str()
};
match tool_name {
"bash" | "shell" => tool_name.to_string(),
_ => normalize_inline_separators(&summary.label),
}
}
pub(super) fn transcript_tool_metadata(summary: &crate::output::ToolDisplaySummary) -> Vec<String> {
summary
.metadata
.iter()
.filter(|(key, _)| !(key == "tasks" && transcript_label_has_task_count(&summary.label)))
.filter_map(|(key, value)| transcript_tool_metadata_part(key, value))
.collect()
}
pub(super) fn transcript_metadata_fields(fields: &[(String, String)]) -> Vec<String> {
fields
.iter()
.filter_map(|(key, value)| transcript_tool_metadata_part(key, value))
.collect()
}
fn transcript_label_has_task_count(label: &str) -> bool {
label
.split([crate::tui::INLINE_SEPARATOR_CHAR, '·'])
.any(|part| {
let part = part.trim();
part.strip_suffix(" tasks")
.or_else(|| part.strip_suffix(" task"))
.is_some_and(|count| count.trim().parse::<usize>().is_ok())
})
}
fn transcript_count_label(value: &str, singular: &str, plural: &str) -> String {
let unit = if value == "1" { singular } else { plural };
format!("{value} {unit}")
}
pub(super) fn is_transient_status_error(message: &str) -> bool {
matches!(message.trim(), "running prompt…" | "running prompt...")
}
pub(super) fn transcript_tool_metadata_part(key: &str, value: &str) -> Option<String> {
match key {
"exit_code" => Some(format!("exit {value}")),
"output_lines" => Some(transcript_count_label(value, "line", "lines")),
"edit_count" => Some(transcript_count_label(value, "edit", "edits")),
"matches_returned" => Some(transcript_count_label(value, "match", "matches")),
"bytes" => Some(format!("{value} bytes")),
"total_tokens" => Some(format!("{value} tokens")),
"tasks" => Some(transcript_count_label(value, "task", "tasks")),
"completed" => Some(format!("{value} completed")),
"failed" => Some(format!("{value} failed")),
"identities" => Some(format!("identities {value}")),
"reference" => Some(format!("reference {value}")),
"timed_out" if value == "true" => Some("timed out".to_string()),
_ => None,
}
}