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
+7 -8
View File
@@ -36,7 +36,7 @@ impl Client {
|| 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 http = reqwest::Client::builder().build()?;
let path_style = if base_url.contains("/backend-api") {
@@ -84,17 +84,16 @@ impl Client {
h.insert(USER_AGENT, HeaderValue::from_static("codex-cli"));
}
if let Some(token) = &self.bearer_token {
let value = format!("Bearer {}", token);
let value = format!("Bearer {token}");
if let Ok(hv) = HeaderValue::from_str(&value) {
h.insert(AUTHORIZATION, hv);
}
}
if let Some(acc) = &self.chatgpt_account_id {
if let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id") {
if let Ok(hv) = HeaderValue::from_str(acc) {
h.insert(name, hv);
}
}
if let Some(acc) = &self.chatgpt_account_id
&& let Ok(name) = HeaderName::from_bytes(b"ChatGPT-Account-Id")
&& let Ok(hv) = HeaderValue::from_str(acc)
{
h.insert(name, hv);
}
// Optional internal toggle: send WHAM-FORCE-INTERNAL header when requested.
// if matches!(
+38 -41
View File
@@ -62,15 +62,14 @@ impl CodeTaskDetailsResponseExt for CodeTaskDetailsResponse {
.and_then(|v| v.as_array());
if let Some(items) = items {
for item in items {
if item.get("type").and_then(Value::as_str) == Some("message") {
if let Some(content) = item.get("content").and_then(Value::as_array) {
for part in content {
if part.get("content_type").and_then(Value::as_str) == Some("text")
{
if let Some(txt) = part.get("text").and_then(Value::as_str) {
out.push(txt.to_string());
}
}
if item.get("type").and_then(Value::as_str) == Some("message")
&& let Some(content) = item.get("content").and_then(Value::as_array)
{
for part in content {
if part.get("content_type").and_then(Value::as_str) == Some("text")
&& let Some(txt) = part.get("text").and_then(Value::as_str)
{
out.push(txt.to_string());
}
}
}
@@ -88,7 +87,7 @@ impl CodeTaskDetailsResponseExt for CodeTaskDetailsResponse {
if message.is_empty() && code.is_empty() {
None
} else if message.is_empty() {
Some(format!("{code}"))
Some(code.to_string())
} else if code.is_empty() {
Some(message.to_string())
} else {
@@ -111,31 +110,30 @@ impl CodeTaskDetailsResponseExt for CodeTaskDetailsResponse {
}
if let Some(od) = obj.get("output_diff").and_then(Value::as_object) {
if let Some(fm) = od.get("files_modified") {
if let Some(map) = fm.as_object() {
if map.len() == 1 {
if let Some((k, _)) = map.iter().next() {
let p = k.to_string();
return Some((p.clone(), p));
}
if let Some(map) = fm.as_object()
&& map.len() == 1
&& let Some((k, _)) = map.iter().next()
{
let p = k.to_string();
return Some((p.clone(), p));
} else if let Some(arr) = fm.as_array()
&& arr.len() == 1
{
let el = &arr[0];
if let Some(p) = el.as_str() {
let p = p.to_string();
return Some((p.clone(), p));
}
} else if let Some(arr) = fm.as_array() {
if arr.len() == 1 {
let el = &arr[0];
if let Some(p) = el.as_str() {
if let Some(o) = el.as_object() {
let path = o.get("path").and_then(Value::as_str);
let oldp = o.get("old_path").and_then(Value::as_str);
let newp = o.get("new_path").and_then(Value::as_str);
if let Some(p) = path {
let p = p.to_string();
return Some((p.clone(), p));
}
if let Some(o) = el.as_object() {
let path = o.get("path").and_then(Value::as_str);
let oldp = o.get("old_path").and_then(Value::as_str);
let newp = o.get("new_path").and_then(Value::as_str);
if let Some(p) = path {
let p = p.to_string();
return Some((p.clone(), p));
}
if let (Some(o1), Some(n1)) = (oldp, newp) {
return Some((o1.to_string(), n1.to_string()));
}
if let (Some(o1), Some(n1)) = (oldp, newp) {
return Some((o1.to_string(), n1.to_string()));
}
}
}
@@ -152,12 +150,11 @@ impl CodeTaskDetailsResponseExt for CodeTaskDetailsResponse {
let candidates: [&Option<std::collections::HashMap<String, serde_json::Value>>; 2] =
[&self.current_diff_task_turn, &self.current_assistant_turn];
for map in candidates {
if let Some(m) = map.as_ref() {
if let Some(items) = m.get("output_items").and_then(serde_json::Value::as_array) {
if let Some(p) = try_from_items(items) {
return Some(p);
}
}
if let Some(m) = map.as_ref()
&& let Some(items) = m.get("output_items").and_then(serde_json::Value::as_array)
&& let Some(p) = try_from_items(items)
{
return Some(p);
}
}
None
@@ -223,10 +220,10 @@ pub fn extract_file_paths_list(details: &CodeTaskDetailsResponse) -> Vec<(String
&details.current_assistant_turn,
];
for map in candidates {
if let Some(m) = map.as_ref() {
if let Some(items) = m.get("output_items").and_then(Value::as_array) {
push_from_items(&mut out, items);
}
if let Some(m) = map.as_ref()
&& let Some(items) = m.get("output_items").and_then(Value::as_array)
{
push_from_items(&mut out, items);
}
}
out
+1 -1
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(),
+1 -1
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}");
+2 -2
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);
}
+7 -7
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() {
+24 -37
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) => {
+75 -92
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")
+15 -16
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)
+13 -2
View File
@@ -30,10 +30,11 @@ pub struct ServerOptions {
pub port: u16,
pub open_browser: bool,
pub force_state: Option<String>,
pub originator: String,
}
impl ServerOptions {
pub fn new(codex_home: PathBuf, client_id: String) -> Self {
pub fn new(codex_home: PathBuf, client_id: String, originator: String) -> Self {
Self {
codex_home,
client_id: client_id.to_string(),
@@ -41,6 +42,7 @@ impl ServerOptions {
port: DEFAULT_PORT,
open_browser: true,
force_state: None,
originator,
}
}
}
@@ -96,7 +98,14 @@ pub fn run_login_server(opts: ServerOptions) -> io::Result<LoginServer> {
let server = Arc::new(server);
let redirect_uri = format!("http://localhost:{actual_port}/auth/callback");
let auth_url = build_authorize_url(&opts.issuer, &opts.client_id, &redirect_uri, &pkce, &state);
let auth_url = build_authorize_url(
&opts.issuer,
&opts.client_id,
&redirect_uri,
&pkce,
&state,
&opts.originator,
);
if opts.open_browser {
let _ = webbrowser::open(&auth_url);
@@ -279,6 +288,7 @@ fn build_authorize_url(
redirect_uri: &str,
pkce: &PkceCodes,
state: &str,
originator: &str,
) -> String {
let query = vec![
("response_type", "code"),
@@ -290,6 +300,7 @@ fn build_authorize_url(
("id_token_add_organizations", "true"),
("codex_cli_simplified_flow", "true"),
("state", state),
("originator", originator),
];
let qs = query
.into_iter()
@@ -100,6 +100,7 @@ async fn end_to_end_login_flow_persists_auth_json() {
port: 0,
open_browser: false,
force_state: Some(state),
originator: "test_originator".to_string(),
};
let server = run_login_server(opts).unwrap();
let login_port = server.actual_port;
@@ -158,6 +159,7 @@ async fn creates_missing_codex_home_dir() {
port: 0,
open_browser: false,
force_state: Some(state),
originator: "test_originator".to_string(),
};
let server = run_login_server(opts).unwrap();
let login_port = server.actual_port;