merge main into code-branch-20250918-072155 before merge

This commit is contained in:
James Peter
2025-09-18 21:05:50 +10:00
26 changed files with 356 additions and 245 deletions
+7
View File
@@ -3,6 +3,13 @@
> [!TIP]
> We're constantly improving Code! This page documents the core changes. You can also check our [releases page](https://github.com/just-every/code/releases) for additional information.
## [0.2.155] - 2025-09-18
- Auth: fix onboarding auth prompt gating. (87a76d25)
- CLI: add long-run calculator script. (b01e2b38)
- TUI: add pulldown-cmark dependency to fix build. (f1718b03)
- Docs: clarify config directories. (cc22fbd9)
## [0.2.154] - 2025-09-18
- TUI/Input: fix Shift+Tab crash. (354a6faa)
+9 -6
View File
@@ -20,7 +20,7 @@
- 🧠 **Reasoning Control** - /reasoning for dynamic effort adjustment
- 🔌 **MCP support** – Extend with filesystem, DBs, APIs, or your own tools.
- 🔒 **Safety modes** – Read-only, approvals, and workspace sandboxing.
- 🔁 **Backwards compatible** – Supports `~/.codex/*` or default `~/.coder/*`
- 🔁 **Backwards compatible** – Reads both `~/.code/*` (primary) and legacy `~/.codex/*`; writes only to `~/.code/*`
 
| <img src="docs/screenshots/simple.png" alt="Simple interface" width="100%"><br>Simple interface | <img src="docs/screenshots/diff.png" alt="Unified diff viewer" width="100%"><br>Unified diffs |
@@ -51,7 +51,7 @@ Note: If another tool already provides a `code` command (e.g. VS Code), our CLI
**Authenticate** (one of the following):
- **Sign in with ChatGPT** (Plus/Pro/Team; uses models available to your plan)
- Run `code` and pick "Sign in with ChatGPT"
- Stores creds locally at `~/.coder/auth.json` (also reads legacy `~/.codex/auth.json`)
- Stores creds locally at `~/.code/auth.json` (still reads legacy `~/.codex/auth.json` if present)
- **API key** (usage-based)
- Set `export OPENAI_API_KEY=xyz` and run `code`
@@ -173,7 +173,7 @@ Code supports MCP for extended capabilities:
- **API integrations**: Connect to external services
- **Custom tools**: Build your own extensions
Configure MCP in `~/.codex/config.toml`. Define each server under a named table like `[mcp_servers.<name>]` (this maps to the JSON `mcpServers` object used by other clients):
Configure MCP in `~/.code/config.toml` (legacy `~/.codex/config.toml` is still read if present). Define each server under a named table like `[mcp_servers.<name>]` (this maps to the JSON `mcpServers` object used by other clients):
```toml
[mcp_servers.filesystem]
@@ -184,7 +184,10 @@ args = ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/project"]
&ensp;
## Configuration
Main config file: `~/.codex/config.toml`
Main config file: `~/.code/config.toml`
> [!NOTE]
> Code reads from both `~/.code/` and `~/.codex/` for backwards compatibility, but it only writes updates to `~/.code/`. If you switch back to Codex and it fails to start, remove `~/.codex/config.toml`. If Code appears to miss settings after upgrading, copy your legacy `~/.codex/config.toml` into `~/.code/`.
```toml
# Model settings
@@ -222,7 +225,7 @@ model_reasoning_summary = "detailed"
> This fork adds browser integration, multi-agent commands (`/plan`, `/solve`, `/code`), theme system, and enhanced reasoning controls while maintaining full compatibility.
**Can I use my existing Codex configuration?**
> Yes! This fork is fully backward compatible with existing `~/.codex/` configurations.
> Yes. Code reads from both `~/.code/` (primary) and legacy `~/.codex/` directories. We only write to `~/.code/`, so Codex will keep running if you switch back; copy or remove legacy files if you notice conflicts.
**Does this work with ChatGPT Plus?**
> Absolutely. Use the same "Sign in with ChatGPT" flow as the original.
@@ -275,7 +278,7 @@ Using OpenAI, Anthropic or Google services through Code means you agree to **the
- If you configure other model providers, you're responsible for their terms.
### Privacy
- Your auth file lives at `~/.codex/auth.json`.
- Your auth file lives at `~/.code/auth.json` (legacy `~/.codex/auth.json` is still read).
- Inputs/outputs you send to AI providers are handled under their Terms and Privacy Policy; consult those documents (and any org-level data-sharing settings).
### Subject to change
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@just-every/code",
"version": "0.2.154",
"version": "0.2.155",
"license": "Apache-2.0",
"description": "Lightweight coding agent that runs in your terminal - fork of OpenAI Codex",
"bin": {
@@ -35,10 +35,10 @@
"prettier": "^3.3.3"
},
"optionalDependencies": {
"@just-every/code-darwin-arm64": "0.2.154",
"@just-every/code-darwin-x64": "0.2.154",
"@just-every/code-linux-x64-musl": "0.2.154",
"@just-every/code-linux-arm64-musl": "0.2.154",
"@just-every/code-win32-x64": "0.2.154"
"@just-every/code-darwin-arm64": "0.2.155",
"@just-every/code-darwin-x64": "0.2.155",
"@just-every/code-linux-x64-musl": "0.2.155",
"@just-every/code-linux-arm64-musl": "0.2.155",
"@just-every/code-win32-x64": "0.2.155"
}
}
+1 -2
View File
@@ -90,7 +90,7 @@ codex --sandbox workspace-write
codex --sandbox danger-full-access
```
The same setting can be persisted in `~/.codex/config.toml` via the top-level `sandbox_mode = "MODE"` key, e.g. `sandbox_mode = "workspace-write"`.
The same setting can be persisted in `~/.code/config.toml` via the top-level `sandbox_mode = "MODE"` key (Code will also read legacy `~/.codex/config.toml`), e.g. `sandbox_mode = "workspace-write"`.
If you want to prevent the agent from updating Git metadata (e.g., local safety), you can opt‑out with a workspace‑write tweak:
@@ -125,4 +125,3 @@ This folder is the root of a Cargo workspace. It contains quite a bit of experim
- [`exec/`](./exec) "headless" CLI for use in automation.
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.
+1 -1
View File
@@ -107,7 +107,7 @@ where
const ILLEGAL_ENV_VAR_PREFIX: &str = "CODEX_";
/// Load env vars from ~/.codex/.env and `$(pwd)/.env`.
/// Load env vars from ~/.code/.env (legacy ~/.codex/.env is still read) and `$(pwd)/.env`.
///
/// Security: Do not allow `.env` files to create or modify any variables
/// with names starting with `CODEX_`.
+4 -2
View File
@@ -174,9 +174,11 @@ struct GenerateTsCommand {
#[derive(Debug, Parser)]
struct OrderReplayArgs {
/// Path to a response.json captured under ~/.codex/debug_logs/*_response.json
/// Path to a response.json captured under ~/.code/debug_logs/*_response.json
/// (legacy ~/.codex/debug_logs/ is still read).
response_json: std::path::PathBuf,
/// Path to codex-tui.log (typically ~/.codex/log/codex-tui.log)
/// Path to codex-tui.log (typically ~/.code/log/codex-tui.log; legacy
/// ~/.codex/log/codex-tui.log is still read).
tui_log: std::path::PathBuf,
}
+1 -1
View File
@@ -20,7 +20,7 @@ use codex_core::config_types::McpServerConfig;
/// - `serve` — run the MCP server on stdio
/// - `list` — list configured servers (with `--json`)
/// - `get` — show a single server (with `--json`)
/// - `add` — add a server launcher entry to `~/.codex/config.toml`
/// - `add` — add a server launcher entry to `~/.code/config.toml` (Code also reads legacy `~/.codex/config.toml`)
/// - `remove` — delete a server entry
#[derive(Debug, clap::Parser)]
pub struct McpCli {
+1 -1
View File
@@ -18,7 +18,7 @@ use toml::Value;
#[derive(Parser, Debug, Default, Clone)]
pub struct CliConfigOverrides {
/// Override a configuration value that would otherwise be loaded from
/// `~/.codex/config.toml`. Use a dotted path (`foo.bar.baz`) to override
/// `~/.code/config.toml` (Code also reads legacy `~/.codex/config.toml`). Use a dotted path (`foo.bar.baz`) to override
/// nested values. The `value` portion is parsed as JSON. If it fails to
/// parse as JSON, the raw string is used as a literal.
///
+3 -3
View File
@@ -8,7 +8,7 @@ This file has moved. Please see the latest configuration documentation here:
- Values can contain objects, such as `--config shell_environment_policy.include_only=["PATH", "HOME", "USER"]`.
- For consistency with `config.toml`, values are in TOML format rather than JSON format, so use `{a = 1, b = 2}` rather than `{"a": 1, "b": 2}`.
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that both `-c model="o3"` and `-c model=o3` are equivalent.
- The `$CODEX_HOME/config.toml` configuration file where the `CODEX_HOME` environment value defaults to `~/.codex`. (Note `CODEX_HOME` will also be where logs and other Codex-related information are stored.)
- The `$CODE_HOME/config.toml` configuration file. `CODE_HOME` defaults to `~/.code`; Code also reads from `$CODEX_HOME`/`~/.codex` for backwards compatibility but only writes to `~/.code`. (Logs and other state use the same directory.)
Both the `--config` flag and the `config.toml` file support the following options:
@@ -369,7 +369,7 @@ This config option is comparable to how Claude and Cursor define `mcpServers` in
}
```
Should be represented as follows in `~/.codex/config.toml`:
Should be represented as follows in `~/.code/config.toml` (Code will also read the legacy `~/.codex/config.toml` if it exists):
```toml
# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`.
@@ -500,7 +500,7 @@ if __name__ == "__main__":
sys.exit(main())
```
To have Codex use this script for notifications, you would configure it via `notify` in `~/.codex/config.toml` using the appropriate path to `notify.py` on your computer:
To have Codex use this script for notifications, you would configure it via `notify` in `~/.code/config.toml` (legacy `~/.codex/config.toml` is still read) using the appropriate path to `notify.py` on your computer:
```toml
notify = ["python3", "/Users/mbolin/.codex/notify.py"]
+265 -176
View File
@@ -17,6 +17,8 @@ use base64::Engine;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::MaybeApplyPatchVerified;
use codex_apply_patch::maybe_parse_apply_patch_verified;
use codex_browser::BrowserConfig as CodexBrowserConfig;
use codex_browser::BrowserManager;
// unused: AuthManager
// unused: ConversationHistoryResponseEvent
use codex_protocol::protocol::TurnAbortReason;
@@ -3381,6 +3383,193 @@ async fn handle_web_fetch(sess: &Session, ctx: &ToolCallCtx, arguments: String)
}
};
struct BrowserFetchOutcome {
html: String,
final_url: Option<String>,
headless: bool,
}
async fn fetch_html_via_headless_browser(
url: &str,
timeout: Duration,
) -> Result<BrowserFetchOutcome, String> {
let mut config = CodexBrowserConfig::default();
config.enabled = true;
config.headless = true;
config.fullpage = false;
config.segments_max = 2;
config.persist_profile = false;
config.idle_timeout_ms = 10_000;
let manager = BrowserManager::new(config);
manager.set_enabled_sync(true);
const CHECK_JS: &str = r#"(function(){
const discuss = document.querySelectorAll('[data-test-selector=\"issue-comment-body\"]');
const timeline = document.querySelectorAll('.js-timeline-item');
const article = document.querySelectorAll('article, main');
return (discuss.length + timeline.length + article.length);
})()"#;
const HTML_JS: &str =
"(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()";
let goto_result = match tokio::time::timeout(timeout, manager.goto(url)).await {
Ok(Ok(res)) => res,
Ok(Err(e)) => {
let _ = manager.stop().await;
return Err(format!("Headless goto failed: {e}"));
}
Err(_) => {
let _ = manager.stop().await;
return Err("Headless goto timed out".to_string());
}
};
for _ in 0..6 {
match tokio::time::timeout(Duration::from_millis(1500), manager.execute_javascript(CHECK_JS)).await {
Ok(Ok(val)) => {
let count = val
.get("value")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if count > 0 {
break;
}
}
Ok(Err(e)) => {
tracing::debug!("Headless readiness check failed: {}", e);
break;
}
Err(_) => {
tracing::debug!("Headless readiness check timed out");
break;
}
}
tokio::time::sleep(Duration::from_millis(800)).await;
}
let html_value = match tokio::time::timeout(timeout, manager.execute_javascript(HTML_JS)).await {
Ok(Ok(val)) => val,
Ok(Err(e)) => {
let _ = manager.stop().await;
return Err(format!("Headless HTML extraction failed: {e}"));
}
Err(_) => {
let _ = manager.stop().await;
return Err("Headless HTML extraction timed out".to_string());
}
};
let html = html_value
.get("value")
.and_then(|v| v.get("html"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if html.trim().is_empty() {
let _ = manager.stop().await;
return Err("Headless browser returned empty HTML".to_string());
}
let final_url = Some(goto_result.url.clone());
let _ = manager.stop().await;
Ok(BrowserFetchOutcome {
html,
final_url,
headless: true,
})
}
async fn fetch_html_via_browser(
url: &str,
timeout: Duration,
prefer_global: bool,
) -> Option<BrowserFetchOutcome> {
const HTML_JS: &str =
"(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()";
const CHECK_JS: &str = r#"(function(){
const discuss = document.querySelectorAll('[data-test-selector=\"issue-comment-body\"]');
const timeline = document.querySelectorAll('.js-timeline-item');
const article = document.querySelectorAll('article, main');
return (discuss.length + timeline.length + article.length);
})()"#;
if prefer_global {
if let Some(manager) = codex_browser::global::get_browser_manager().await {
if manager.is_enabled_sync() {
match tokio::time::timeout(timeout, manager.goto(url)).await {
Ok(Ok(res)) => {
for _ in 0..6 {
match tokio::time::timeout(Duration::from_millis(1500), manager.execute_javascript(CHECK_JS)).await {
Ok(Ok(val)) => {
let count = val
.get("value")
.and_then(|v| v.as_i64())
.unwrap_or(0);
if count > 0 {
break;
}
}
Ok(Err(e)) => {
tracing::debug!("Global browser readiness check failed: {}", e);
break;
}
Err(_) => {
tracing::debug!("Global browser readiness timed out");
break;
}
}
tokio::time::sleep(Duration::from_millis(800)).await;
}
match tokio::time::timeout(timeout, manager.execute_javascript(HTML_JS)).await {
Ok(Ok(val)) => {
if let Some(html) = val
.get("value")
.and_then(|v| v.get("html"))
.and_then(|v| v.as_str())
{
if !html.trim().is_empty() {
return Some(BrowserFetchOutcome {
html: html.to_string(),
final_url: Some(res.url.clone()),
headless: false,
});
}
}
}
Ok(Err(e)) => {
tracing::debug!("Global browser HTML extraction failed: {}", e);
}
Err(_) => {
tracing::debug!("Global browser HTML extraction timed out");
}
}
}
Ok(Err(e)) => {
tracing::warn!("Global browser navigation failed: {}", e);
}
Err(_) => {
tracing::warn!("Global browser navigation timed out");
}
}
} else {
tracing::debug!("Global browser manager disabled; skipping UI fetch");
}
}
}
match fetch_html_via_headless_browser(url, timeout).await {
Ok(outcome) => Some(outcome),
Err(err) => {
tracing::warn!("Headless browser fallback failed for {}: {}", url, err);
None
}
}
}
// Helper: build a client with a specific UA and common headers.
async fn do_request(
url: &str,
@@ -3830,89 +4019,33 @@ async fn handle_web_fetch(sess: &Session, ctx: &ToolCallCtx, arguments: String)
let timeout = Duration::from_millis(params.timeout_ms.unwrap_or(15000));
let codex_ua = crate::default_client::get_codex_user_agent(Some("web_fetch"));
// Heuristic: some domains render key content client-side. Prefer a
// quick browser render first so we capture comments/timelines.
let is_dynamic_domain = {
let u = params.url.to_lowercase();
u.contains("github.com/")
};
if !matches!(params.mode.as_deref(), Some("http")) && is_dynamic_domain {
let browser_manager = codex_browser::global::get_or_create_browser_manager().await;
if let Ok(res) = browser_manager.goto(&params.url).await {
// Poll briefly for discussion/timeline elements to appear
for _ in 0..6 {
let js = r#"(function(){ const sel1 = document.querySelectorAll('[data-test-selector=\"issue-comment-body\"]'); const sel2 = document.querySelectorAll('.js-timeline-item'); return (sel1.length + sel2.length); })()"#;
if let Ok(val) = browser_manager.execute_javascript(js).await {
let n = val.get("value").and_then(|v| v.as_i64()).unwrap_or(0);
if n > 0 { break; }
}
tokio::time::sleep(Duration::from_millis(800)).await;
}
if let Ok(val) = browser_manager.execute_javascript(r#"(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()"#).await {
if let Some(html) = val.get("value").and_then(|v| v.get("html")).and_then(|v| v.as_str()) {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(html.to_string(), 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) } };
}
};
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": res.url,
"content_type": "text/html",
"used_browser_ua": true,
"via_browser": true,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) } };
}
}
}
}
// If explicit browser mode requested, try the internal browser first.
if matches!(params.mode.as_deref(), Some("browser")) {
{
let browser_manager = codex_browser::global::get_or_create_browser_manager().await;
if let Ok(res) = browser_manager.goto(&params.url).await {
// Allow a few short settles for JS/cookie challenges to auto-resolve
let mut html: Option<String> = None;
for _ in 0..4 {
if let Ok(val) = browser_manager.execute_javascript("(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()").await {
html = val.get("value").and_then(|v| v.get("html")).and_then(|v| v.as_str()).map(|s| s.to_string());
let t_low = val.get("value").and_then(|v| v.get("title")).and_then(|v| v.as_str()).unwrap_or("").to_lowercase();
if !(t_low.contains("just a moment") || t_low.contains("checking if") || t_low.contains("waiting for")) {
break;
}
}
tokio::time::sleep(Duration::from_millis(1200)).await;
}
if let Some(html) = html {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
}
if let Some(browser_fetch) = fetch_html_via_browser(&params.url, timeout, true).await {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(browser_fetch.html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": res.url,
"content_type": "text/html",
"used_browser_ua": true,
"via_browser": true,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) } };
}
}
};
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": browser_fetch.final_url.unwrap_or_else(|| params.url.clone()),
"content_type": "text/html",
"used_browser_ua": true,
"via_browser": true,
"headless": browser_fetch.headless,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) },
};
}
}
// Attempt 1: Codex UA + polite headers
@@ -3990,71 +4123,37 @@ async fn handle_web_fetch(sess: &Session, ctx: &ToolCallCtx, arguments: String)
if let Some(ra) = retry_after { diag["retry_after"] = serde_json::json!(ra); }
if let Some(ray) = cf_ray { diag["cf_ray"] = serde_json::json!(ray); }
// Attempt a last-resort browser-based fetch if the live browser is available.
{
let browser_manager = codex_browser::global::get_or_create_browser_manager().await;
browser_manager.set_enabled_sync(true);
// Try navigate and extract outerHTML via JS (after SPA settles per manager config)
if browser_manager.goto(&params.url).await.is_ok() {
let js = "(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()";
if let Ok(val) = browser_manager.execute_javascript(js).await {
let html = val.get("value").and_then(|v| v.get("html")).and_then(|v| v.as_str()).unwrap_or("").to_string();
let title = val.get("value").and_then(|v| v.get("title")).and_then(|v| v.as_str()).unwrap_or("");
if !html.is_empty() && title.to_lowercase() != "just a moment..." {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
}
};
diag["via_browser"] = serde_json::json!(true);
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": final_url,
"content_type": content_type,
"used_browser_ua": used_browser_ua,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) } };
}
if let Some(browser_fetch) = fetch_html_via_browser(&params.url, timeout, false).await {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(browser_fetch.html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
}
};
// If JS extraction failed, try a CDP outerHTML of root
let root = browser_manager.execute_cdp("DOM.getDocument", json!({"depth": 1})).await.ok();
if let Some(root) = root {
if let Some(node_id) = root.get("root").and_then(|r| r.get("nodeId")).and_then(|n| n.as_u64()) {
if let Ok(outer) = browser_manager.execute_cdp("DOM.getOuterHTML", json!({"nodeId": node_id})).await {
if let Some(html) = outer.get("outerHTML").and_then(|v| v.as_str()) {
let (markdown, truncated) = match convert_html_to_markdown_trimmed(html.to_string(), 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
}
};
diag["via_browser"] = serde_json::json!(true);
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": final_url,
"content_type": content_type,
"used_browser_ua": used_browser_ua,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) } };
}
}
}
}
diag["via_browser"] = serde_json::json!(true);
if browser_fetch.headless {
diag["headless"] = serde_json::json!(true);
}
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": browser_fetch.final_url.unwrap_or_else(|| final_url.clone()),
"content_type": content_type,
"used_browser_ua": true,
"via_browser": true,
"headless": browser_fetch.headless,
"truncated": truncated,
"markdown": markdown,
});
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) },
};
}
let (md_preview, _trunc) = match convert_html_to_markdown_trimmed(body_text, 2000) {
@@ -4147,42 +4246,32 @@ async fn handle_web_fetch(sess: &Session, ctx: &ToolCallCtx, arguments: String)
// If the rendered markdown still looks like a challenge page, attempt browser fallback (unless http-only).
if !matches!(params.mode.as_deref(), Some("http")) && looks_like_challenge_markdown(&markdown) {
{
let browser_manager = codex_browser::global::get_or_create_browser_manager().await;
browser_manager.set_enabled_sync(true);
if browser_manager.goto(&params.url).await.is_ok() {
let js = "(function(){ return { html: document.documentElement.outerHTML, title: document.title||'' }; })()";
let mut html: Option<String> = None;
for _ in 0..3 {
if let Ok(val) = browser_manager.execute_javascript(js).await {
html = val.get("value").and_then(|v| v.get("html")).and_then(|v| v.as_str()).map(|s| s.to_string());
let t_low = val.get("value").and_then(|v| v.get("title")).and_then(|v| v.as_str()).unwrap_or("").to_lowercase();
if !(t_low.contains("just a moment") || t_low.contains("checking if") || t_low.contains("waiting for")) {
break;
}
}
tokio::time::sleep(Duration::from_millis(1200)).await;
}
if let Some(html) = html {
let (md2, truncated2) = match convert_html_to_markdown_trimmed(html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) } };
}
if let Some(browser_fetch) = fetch_html_via_browser(&params.url, timeout, false).await {
let (md2, truncated2) = match convert_html_to_markdown_trimmed(browser_fetch.html, 120_000) {
Ok(t) => t,
Err(e) => {
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: format!("Markdown conversion failed: {e}"), success: Some(false) },
};
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": final_url,
"content_type": content_type,
"used_browser_ua": true,
"via_browser": true,
"truncated": truncated2,
"markdown": md2,
});
return ResponseInputItem::FunctionCallOutput { call_id: call_id_clone, output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) } };
}
}
};
let body = serde_json::json!({
"url": params.url,
"status": 200,
"final_url": browser_fetch.final_url.unwrap_or_else(|| final_url.clone()),
"content_type": content_type,
"used_browser_ua": true,
"via_browser": true,
"headless": browser_fetch.headless,
"truncated": truncated2,
"markdown": md2,
});
return ResponseInputItem::FunctionCallOutput {
call_id: call_id_clone,
output: FunctionCallOutputPayload { content: body.to_string(), success: Some(true) },
};
}
// If fallback not possible, return structured error rather than a useless challenge page
+10 -7
View File
@@ -120,7 +120,8 @@ pub struct Config {
/// appends one extra argument containing a JSON payload describing the
/// event.
///
/// Example `~/.codex/config.toml` snippet:
/// Example `~/.code/config.toml` snippet (Code also reads legacy
/// `~/.codex/config.toml`):
///
/// ```toml
/// notify = ["notify-send", "Codex"]
@@ -156,11 +157,12 @@ pub struct Config {
/// Maximum number of bytes to include from an AGENTS.md project doc file.
pub project_doc_max_bytes: usize,
/// Directory containing all Codex state (defaults to `~/.codex` but can be
/// overridden by the `CODEX_HOME` environment variable).
/// Directory containing all Codex state (defaults to `~/.code`; can be
/// overridden by the `CODE_HOME` or `CODEX_HOME` environment variables).
pub codex_home: PathBuf,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
/// Settings that govern if and what will be written to `~/.code/history.jsonl`
/// (Code still reads legacy `~/.codex/history.jsonl`).
pub history: History,
/// Optional URI-based file opener. If set, citations to files in the model
@@ -239,7 +241,7 @@ impl Config {
cli_overrides: Vec<(String, TomlValue)>,
overrides: ConfigOverrides,
) -> std::io::Result<Self> {
// Resolve the directory that stores Codex state (e.g. ~/.codex or the
// Resolve the directory that stores Codex state (e.g. ~/.code or the
// value of $CODEX_HOME) so we can embed it into the resulting
// `Config` instance.
let codex_home = find_codex_home()?;
@@ -1112,7 +1114,7 @@ fn apply_toml_override(root: &mut TomlValue, path: &str, value: TomlValue) {
}
}
/// Base config deserialized from ~/.codex/config.toml.
/// Base config deserialized from ~/.code/config.toml (legacy ~/.codex/config.toml is still read).
#[derive(Deserialize, Debug, Clone, Default)]
pub struct ConfigToml {
/// Optional override of model selection.
@@ -1181,7 +1183,8 @@ pub struct ConfigToml {
#[serde(default)]
pub profiles: HashMap<String, ConfigProfile>,
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
/// Settings that govern if and what will be written to `~/.code/history.jsonl`
/// (Code still reads legacy `~/.codex/history.jsonl`).
#[serde(default)]
pub history: Option<History>,
+2 -1
View File
@@ -248,7 +248,8 @@ impl UriBasedFileOpener {
}
}
/// Settings that govern if and what will be written to `~/.codex/history.jsonl`.
/// Settings that govern if and what will be written to `~/.code/history.jsonl`
/// (Code still reads legacy `~/.codex/history.jsonl`).
#[derive(Deserialize, Debug, Clone, PartialEq, Default)]
pub struct History {
/// If true, history entries will not be written to disk.
+5 -3
View File
@@ -1,6 +1,7 @@
//! Persistence layer for the global, append-only *message history* file.
//!
//! The history is stored at `~/.codex/history.jsonl` with **one JSON object per
//! The history is stored at `~/.code/history.jsonl` (Code still reads legacy
//! `~/.codex/history.jsonl`) with **one JSON object per
//! line** so that it can be efficiently appended to and parsed with standard
//! JSON-Lines tooling. Each record has the following schema:
//!
@@ -35,7 +36,8 @@ use std::os::unix::fs::OpenOptionsExt;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
/// Filename that stores the message history inside `~/.codex`.
/// Filename that stores the message history inside `~/.code` (legacy `~/.codex`
/// is still read).
const HISTORY_FILENAME: &str = "history.jsonl";
const MAX_RETRIES: usize = 10;
@@ -70,7 +72,7 @@ pub(crate) async fn append_entry(text: &str, session_id: &Uuid, config: &Config)
// TODO: check `text` for sensitive patterns
// Resolve `~/.codex/history.jsonl` and ensure the parent directory exists.
// Resolve `~/.code/history.jsonl` and ensure the parent directory exists.
let path = history_filepath(config);
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
+2 -1
View File
@@ -2,7 +2,8 @@
//!
//! Providers can be defined in two places:
//! 1. Built-in defaults compiled into the binary so Codex works out-of-the-box.
//! 2. User-defined entries inside `~/.codex/config.toml` under the `model_providers`
//! 2. User-defined entries inside `~/.code/config.toml` under the `model_providers`
//! table (Code also reads legacy `~/.codex/config.toml`).
//! key. These override or extend the defaults at runtime.
use crate::CodexAuth;
+2 -1
View File
@@ -117,7 +117,8 @@ pub(crate) async fn get_conversation(path: &Path) -> io::Result<String> {
/// Load conversation file paths from disk using directory traversal.
///
/// Directory layout: `~/.codex/sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-<uuid>.jsonl`
/// Directory layout: `~/.code/sessions/YYYY/MM/DD/rollout-YYYY-MM-DDThh-mm-ss-<uuid>.jsonl`
/// (Code still reads legacy `~/.codex/sessions/...`).
/// Returned newest (latest) first.
async fn traverse_directories_for_paths(
root: PathBuf,
+4 -3
View File
@@ -57,8 +57,8 @@ pub struct SavedSession {
/// Rollouts are recorded as JSONL and can be inspected with tools such as:
///
/// ```ignore
/// $ jq -C . ~/.codex/sessions/rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl
/// $ fx ~/.codex/sessions/rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl
/// $ jq -C . ~/.code/sessions/rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl
/// $ fx ~/.code/sessions/rollout-2025-05-07T17-24-21-5973b6c0-94b8-487b-a530-2aeb6098ae0e.jsonl
/// ```
#[derive(Clone)]
pub struct RolloutRecorder {
@@ -358,7 +358,8 @@ fn create_log_file(
config: &Config,
conversation_id: ConversationId,
) -> std::io::Result<LogFileInfo> {
// Resolve ~/.codex/sessions/YYYY/MM/DD and create it if missing.
// Resolve ~/.code/sessions/YYYY/MM/DD and create it if missing (Code still
// reads legacy ~/.codex/sessions/ paths).
let timestamp = OffsetDateTime::now_local()
.map_err(|e| IoError::other(format!("failed to get local time: {e}")))?;
let mut dir = config.codex_home.clone();
+2 -1
View File
@@ -9,7 +9,8 @@ use codex_core::config::ConfigToml;
/// Returns a default `Config` whose on-disk state is confined to the provided
/// temporary directory. Using a per-test directory keeps tests hermetic and
/// avoids clobbering a developer’s real `~/.codex`.
/// avoids clobbering a developer’s real `~/.code` directory (legacy `~/.codex`
/// is still read).
pub fn load_default_config_for_test(codex_home: &TempDir) -> Config {
Config::load_from_base_config_with_overrides(
ConfigToml::default(),
+2 -4
View File
@@ -1752,16 +1752,14 @@ impl App<'_> {
}
fn should_show_onboarding(
_login_status: crate::LoginStatus,
login_status: crate::LoginStatus,
_config: &Config,
show_trust_screen: bool,
) -> bool {
if show_trust_screen {
return true;
}
// Defer login screen visibility decision to onboarding screen logic.
// Here we only gate on trust flow.
false
matches!(login_status, crate::LoginStatus::NotAuthenticated)
}
fn should_show_login_screen(login_status: crate::LoginStatus, _config: &Config) -> bool {
+2 -1
View File
@@ -24,7 +24,8 @@ pub struct ResumeCandidate {
// No fallback scan: meta parsing for rollout headers no longer needed here.
/// Return rollout files under ~/.codex/sessions matching the provided cwd.
/// Return rollout files under ~/.code/sessions matching the provided cwd (Code
/// still reads legacy ~/.codex/sessions directories).
/// Reads only the first line of each file to avoid heavy IO.
pub fn list_sessions_for_cwd(cwd: &Path, codex_home: &Path) -> Vec<ResumeCandidate> {
use std::collections::HashMap;
+3 -3
View File
@@ -50,10 +50,10 @@ Notes:
Because Codex is written in Rust, it honors the `RUST_LOG` environment variable to configure its logging behavior.
The TUI defaults to `RUST_LOG=codex_core=info,codex_tui=info` and log messages are written to `~/.codex/log/codex-tui.log`, so you can leave the following running in a separate terminal to monitor log messages as they are written:
The TUI defaults to `RUST_LOG=codex_core=info,codex_tui=info` and log messages are written to `~/.code/log/codex-tui.log` (Code still reads the legacy `~/.codex/log/` path), so you can leave the following running in a separate terminal to monitor log messages as they are written:
```
tail -F ~/.codex/log/codex-tui.log
tail -F ~/.code/log/codex-tui.log
```
By comparison, the non-interactive mode (`codex exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
@@ -62,7 +62,7 @@ See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env
## Model Context Protocol (MCP)
The Codex CLI can be configured to leverage MCP servers by defining an [`mcp_servers`](./config.md#mcp_servers) section in `~/.codex/config.toml`. It is intended to mirror how tools such as Claude and Cursor define `mcpServers` in their respective JSON config files, though the Codex format is slightly different since it uses TOML rather than JSON, e.g.:
The Codex CLI can be configured to leverage MCP servers by defining an [`mcp_servers`](./config.md#mcp_servers) section in `~/.code/config.toml` (Code will also read a legacy `~/.codex/config.toml`). It is intended to mirror how tools such as Claude and Cursor define `mcpServers` in their respective JSON config files, though the Codex format is slightly different since it uses TOML rather than JSON, e.g.:
```toml
# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`.
+9 -9
View File
@@ -15,7 +15,7 @@ This key must, at minimum, have write access to the Responses API.
If you've used the Codex CLI before with usage-based billing via an API key and want to switch to using your ChatGPT plan, follow these steps:
1. Update the CLI and ensure `codex --version` is `0.20.0` or later
2. Delete `~/.codex/auth.json` (on Windows: `C:\\Users\\USERNAME\\.codex\\auth.json`)
2. Delete `~/.code/auth.json` (and remove the legacy `~/.codex/auth.json` if it exists; on Windows these live under `C:\\Users\\USERNAME\\.code\\auth.json` and `C:\\Users\\USERNAME\\.codex\\auth.json`)
3. Run `codex login` again
## Forcing a specific auth method (advanced)
@@ -25,7 +25,7 @@ You can explicitly choose which authentication Codex should prefer when both are
- To always use your API key (even when ChatGPT auth exists), set:
```toml
# ~/.codex/config.toml
# ~/.code/config.toml (Code also reads legacy ~/.codex/config.toml)
preferred_auth_method = "apikey"
```
@@ -38,7 +38,7 @@ codex --config preferred_auth_method="apikey"
- To prefer ChatGPT auth (default), set:
```toml
# ~/.codex/config.toml
# ~/.code/config.toml (Code also reads legacy ~/.codex/config.toml)
preferred_auth_method = "chatgpt"
```
@@ -71,28 +71,28 @@ Today, the login process entails running a server on `localhost:1455`. If you ar
### Authenticate locally and copy your credentials to the "headless" machine
The easiest solution is likely to run through the `codex login` process on your local machine such that `localhost:1455` _is_ accessible in your web browser. When you complete the authentication process, an `auth.json` file should be available at `$CODEX_HOME/auth.json` (on Mac/Linux, `$CODEX_HOME` defaults to `~/.codex` whereas on Windows, it defaults to `%USERPROFILE%\\.codex`).
The easiest solution is likely to run through the `codex login` process on your local machine such that `localhost:1455` _is_ accessible in your web browser. When you complete the authentication process, an `auth.json` file should be available at `$CODE_HOME/auth.json` (defaults to `~/.code/auth.json`; Code will still read `$CODEX_HOME`/`~/.codex/auth.json` if present).
Because the `auth.json` file is not tied to a specific host, once you complete the authentication flow locally, you can copy the `$CODEX_HOME/auth.json` file to the headless machine and then `codex` should "just work" on that machine. Note to copy a file to a Docker container, you can do:
```shell
# substitute MY_CONTAINER with the name or id of your Docker container:
CONTAINER_HOME=$(docker exec MY_CONTAINER printenv HOME)
docker exec MY_CONTAINER mkdir -p "$CONTAINER_HOME/.codex"
docker cp auth.json MY_CONTAINER:"$CONTAINER_HOME/.codex/auth.json"
docker exec MY_CONTAINER mkdir -p "$CONTAINER_HOME/.code"
docker cp auth.json MY_CONTAINER:"$CONTAINER_HOME/.code/auth.json"
```
whereas if you are `ssh`'d into a remote machine, you likely want to use [`scp`](https://en.wikipedia.org/wiki/Secure_copy_protocol):
```shell
ssh user@remote 'mkdir -p ~/.codex'
scp ~/.codex/auth.json user@remote:~/.codex/auth.json
ssh user@remote 'mkdir -p ~/.code'
scp ~/.code/auth.json user@remote:~/.code/auth.json
```
or try this one-liner:
```shell
ssh user@remote 'mkdir -p ~/.codex && cat > ~/.codex/auth.json' < ~/.codex/auth.json
ssh user@remote 'mkdir -p ~/.code && cat > ~/.code/auth.json' < ~/.code/auth.json
```
### Connecting through VPS or remote
+3 -3
View File
@@ -12,7 +12,7 @@ Codex supports several mechanisms for setting config values:
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that `-c model='"o3"'` and `-c model=o3` are equivalent.
- In the first case, the value is the TOML string `"o3"`, while in the second the value is `o3`, which is not valid TOML and therefore treated as the TOML string `"o3"`.
- Because quotes are interpreted by one's shell, `-c key="true"` will be correctly interpreted in TOML as `key = true` (a boolean) and not `key = "true"` (a string). If for some reason you needed the string `"true"`, you would need to use `-c key='"true"'` (note the two sets of quotes).
- The `$CODEX_HOME/config.toml` configuration file where the `CODEX_HOME` environment value defaults to `~/.codex`. (Note `CODEX_HOME` will also be where logs and other Codex-related information are stored.)
- The `$CODE_HOME/config.toml` configuration file. `CODE_HOME` defaults to `~/.code`; Code also reads from `$CODEX_HOME`/`~/.codex` for backwards compatibility but only writes to `~/.code`. (Logs and other state use the same directory.)
Both the `--config` flag and the `config.toml` file support the following options:
@@ -365,7 +365,7 @@ This config option is comparable to how Claude and Cursor define `mcpServers` in
}
```
Should be represented as follows in `~/.codex/config.toml`:
Should be represented as follows in `~/.code/config.toml` (Code will also read the legacy `~/.codex/config.toml` if it exists):
```toml
# IMPORTANT: the top-level key is `mcp_servers` rather than `mcpServers`.
@@ -518,7 +518,7 @@ if __name__ == "__main__":
sys.exit(main())
```
To have Codex use this script for notifications, you would configure it via `notify` in `~/.codex/config.toml` using the appropriate path to `notify.py` on your computer:
To have Codex use this script for notifications, you would configure it via `notify` in `~/.code/config.toml` (legacy `~/.codex/config.toml` is still read) using the appropriate path to `notify.py` on your computer:
```toml
notify = ["python3", "/Users/mbolin/.codex/notify.py"]
+1 -1
View File
@@ -44,7 +44,7 @@ Below are a few bite-size examples you can copy-paste. Replace the text in quote
You can give Codex extra instructions and guidance using `AGENTS.md` files. Codex looks for `AGENTS.md` files in the following places, and merges them top-down:
1. `~/.codex/AGENTS.md` - personal global guidance
1. `~/.code/AGENTS.md` - personal global guidance (Code will also read a legacy `~/.codex/AGENTS.md` if present)
2. `AGENTS.md` at repo root - shared project notes
3. `AGENTS.md` in the current working directory - sub-folder/feature specifics
+1 -1
View File
@@ -2,7 +2,7 @@
Save frequently used prompts as Markdown files and reuse them quickly from the slash menu.
- Location: Put files in `$CODEX_HOME/prompts/` (defaults to `~/.codex/prompts/`).
- Location: Put files in `$CODE_HOME/prompts/` (defaults to `~/.code/prompts/`; Code also reads legacy `~/.codex/prompts/`).
- File type: Only Markdown files with the `.md` extension are recognized.
- Name: The filename without the `.md` extension becomes the slash entry. For a file named `my-prompt.md`, type `/my-prompt`.
- Content: The file contents are sent as your message when you select the item in the slash popup and press Enter.
+2 -2
View File
@@ -6,10 +6,10 @@ Codex CLI **does** support OpenAI organizations with [Zero Data Retention (ZDR)]
OpenAI rejected the request. Error details: Status: 400, Code: unsupported_parameter, Type: invalid_request_error, Message: 400 Previous response cannot be used for this organization due to Zero Data Retention.
```
Ensure you are running `codex` with `--config disable_response_storage=true` or add this line to `~/.codex/config.toml` to avoid specifying the command line option each time:
Ensure you are running `codex` with `--config disable_response_storage=true` or add this line to `~/.code/config.toml` (Code still reads legacy `~/.codex/config.toml`) to avoid specifying the command line option each time:
```toml
disable_response_storage = true
```
See [the configuration documentation on `disable_response_storage`](./config.md#disable_response_storage) for details.
See [the configuration documentation on `disable_response_storage`](./config.md#disable_response_storage) for details.
+8 -6
View File
@@ -1,11 +1,12 @@
## @just-every/code v0.2.154
## @just-every/code v0.2.155
This release fixes a Shift+Tab crash and polishes multi‑agent command UX.
This release includes small fixes and improvements across auth, TUI, and docs.
### Changes
- TUI/Input: fix Shift+Tab crash.
- TUI/Agents: improve visibility for multi‑agent commands.
- TUI/Slash: make @ shortcut work with /solve and /plan.
- Auth: fix onboarding auth prompt gating.
- CLI: add long-run calculator script.
- TUI: add pulldown-cmark dependency to fix build.
- Docs: clarify config directories.
### Install
```
@@ -13,4 +14,5 @@ npm install -g @just-every/code@latest
code
```
Compare: https://github.com/just-every/code/compare/v0.2.153...v0.2.154
Compare: https://github.com/just-every/code/compare/v0.2.154...v0.2.155