This commit is contained in:
easong-openai
2025-09-04 21:09:39 -07:00
parent e5d31d5ccc
commit 83dfb43dbd
11 changed files with 185 additions and 207 deletions

View File

@@ -204,7 +204,7 @@ mod tests {
let mut out = Vec::new();
for (i, t) in titles.into_iter().enumerate() {
out.push(TaskSummary {
id: TaskId(format!("T-{}", i)),
id: TaskId(format!("T-{i}")),
title: t.to_string(),
status: codex_cloud_tasks_api::TaskStatus::Ready,
updated_at: Utc::now(),

View File

@@ -105,7 +105,7 @@ async fn main() -> anyhow::Result<()> {
// Print the full response object for debugging/inspection.
match serde_json::to_string_pretty(&list) {
Ok(json) => {
println!("\nfull response object (pretty JSON):\n{}", json);
println!("\nfull response object (pretty JSON):\n{json}");
}
Err(e) => {
println!("failed to serialize response to JSON: {e}");

View File

@@ -36,7 +36,7 @@ async fn main() -> anyhow::Result<()> {
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
println!("base_url: {base_url}");
println!(
@@ -68,7 +68,7 @@ async fn main() -> anyhow::Result<()> {
match auth.get_token().await {
Ok(token) if !token.is_empty() => {
println!("auth: ChatGPT token present ({} chars)", token.len());
let value = format!("Bearer {}", token);
let value = format!("Bearer {token}");
if let Ok(hv) = HeaderValue::from_str(&value) {
headers.insert(AUTHORIZATION, hv);
}

View File

@@ -47,7 +47,7 @@ async fn main() -> anyhow::Result<()> {
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
println!("base_url: {base_url}");
let is_wham = base_url.contains("/backend-api");
@@ -73,7 +73,7 @@ async fn main() -> anyhow::Result<()> {
Ok(token) if !token.is_empty() => {
have_auth = true;
println!("auth: ChatGPT token present ({} chars)", token.len());
let value = format!("Bearer {}", token);
let value = format!("Bearer {token}");
if let Ok(hv) = HeaderValue::from_str(&value) {
headers.insert(AUTHORIZATION, hv);
}
@@ -123,9 +123,9 @@ async fn main() -> anyhow::Result<()> {
// Build request payload patterned after VSCode: POST /wham/tasks
let url = if is_wham {
format!("{}/wham/tasks", base_url)
format!("{base_url}/wham/tasks")
} else {
format!("{}/api/codex/tasks", base_url)
format!("{base_url}/api/codex/tasks")
};
println!(
"request: POST {}",
@@ -176,14 +176,14 @@ async fn main() -> anyhow::Result<()> {
.unwrap_or("")
.to_string();
let body = res.text().await.unwrap_or_default();
println!("status: {}", status);
println!("content-type: {}", ct);
println!("status: {status}");
println!("content-type: {ct}");
match serde_json::from_str::<serde_json::Value>(&body) {
Ok(v) => println!(
"response (pretty JSON):\n{}",
serde_json::to_string_pretty(&v).unwrap_or(body)
),
Err(_) => println!("response (raw):\n{}", body),
Err(_) => println!("response (raw):\n{body}"),
}
if !status.is_success() {

View File

@@ -28,7 +28,7 @@ pub async fn autodetect_environment_id(
) -> anyhow::Result<AutodetectSelection> {
// 1) Try repo-specific environments based on local git origins (GitHub only, like VSCode)
let origins = get_git_origins();
crate::append_error_log(format!("env: git origins: {:?}", origins));
crate::append_error_log(format!("env: git origins: {origins:?}"));
let mut by_repo_envs: Vec<CodeEnvironment> = Vec::new();
for origin in &origins {
if let Some((owner, repo)) = parse_owner_repo(origin) {
@@ -43,20 +43,17 @@ pub async fn autodetect_environment_id(
base_url, "github", owner, repo
)
};
crate::append_error_log(format!("env: GET {}", url));
crate::append_error_log(format!("env: GET {url}"));
match get_json::<Vec<CodeEnvironment>>(&url, headers).await {
Ok(mut list) => {
crate::append_error_log(format!(
"env: by-repo returned {} env(s) for {}/{}",
"env: by-repo returned {} env(s) for {owner}/{repo}",
list.len(),
owner,
repo
));
by_repo_envs.append(&mut list);
}
Err(e) => crate::append_error_log(format!(
"env: by-repo fetch failed for {}/{}: {e}",
owner, repo
"env: by-repo fetch failed for {owner}/{repo}: {e}"
)),
}
}
@@ -70,11 +67,11 @@ pub async fn autodetect_environment_id(
// 2) Fallback to the full list
let list_url = if base_url.contains("/backend-api") {
format!("{}/wham/environments", base_url)
format!("{base_url}/wham/environments")
} else {
format!("{}/api/codex/environments", base_url)
format!("{base_url}/api/codex/environments")
};
crate::append_error_log(format!("env: GET {}", list_url));
crate::append_error_log(format!("env: GET {list_url}"));
// Fetch and log the full environments JSON for debugging
let http = reqwest::Client::builder().build()?;
let res = http.get(&list_url).headers(headers.clone()).send().await?;
@@ -86,25 +83,21 @@ pub async fn autodetect_environment_id(
.unwrap_or("")
.to_string();
let body = res.text().await.unwrap_or_default();
crate::append_error_log(format!("env: status={} content-type={}", status, ct));
crate::append_error_log(format!("env: status={status} content-type={ct}"));
match serde_json::from_str::<serde_json::Value>(&body) {
Ok(v) => {
let pretty = serde_json::to_string_pretty(&v).unwrap_or(body.clone());
crate::append_error_log(format!("env: /environments JSON (pretty):\n{}", pretty));
crate::append_error_log(format!("env: /environments JSON (pretty):\n{pretty}"));
}
Err(_) => crate::append_error_log(format!("env: /environments (raw):\n{}", body)),
Err(_) => crate::append_error_log(format!("env: /environments (raw):\n{body}")),
}
if !status.is_success() {
anyhow::bail!(format!(
"GET {} failed: {}; content-type={}; body={}",
list_url, status, ct, body
));
anyhow::bail!("GET {list_url} failed: {status}; content-type={ct}; body={body}");
}
let all_envs: Vec<CodeEnvironment> = serde_json::from_str(&body).map_err(|e| {
anyhow::anyhow!(format!(
"Decode error for {}: {}; content-type={}; body={}",
list_url, e, ct, body
))
anyhow::anyhow!(
"Decode error for {list_url}: {e}; content-type={ct}; body={body}"
)
})?;
if let Some(env) = pick_environment_row(&all_envs, desired_label.as_deref()) {
return Ok(AutodetectSelection {
@@ -128,7 +121,7 @@ fn pick_environment_row(
.iter()
.find(|e| e.label.as_deref().unwrap_or("").to_lowercase() == lc)
{
crate::append_error_log(format!("env: matched by label: {} -> {}", label, e.id));
crate::append_error_log(format!("env: matched by label: {label} -> {}", e.id));
return Some(e.clone());
}
}
@@ -166,16 +159,12 @@ async fn get_json<T: serde::de::DeserializeOwned>(
.unwrap_or("")
.to_string();
let body = res.text().await.unwrap_or_default();
crate::append_error_log(format!("env: status={} content-type={}", status, ct));
crate::append_error_log(format!("env: status={status} content-type={ct}"));
if !status.is_success() {
anyhow::bail!(format!(
"GET {url} failed: {status}; content-type={ct}; body={body}"
));
anyhow::bail!("GET {url} failed: {status}; content-type={ct}; body={body}");
}
let parsed = serde_json::from_str::<T>(&body).map_err(|e| {
anyhow::anyhow!(format!(
"Decode error for {url}: {e}; content-type={ct}; body={body}"
))
anyhow::anyhow!("Decode error for {url}: {e}; content-type={ct}; body={body}")
})?;
Ok(parsed)
}
@@ -242,8 +231,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
let owner = parts.next()?.to_string();
let repo = parts.next()?.to_string();
crate::append_error_log(format!(
"env: parsed SSH GitHub origin => {}/{}",
owner, repo
"env: parsed SSH GitHub origin => {owner}/{repo}"
));
return Some((owner, repo));
}
@@ -260,8 +248,7 @@ fn parse_owner_repo(url: &str) -> Option<(String, String)> {
let owner = parts.next()?.to_string();
let repo = parts.next()?.to_string();
crate::append_error_log(format!(
"env: parsed HTTP GitHub origin => {}/{}",
owner, repo
"env: parsed HTTP GitHub origin => {owner}/{repo}"
));
return Some((owner, repo));
}
@@ -302,7 +289,7 @@ pub async fn list_environments(
id: e.id.clone(),
label: e.label.clone(),
is_pinned: e.is_pinned.unwrap_or(false),
repo_hints: Some(format!("{}/{}", owner, repo)),
repo_hints: Some(format!("{owner}/{repo}")),
});
// Merge: keep label if present, or use new; accumulate pinned flag
if entry.label.is_none() {
@@ -310,7 +297,7 @@ pub async fn list_environments(
}
entry.is_pinned = entry.is_pinned || e.is_pinned.unwrap_or(false);
if entry.repo_hints.is_none() {
entry.repo_hints = Some(format!("{}/{}", owner, repo));
entry.repo_hints = Some(format!("{owner}/{repo}"));
}
}
}
@@ -326,9 +313,9 @@ pub async fn list_environments(
// 2) Fallback to the full list; on error return what we have if any.
let list_url = if base_url.contains("/backend-api") {
format!("{}/wham/environments", base_url)
format!("{base_url}/wham/environments")
} else {
format!("{}/api/codex/environments", base_url)
format!("{base_url}/api/codex/environments")
};
match get_json::<Vec<CodeEnvironment>>(&list_url, headers).await {
Ok(list) => {

View File

@@ -112,7 +112,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
append_error_log(format!("startup: base_url={base_url} path_style={style}"));
// Require ChatGPT login (SWIC). Exit with a clear message if missing.
let token = match codex_core::config::find_codex_home()
let _token = match codex_core::config::find_codex_home()
.ok()
.map(|home| {
codex_login::AuthManager::new(
@@ -250,7 +250,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
let ua =
codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
@@ -267,30 +267,25 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
"codex_cloud_tasks_tui".to_string(),
);
if let Some(auth) = am.auth() {
if let Ok(tok) = auth.get_token().await {
if !tok.is_empty() {
let v = format!("Bearer {}", tok);
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) {
headers.insert(reqwest::header::AUTHORIZATION, hv);
}
if let Some(acc) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&tok))
{
if let Ok(name) =
reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
{
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) {
headers.insert(name, hv);
}
}
}
if let Ok(tok) = auth.get_token().await && !tok.is_empty() {
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) {
headers.insert(reqwest::header::AUTHORIZATION, hv);
}
if let Some(acc) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) =
reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc)
{
headers.insert(name, hv);
}
}
}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
@@ -309,7 +304,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
|| base_url.starts_with("https://chat.openai.com"))
&& !base_url.contains("/backend-api")
{
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
// Build headers: UA + ChatGPT auth if available
@@ -328,27 +323,20 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
"codex_cloud_tasks_tui".to_string(),
);
if let Some(auth) = am.auth() {
if let Ok(token) = auth.get_token().await {
if !token.is_empty() {
if let Ok(hv) =
reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token))
{
headers.insert(reqwest::header::AUTHORIZATION, hv);
}
if let Some(account_id) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&token))
{
if let Ok(name) =
reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
{
if let Ok(hv) =
reqwest::header::HeaderValue::from_str(&account_id)
{
headers.insert(name, hv);
}
}
}
if let Ok(token) = auth.get_token().await && !token.is_empty() {
if let Ok(hv) =
reqwest::header::HeaderValue::from_str(&format!("Bearer {token}"))
{
headers.insert(reqwest::header::AUTHORIZATION, hv);
}
if let Some(account_id) = auth
.get_account_id()
.or_else(|| extract_chatgpt_account_id(&token))
&& let Ok(name) =
reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = reqwest::header::HeaderValue::from_str(&account_id)
{
headers.insert(name, hv);
}
}
}
@@ -356,9 +344,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
// Run autodetect. If it fails, we keep using "All".
let res = crate::env_detect::autodetect_environment_id(&base_url, &headers, None).await;
let _ = tx2.send(app::AppEvent::EnvironmentAutodetected(
res.map_err(|e| e.into()),
));
let _ = tx2.send(app::AppEvent::EnvironmentAutodetected(res));
});
}
@@ -382,7 +368,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
recv = frame_rx.recv() => {
match recv {
Some(at) => {
if next_deadline.map_or(true, |cur| at < cur) {
if next_deadline.is_none_or(|cur| at < cur) {
next_deadline = Some(at);
}
continue; // recompute sleep target
@@ -486,9 +472,9 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
let _ = frame_tx.send(Instant::now());
}
Err(msg) => {
append_error_log(format!("new-task: submit failed: {}", msg));
append_error_log(format!("new-task: submit failed: {msg}"));
if let Some(page) = app.new_task.as_mut() { page.submitting = false; }
app.status = format!("Submit failed: {}. See error.log for details.", msg);
app.status = format!("Submit failed: {msg}. See error.log for details.");
needs_redraw = true;
let _ = frame_tx.send(Instant::now());
}
@@ -574,7 +560,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
let mut base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
while base_url.ends_with('/') { base_url.pop(); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") {
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
let ua = codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
let mut headers = reqwest::header::HeaderMap::new();
@@ -586,19 +572,19 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
"codex_cloud_tasks_tui".to_string(),
);
if let Some(auth) = am.auth() {
if let Ok(tok) = auth.get_token().await { if !tok.is_empty() {
let v = format!("Bearer {}", tok);
if let Ok(tok) = auth.get_token().await && !tok.is_empty() {
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) { headers.insert(reqwest::header::AUTHORIZATION, hv); }
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok)) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id") {
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) { headers.insert(name, hv); }
}
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) {
headers.insert(name, hv);
}
}}
}
}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx3.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx3.send(app::AppEvent::EnvironmentsLoaded(res));
});
let _ = frame_tx.send(Instant::now());
}
@@ -629,7 +615,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
}
app::AppEvent::DetailsFailed { id, title, error } => {
if let Some(ov) = &app.diff_overlay { if ov.task_id != id { continue; } }
append_error_log(format!("details failed for {}: {}", id.0, error));
append_error_log(format!("details failed for {}: {error}", id.0));
let pretty = pretty_lines_from_error(&error);
let mut sd = crate::scrollable_diff::ScrollableDiff::new();
sd.set_content(pretty);
@@ -698,7 +684,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
.unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
while base_url.ends_with('/') { base_url.pop(); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") {
base_url = format!("{}/backend-api", base_url);
base_url = format!("{base_url}/backend-api");
}
let ua = codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
let mut headers = reqwest::header::HeaderMap::new();
@@ -710,19 +696,19 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
"codex_cloud_tasks_tui".to_string(),
);
if let Some(auth) = am.auth() {
if let Ok(tok) = auth.get_token().await { if !tok.is_empty() {
let v = format!("Bearer {}", tok);
if let Ok(tok) = auth.get_token().await && !tok.is_empty() {
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) { headers.insert(reqwest::header::AUTHORIZATION, hv); }
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok)) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id") {
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) { headers.insert(name, hv); }
}
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) {
headers.insert(name, hv);
}
}}
}
}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
// Render after opening env modal to show it instantly.
@@ -745,8 +731,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
if page.submitting {
// Ignore input while submitting
} else {
match page.composer.input(key) {
codex_tui::ComposerAction::Submitted(text) => {
if let codex_tui::ComposerAction::Submitted(text) = page.composer.input(key) {
// Submit only if we have an env id
if let Some(env) = page.env_id.clone() {
append_error_log(format!(
@@ -762,15 +747,13 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
let result = codex_cloud_tasks_api::CloudBackend::create_task(&*backend2, &env, &text, "main", false).await;
let evt = match result {
Ok(ok) => app::AppEvent::NewTaskSubmitted(Ok(ok)),
Err(e) => app::AppEvent::NewTaskSubmitted(Err(format!("{}", e))),
Err(e) => app::AppEvent::NewTaskSubmitted(Err(format!("{e}"))),
};
let _ = tx2.send(evt);
});
} else {
app.status = "No environment selected (press 'e' to choose)".to_string();
}
}
_ => {}
}
}
needs_redraw = true;
@@ -895,7 +878,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
// Build headers (UA + ChatGPT token + account id)
let mut base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
while base_url.ends_with('/') { base_url.pop(); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{}/backend-api", base_url); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{base_url}/backend-api"); }
let ua = codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&ua).unwrap_or(reqwest::header::HeaderValue::from_static("codex-cli")));
@@ -905,18 +888,18 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
codex_login::AuthMode::ChatGPT,
"codex_cloud_tasks_tui".to_string(),
);
if let Some(auth) = am.auth() { if let Ok(tok) = auth.get_token().await { if !tok.is_empty() {
let v = format!("Bearer {}", tok);
if let Some(auth) = am.auth() { if let Ok(tok) = auth.get_token().await && !tok.is_empty() {
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) { headers.insert(reqwest::header::AUTHORIZATION, hv); }
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok)) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id") {
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) { headers.insert(name, hv); }
}
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok))
&& let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = reqwest::header::HeaderValue::from_str(&acc) {
headers.insert(name, hv);
}
}}}
}}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
}
@@ -957,7 +940,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
// Build headers (UA + ChatGPT token + account id)
let mut base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
while base_url.ends_with('/') { base_url.pop(); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{}/backend-api", base_url); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{base_url}/backend-api"); }
let ua = codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&ua).unwrap_or(reqwest::header::HeaderValue::from_static("codex-cli")));
@@ -969,7 +952,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
);
if let Some(auth) = am.auth() {
if let Ok(tok) = auth.get_token().await { if !tok.is_empty() {
let v = format!("Bearer {}", tok);
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) { headers.insert(reqwest::header::AUTHORIZATION, hv); }
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok)) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id") {
@@ -980,7 +963,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
KeyCode::Char(ch) if !key.modifiers.contains(KeyModifiers::CONTROL) && !key.modifiers.contains(KeyModifiers::ALT) => {
@@ -1099,7 +1082,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
// Build headers (UA + ChatGPT token + account id)
let mut base_url = std::env::var("CODEX_CLOUD_TASKS_BASE_URL").unwrap_or_else(|_| "https://chatgpt.com/backend-api".to_string());
while base_url.ends_with('/') { base_url.pop(); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{}/backend-api", base_url); }
if (base_url.starts_with("https://chatgpt.com") || base_url.starts_with("https://chat.openai.com")) && !base_url.contains("/backend-api") { base_url = format!("{base_url}/backend-api"); }
let ua = codex_core::default_client::get_codex_user_agent(Some("codex_cloud_tasks_tui"));
let mut headers = reqwest::header::HeaderMap::new();
headers.insert(reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_str(&ua).unwrap_or(reqwest::header::HeaderValue::from_static("codex-cli")));
@@ -1111,7 +1094,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
);
if let Some(auth) = am.auth() {
if let Ok(tok) = auth.get_token().await { if !tok.is_empty() {
let v = format!("Bearer {}", tok);
let v = format!("Bearer {tok}");
if let Ok(hv) = reqwest::header::HeaderValue::from_str(&v) { headers.insert(reqwest::header::AUTHORIZATION, hv); }
if let Some(acc) = auth.get_account_id().or_else(|| extract_chatgpt_account_id(&tok)) {
if let Ok(name) = reqwest::header::HeaderName::from_bytes(b"ChatGPT-Account-Id") {
@@ -1122,7 +1105,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
}
}
let res = crate::env_detect::list_environments(&base_url, &headers).await;
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res.map_err(|e| e.into())));
let _ = tx2.send(app::AppEvent::EnvironmentsLoaded(res));
});
}
}
@@ -1134,7 +1117,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
}
KeyCode::Enter => {
if let Some(task) = app.tasks.get(app.selected).cloned() {
app.status = format!("Loading details for {}", task.title);
app.status = format!("Loading details for {title}", title = task.title);
app.details_inflight = true;
// Open empty overlay immediately; content arrives via events
let mut sd = crate::scrollable_diff::ScrollableDiff::new();
@@ -1157,7 +1140,7 @@ pub async fn run_main(_cli: Cli, _codex_linux_sandbox_exe: Option<PathBuf>) -> a
let _ = tx2.send(app::AppEvent::DetailsMessagesLoaded { id: task.id, title: task.title, messages: msgs });
}
Err(e2) => {
let _ = tx2.send(app::AppEvent::DetailsFailed { id: task.id, title: task.title, error: format!("{}", e2) });
let _ = tx2.send(app::AppEvent::DetailsFailed { id: task.id, title: task.title, error: format!("{e2}") });
}
}
}
@@ -1295,11 +1278,11 @@ fn pretty_lines_from_error(raw: &str) -> Vec<String> {
} else {
format!("{code}: {msg}")
};
lines.push(format!("Assistant error: {}", summary));
lines.push(format!("Assistant error: {summary}"));
}
}
if let Some(status) = t.get("turn_status").and_then(|s| s.as_str()) {
lines.push(format!("Status: {}", status));
lines.push(format!("Status: {status}"));
}
if let Some(text) = t
.get("latest_event")

View File

@@ -95,7 +95,6 @@ fn overlay_content(area: Rect) -> Rect {
}
pub fn draw_new_task_page(frame: &mut Frame, area: Rect, app: &mut App) {
use ratatui::widgets::Wrap;
let title_spans = {
let mut spans: Vec<ratatui::text::Span> = vec!["New Task".magenta().bold()];
@@ -153,7 +152,7 @@ pub fn draw_new_task_page(frame: &mut Frame, area: Rect, app: &mut App) {
// Place cursor where composer wants it
if let Some(page) = app.new_task.as_ref() {
if let Some((x, y)) = page.composer.cursor_pos(composer_area) {
frame.set_cursor(x, y);
frame.set_cursor_position((x, y));
}
}
}
@@ -173,7 +172,7 @@ fn draw_list(frame: &mut Frame, area: Rect, app: &mut App) {
.find(|r| &r.id == id)
.and_then(|r| r.label.clone())
.unwrap_or_else(|| "Selected".to_string());
format!("{}", label).dim()
format!("{label}").dim()
} else {
" • All".dim()
};
@@ -182,7 +181,7 @@ fn draw_list(frame: &mut Frame, area: Rect, app: &mut App) {
" • 0%".dim()
} else {
let p = ((app.selected as f32) / ((app.tasks.len() - 1) as f32) * 100.0).round() as i32;
format!("{}%", p.clamp(0, 100)).dim()
format!("{}%", p.clamp(0, 100)).dim()
};
let title_line = {
let base = Line::from(vec!["Cloud Tasks".into(), suffix_span, percent_span]);
@@ -270,7 +269,7 @@ fn draw_footer(frame: &mut Frame, area: Rect, app: &mut App) {
if status_line.len() > 2000 {
// hard cap to avoid TUI noise
status_line.truncate(2000);
status_line.push_str("");
status_line.push('…');
}
// Clear the status row to avoid trailing characters when the message shrinks.
frame.render_widget(Clear, rows[1]);
@@ -332,7 +331,7 @@ fn draw_diff_overlay(frame: &mut Frame, area: Rect, app: &mut App) {
};
if let Some(p) = pct_opt {
title_spans.push("".dim());
title_spans.push(format!("{}%", p).dim());
title_spans.push(format!("{p}%").dim());
}
let block = overlay_block().title(Line::from(title_spans));
frame.render_widget(Clear, inner);
@@ -428,7 +427,7 @@ pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) {
let mut body_lines: Vec<Line> = Vec::new();
let first = match m.result_level {
Some(crate::app::ApplyResultLevel::Success) => msg.clone().green(),
Some(crate::app::ApplyResultLevel::Partial) => msg.clone().yellow(),
Some(crate::app::ApplyResultLevel::Partial) => msg.clone().magenta(),
Some(crate::app::ApplyResultLevel::Error) => msg.clone().red(),
None => msg.clone().into(),
};
@@ -453,7 +452,7 @@ pub fn draw_apply_modal(frame: &mut Frame, area: Rect, app: &mut App) {
body_lines.push(Line::from(""));
body_lines.push(
Line::from(format!("Skipped ({}):", m.skipped_paths.len()))
.yellow()
.magenta()
.bold(),
);
for p in &m.skipped_paths {
@@ -504,7 +503,7 @@ fn style_diff_line(raw: &str) -> Line<'static> {
Line::from(vec![Span::raw(raw.to_string())])
}
fn render_task_item(app: &App, t: &codex_cloud_tasks_api::TaskSummary) -> ListItem<'static> {
fn render_task_item(_app: &App, t: &codex_cloud_tasks_api::TaskSummary) -> ListItem<'static> {
let status = match t.status {
TaskStatus::Ready => "READY".green(),
TaskStatus::Pending => "PENDING".magenta(),
@@ -543,13 +542,13 @@ fn render_task_item(app: &App, t: &codex_cloud_tasks_api::TaskSummary) -> ListIt
let dels = t.summary.lines_removed;
let files = t.summary.files_changed;
Line::from(vec![
format!("+{}", adds).green(),
format!("+{adds}").green(),
"/".into(),
format!("{}", dels).red(),
format!("{dels}").red(),
" ".into(),
"".dim(),
" ".into(),
format!("{}", files).into(),
format!("{files}").into(),
" ".into(),
"files".dim(),
])
@@ -569,15 +568,15 @@ fn format_relative_time(ts: chrono::DateTime<Utc>) -> String {
secs = 0;
}
if secs < 60 {
return format!("{}s ago", secs);
return format!("{secs}s ago");
}
let mins = secs / 60;
if mins < 60 {
return format!("{}m ago", mins);
return format!("{mins}m ago");
}
let hours = mins / 60;
if hours < 24 {
return format!("{}h ago", hours);
return format!("{hours}h ago");
}
let local = ts.with_timezone(&Local);
local.format("%b %e %H:%M").to_string()
@@ -689,7 +688,7 @@ pub fn draw_env_modal(frame: &mut Frame, area: Rect, app: &mut App) {
.map(|m| m.query.clone())
.unwrap_or_default();
let ql = query.to_lowercase();
let search = Paragraph::new(format!("Search: {}", query)).wrap(Wrap { trim: true });
let search = Paragraph::new(format!("Search: {query}")).wrap(Wrap { trim: true });
frame.render_widget(search, rows[1]);
// Filter environments by query (case-insensitive substring over label/id/hints)