mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Introduce a full codex-tui source snapshot under the new codex-tui2 crate so viewport work can be replayed in isolation. This change copies the entire codex-rs/tui/src tree into codex-rs/tui2/src in one atomic step, rather than piecemeal, to keep future diffs vs the original viewport bookmark easy to reason about. The goal is for codex-tui2 to render identically to the existing TUI behind the `features.tui2` flag while we gradually port the viewport/history commits from the joshka/viewport bookmark onto this forked tree. While on this baseline change, we also ran the codex-tui2 snapshot test suite and accepted all insta snapshots for the new crate, so the snapshot files now use the codex-tui2 naming scheme and encode the unmodified legacy TUI behavior. This keeps later viewport commits focused on intentional behavior changes (and their snapshots) rather than on mechanical snapshot renames.
190 lines
6.0 KiB
Rust
190 lines
6.0 KiB
Rust
use crate::exec_command::relativize_to_home;
|
|
use crate::text_formatting;
|
|
use chrono::DateTime;
|
|
use chrono::Local;
|
|
use codex_app_server_protocol::AuthMode;
|
|
use codex_core::AuthManager;
|
|
use codex_core::config::Config;
|
|
use codex_core::project_doc::discover_project_doc_paths;
|
|
use codex_protocol::account::PlanType;
|
|
use std::path::Path;
|
|
use unicode_width::UnicodeWidthStr;
|
|
|
|
use super::account::StatusAccountDisplay;
|
|
|
|
fn normalize_agents_display_path(path: &Path) -> String {
|
|
dunce::simplified(path).display().to_string()
|
|
}
|
|
|
|
pub(crate) fn compose_model_display(
|
|
model_name: &str,
|
|
entries: &[(&str, String)],
|
|
) -> (String, Vec<String>) {
|
|
let mut details: Vec<String> = Vec::new();
|
|
if let Some((_, effort)) = entries.iter().find(|(k, _)| *k == "reasoning effort") {
|
|
details.push(format!("reasoning {}", effort.to_ascii_lowercase()));
|
|
}
|
|
if let Some((_, summary)) = entries.iter().find(|(k, _)| *k == "reasoning summaries") {
|
|
let summary = summary.trim();
|
|
if summary.eq_ignore_ascii_case("none") || summary.eq_ignore_ascii_case("off") {
|
|
details.push("summaries off".to_string());
|
|
} else if !summary.is_empty() {
|
|
details.push(format!("summaries {}", summary.to_ascii_lowercase()));
|
|
}
|
|
}
|
|
|
|
(model_name.to_string(), details)
|
|
}
|
|
|
|
pub(crate) fn compose_agents_summary(config: &Config) -> String {
|
|
match discover_project_doc_paths(config) {
|
|
Ok(paths) => {
|
|
let mut rels: Vec<String> = Vec::new();
|
|
for p in paths {
|
|
let file_name = p
|
|
.file_name()
|
|
.map(|name| name.to_string_lossy().to_string())
|
|
.unwrap_or_else(|| "<unknown>".to_string());
|
|
let display = if let Some(parent) = p.parent() {
|
|
if parent == config.cwd {
|
|
file_name.clone()
|
|
} else {
|
|
let mut cur = config.cwd.as_path();
|
|
let mut ups = 0usize;
|
|
let mut reached = false;
|
|
while let Some(c) = cur.parent() {
|
|
if cur == parent {
|
|
reached = true;
|
|
break;
|
|
}
|
|
cur = c;
|
|
ups += 1;
|
|
}
|
|
if reached {
|
|
let up = format!("..{}", std::path::MAIN_SEPARATOR);
|
|
format!("{}{}", up.repeat(ups), file_name)
|
|
} else if let Ok(stripped) = p.strip_prefix(&config.cwd) {
|
|
normalize_agents_display_path(stripped)
|
|
} else {
|
|
normalize_agents_display_path(&p)
|
|
}
|
|
}
|
|
} else {
|
|
normalize_agents_display_path(&p)
|
|
};
|
|
rels.push(display);
|
|
}
|
|
if rels.is_empty() {
|
|
"<none>".to_string()
|
|
} else {
|
|
rels.join(", ")
|
|
}
|
|
}
|
|
Err(_) => "<none>".to_string(),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn compose_account_display(
|
|
auth_manager: &AuthManager,
|
|
plan: Option<PlanType>,
|
|
) -> Option<StatusAccountDisplay> {
|
|
let auth = auth_manager.auth()?;
|
|
|
|
match auth.mode {
|
|
AuthMode::ChatGPT => {
|
|
let email = auth.get_account_email();
|
|
let plan = plan
|
|
.map(|plan_type| title_case(format!("{plan_type:?}").as_str()))
|
|
.or_else(|| Some("Unknown".to_string()));
|
|
Some(StatusAccountDisplay::ChatGpt { email, plan })
|
|
}
|
|
AuthMode::ApiKey => Some(StatusAccountDisplay::ApiKey),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn format_tokens_compact(value: i64) -> String {
|
|
let value = value.max(0);
|
|
if value == 0 {
|
|
return "0".to_string();
|
|
}
|
|
if value < 1_000 {
|
|
return value.to_string();
|
|
}
|
|
|
|
let value_f64 = value as f64;
|
|
let (scaled, suffix) = if value >= 1_000_000_000_000 {
|
|
(value_f64 / 1_000_000_000_000.0, "T")
|
|
} else if value >= 1_000_000_000 {
|
|
(value_f64 / 1_000_000_000.0, "B")
|
|
} else if value >= 1_000_000 {
|
|
(value_f64 / 1_000_000.0, "M")
|
|
} else {
|
|
(value_f64 / 1_000.0, "K")
|
|
};
|
|
|
|
let decimals = if scaled < 10.0 {
|
|
2
|
|
} else if scaled < 100.0 {
|
|
1
|
|
} else {
|
|
0
|
|
};
|
|
|
|
let mut formatted = format!("{scaled:.decimals$}");
|
|
if formatted.contains('.') {
|
|
while formatted.ends_with('0') {
|
|
formatted.pop();
|
|
}
|
|
if formatted.ends_with('.') {
|
|
formatted.pop();
|
|
}
|
|
}
|
|
|
|
format!("{formatted}{suffix}")
|
|
}
|
|
|
|
pub(crate) fn format_directory_display(directory: &Path, max_width: Option<usize>) -> String {
|
|
let formatted = if let Some(rel) = relativize_to_home(directory) {
|
|
if rel.as_os_str().is_empty() {
|
|
"~".to_string()
|
|
} else {
|
|
format!("~{}{}", std::path::MAIN_SEPARATOR, rel.display())
|
|
}
|
|
} else {
|
|
directory.display().to_string()
|
|
};
|
|
|
|
if let Some(max_width) = max_width {
|
|
if max_width == 0 {
|
|
return String::new();
|
|
}
|
|
if UnicodeWidthStr::width(formatted.as_str()) > max_width {
|
|
return text_formatting::center_truncate_path(&formatted, max_width);
|
|
}
|
|
}
|
|
|
|
formatted
|
|
}
|
|
|
|
pub(crate) fn format_reset_timestamp(dt: DateTime<Local>, captured_at: DateTime<Local>) -> String {
|
|
let time = dt.format("%H:%M").to_string();
|
|
if dt.date_naive() == captured_at.date_naive() {
|
|
time
|
|
} else {
|
|
format!("{time} on {}", dt.format("%-d %b"))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn title_case(s: &str) -> String {
|
|
if s.is_empty() {
|
|
return String::new();
|
|
}
|
|
let mut chars = s.chars();
|
|
let first = match chars.next() {
|
|
Some(c) => c,
|
|
None => return String::new(),
|
|
};
|
|
let rest: String = chars.as_str().to_ascii_lowercase();
|
|
first.to_uppercase().collect::<String>() + &rest
|
|
}
|